前言
作为分布式项目,单点登录是必不可少的,文本基于之前的的博客(猛戳: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