前言
作為分布式項目,單點登錄是必不可少的,文本基于之前的的博客(猛戳:springcloud系列——zuul 動態路由,springboot系列——redis)記錄zuul配合redis實現一個簡單的sso單點登錄實例
sso單點登錄思路:
1、訪問分布式系統的任意請求,被zuul的filter攔截過濾
2、在run方法里實現過濾規則:cookie有令牌accesstoken且作為key存在于redis,或者訪問的是登錄頁面、登錄請求則放行
3、否則,將重定向到sso-server的登錄頁面且原先的請求路徑作為一個參數;response.sendredirect("http://localhost:10010/sso-server/sso/loginpage?url=" + url);
4、登錄成功,sso-server生成accesstoken,并作為key(用戶名+時間戳,這里只是demo,正常項目的令牌應該要更為復雜)存到redis,value值存用戶id作為value(或者直接存儲可暴露的部分用戶信息也行)設置過期時間(我這里設置3分鐘);設置cookie:new cookie("accesstoken",accesstoken);,設置maxage(60*3);、path("/");
5、sso-server單點登錄服務負責校驗用戶信息、獲取用戶信息、操作redis緩存,提供接口,在eureka上注冊
代碼編寫
sso-server
首先我們創建一個單點登錄服務sso-server,并在eureka上注冊(創建項目請參考之前的springcloud系列博客跟springboot系列——redis)
login.html
我們這里需要用到頁面,要先maven引入thymeleaf
1
2
3
4
|
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency> |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<!doctype html> <html xmlns:th= "http://www.thymeleaf.org" > <head> <meta charset= "utf-8" > <title>登錄頁面</title> </head> <body> <form action= "/sso-server/sso/login" method= "post" > <input name= "url" type= "hidden" th:value= "${url}" /> 用戶名:<input name= "username" type= "text" /> 密碼:<input name= "password" type= "password" /> <input value= "登錄" type= "submit" /> </form> </body> </html> |
提供如下接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
@restcontroller @enableeurekaclient @springbootapplication public class ssoserverapplication { public static void main(string[] args) { springapplication.run(ssoserverapplication. class , args); } @autowired private stringredistemplate template; /** * 判斷key是否存在 */ @requestmapping ( "/redis/haskey/{key}" ) public boolean haskey( @pathvariable ( "key" ) string key) { try { return template.haskey(key); } catch (exception e) { e.printstacktrace(); return false ; } } /** * 校驗用戶名密碼,成功則返回通行令牌(這里寫死huanzi/123456) */ @requestmapping ( "/sso/checkusernameandpassword" ) private string checkusernameandpassword(string username, string password) { //通行令牌 string flag = null ; if ( "huanzi" .equals(username) && "123456" .equals(password)) { //用戶名+時間戳(這里只是demo,正常項目的令牌應該要更為復雜) flag = username + system.currenttimemillis(); //令牌作為key,存用戶id作為value(或者直接存儲可暴露的部分用戶信息也行)設置過期時間(我這里設置3分鐘) template.opsforvalue().set(flag, "1" , ( long ) ( 3 * 60 ), timeunit.seconds); } return flag; } /** * 跳轉登錄頁面 */ @requestmapping ( "/sso/loginpage" ) private modelandview loginpage(string url) { modelandview modelandview = new modelandview( "login" ); modelandview.addobject( "url" , url); return modelandview; } /** * 頁面登錄 */ @requestmapping ( "/sso/login" ) private string login(httpservletresponse response, string username, string password, string url) { string check = checkusernameandpassword(username, password); if (!stringutils.isempty(check)) { try { cookie cookie = new cookie( "accesstoken" , check); cookie.setmaxage( 60 * 3 ); //設置域 // cookie.setdomain("huanzi.cn"); //設置訪問路徑 cookie.setpath( "/" ); response.addcookie(cookie); //重定向到原先訪問的頁面 response.sendredirect(url); } catch (ioexception e) { e.printstacktrace(); } return null ; } return "登錄失敗" ; } } |
zuul-server
引入feign,用于調用sso-server服務
1
2
3
4
5
|
<!-- feign --> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-openfeign</artifactid> </dependency> |
創建ssofeign.java接口
1
2
3
4
5
6
7
8
9
|
@feignclient (name = "sso-server" , path = "/" ) public interface ssofeign { /** * 判斷key是否存在 */ @requestmapping ( "redis/haskey/{key}" ) public boolean haskey( @pathvariable ( "key" ) string key); } |
啟動類加入@enablefeignclients注解,否則啟動會報錯,無法注入ssofeign對象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@enablezuulproxy @enableeurekaclient @enablefeignclients @springbootapplication public class zuulserverapplication { public static void main(string[] args) { springapplication.run(zuulserverapplication. class , args); } @bean public accessfilter accessfilter() { return new accessfilter(); } } |
修改accessfilter過濾邏輯,注入feign接口,用于調用sso-server檢查redis,修改run方法的過濾邏輯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
/** * zuul過濾器,實現了路由檢查 */ public class accessfilter extends zuulfilter { @autowired private ssofeign ssofeign; /** * 通過int值來定義過濾器的執行順序 */ @override public int filterorder() { // predecoration之前運行 return pre_decoration_filter_order - 1 ; } /** * 過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型: * public static final string error_type = "error"; * public static final string post_type = "post"; * public static final string pre_type = "pre"; * public static final string route_type = "route"; */ @override public string filtertype() { return pre_type; } /** * 過濾器的具體邏輯 */ @override public object run() { requestcontext ctx = requestcontext.getcurrentcontext(); httpservletrequest request = ctx.getrequest(); httpservletresponse response = ctx.getresponse(); //訪問路徑 string url = request.getrequesturl().tostring(); //從cookie里面取值(zuul丟失cookie的解決方案:https://blog.csdn.net/lindan1984/article/details/79308396) string accesstoken = request.getparameter( "accesstoken" ); for (cookie cookie : request.getcookies()) { if ( "accesstoken" .equals(cookie.getname())) { accesstoken = cookie.getvalue(); } } //過濾規則:cookie有令牌且存在于redis,或者訪問的是登錄頁面、登錄請求則放行 if (url.contains( "sso-server/sso/loginpage" ) || url.contains( "sso-server/sso/login" ) || (!stringutils.isempty(accesstoken) && ssofeign.haskey(accesstoken))) { ctx.setsendzuulresponse( true ); ctx.setresponsestatuscode( 200 ); return null ; } else { ctx.setsendzuulresponse( false ); ctx.setresponsestatuscode( 401 ); //重定向到登錄頁面 try { response.sendredirect( "http://localhost:10010/sso-server/sso/loginpage?url=" + url); } catch (ioexception e) { e.printstacktrace(); } return null ; } } /** * 返回一個boolean類型來判斷該過濾器是否要執行 */ @override public boolean shouldfilter() { return true ; } } |
修改配置文件,映射sso-server代理路徑,超時時間與丟失cookie的解決
1
2
3
4
5
6
7
8
|
zuul.routes.sso-server.path=/sso-server/** zuul.routes.sso-server.service-id=sso-server zuul.host.socket-timeout-millis= 60000 zuul.host.connect-timeout-millis= 10000 #zuul丟失cookie的解決方案:https: //blog.csdn.net/lindan1984/article/details/79308396 zuul.sensitive-headers= |
測試效果
啟動eureka、zuul-server、sso-server、config-server、myspringboot、springdatajpa(由兩個應用組成,實現了ribbon負載均衡),記得啟動我們的rabbitmq服務和redis服務!
剛開始,沒有cookie且無redis的情況下,瀏覽器訪問http://localhost:10010/myspringboot/feign/ribbon,被zuul-server攔截重定向到sso-server登錄頁面
開始登錄校驗,為了方便演示,我將密碼的type改成text
登錄失敗,返回提示語
登錄成功,重定向到之前的請求
cookie的值,以及過期時間
3分鐘后我們再次訪問http://localhost:10010/myspringboot/feign/ribbon,cookie、redis失效,需要從新登錄
后記
sso單點登錄就記錄到這里,這里只是實現了單機版的sso,以后在進行升級吧。
問題報錯:我們在sso-server設置cookie后,在zuul-server的run方法里獲取不到設置的cookie,去瀏覽器查看,cookie沒有設置成功,zuul丟失cookie
解決方案
我們是使用spring cloud zuul作為api-gateway實踐中,發現默認zuul會過濾掉cookie等header信息,有些業務場景需要傳遞這些信息該怎么處理呢?
處理方式 在api-gateway的application.properties文件中添加 zuul.sensitive-headers=
問題原因
負責根據serviceid來路由的ribbonroutingfilter在route之前會調用proxyrequesthelper的buildzuulrequestheaders(request)來重新組裝一個新的header。
在buildzuulrequestheaders方法中會對requsetheader中的每一項調用isincludedheader(name)來判斷當前項是否應該留在新的header中,如下圖,如果當前項在ignored_headers(需要忽略的信息)中,就不會在新header中保留。
predecorationfilter過濾器會調用proxyrequesthelper的addignoredheaders方法把敏感信息(zuulproperties的sensitiveheaders屬性)添加到請求上下文的ignored_headers中
sensitiveheaders的默認值初始值是"cookie", "set-cookie", "authorization"這三項,可以看到cookie被列為了敏感信息,所以不會放到新header中
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/huanzi-qch/p/10249227.html