上一篇将OAuth2授权模式的password模式改造成了授权码模式,并初步实现了一个前后端分离架构下基于session的微服务的SSO。用户在客户端点击登录,会跳转到认证服务器的登录页面进行登录,登录成功后,认证服务器回调到客户端应用的callback方法,并携带了授权码,客户端拿着授权码去认证服务器换取access_token ,客户端拿到access_token后存到自己的session,就认为该用户已登录成功。
上边这个流程是一个基于session的SSO,其中有三个效期:
1,客户端应用的session的有效期,控制着多长时间跳转一次认证服务器
2,认证服务器的session的有效期 , 控制多长时间需要用户输入一次用户名密码
3,access_token的有效期,控制着登录一次能访问多久的微服务
如上篇所说,目前还存在着一系列的问题,比如点击退出,只是将客户端应用的session失效掉了,并没有将认证服务器的session失效,用户退出后,点击登录按钮,重定向到认证服务器,由于认证服务器的session并没有失效,所以认证服务器会自动回调到客户端,客户端表现就是直接就又登录了,给用户的感觉就是点了退出按钮,但是并没有退出去。下边就来解决这个问题,思路也很简单,点击退出按钮的时候,同时将客户端和认证服务器的session都失效。下面开始写代码。
处理退出登录逻辑
退出按钮的处理:
1,将自己客户端应用的session失效
2,将认证服务器的session失效,
这样,再次点击退出按钮,客户端session失效后,又跳转到了认证服务器,这是认证服务器默认给的一个提示
点击确定,页面停留在了认证服务器的默认的登录页面:
输入用户名(随便),密码(123456 认证服务器写死的),点击sign in,
会跳转到了认证服务器默认的首页,没有,所以出现了404。
为什么直接在客户端应用点击登录按钮,登录成功后就可以跳回到客户端应用?看一下在客户端应用点击登录按钮的处理:
里面有一个 redirect_uri 参数,这样的请求,认证服务器在登录成功后,就知道要跳转到redirect_uri 。但是点击退出后出现的登录页面 ,是由【退出】触发的,认证服务器是不知道登录成功后要跳转到admin应用的。所以,要做退出的处理,让认证服务器知道,退出后要跳转到指定的uri,思路就是在退出的请求上,加一个 redirect_uri的参数,重写认证服务器的退出逻辑,退出后跳转到redirect_uri 即可。
请求认证服务器的退出逻辑的请求上,加上 redirect_uri=http://admin.nb.com:8080/index
在认证服务器上找到Spring处理退出逻辑的过滤器 org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter :
/** * Generates a default log out page. * * @author Rob Winch * @since 5.1 */ public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = request -> Collections .emptyMap(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (this.matcher.matches(request)) { renderLogout(request, response); } else { filterChain.doFilter(request, response); } } private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException { String page = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + " <head>\n" + " <meta charset=\"utf-8\">\n" + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n" + " <meta name=\"description\" content=\"\">\n" + " <meta name=\"author\" content=\"\">\n" + " <title>Confirm Log Out?</title>\n" + " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n" + " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n" + " </head>\n" + " <body>\n" + " <div class=\"container\">\n" + " <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n" + " <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n" + " </form>\n" + " </div>\n" + " </body>\n" + "</html>"; response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(page); } /** * Sets a Function used to resolve a Map of the hidden inputs where the key is the * name of the input and the value is the value of the input. Typically this is used * to resolve the CSRF token. * @param resolveHiddenInputs the function to resolve the inputs */ public void setResolveHiddenInputs( Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) { Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); this.resolveHiddenInputs = resolveHiddenInputs; } private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) { sb.append("<input name=\"").append(input.getKey()).append("\" type=\"hidden\" value=\"").append(input.getValue()).append("\" />\n"); } return sb.toString(); } }
1,重写退出表单源码
这就是处理退出逻辑的过滤器,其中的html就是之前看到的让用户确认退出的页面,在认证服务器项目新建一个一模一样的包,将上边的类copy进去,由于java的类加载机制,自己写的类会优先于spring的类加载,java会加载我们自己写的类,而不加载spring包里的类:
重写后的类源码:
package org.springframework.security.web.authentication.ui; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.function.Function; /** * 重写退出逻辑,由于java的类加载机制,会优先执行自己的类,就不加载spring的了 * 这里有一个默认的 确认退出页面,可以定制 * 这里注释掉确认退出的提示语,直接写一段js脚本,提交退出表单 * 从request里获取到退出逻辑携带的 redirect_uri 参数,放入退出表单的隐藏input, * 这样在重写退出成功handler时,可以拿出这个参数,做跳转 * Generates a default log out page. * * @author Rob Winch * @since 5.1 */ public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = request -> Collections .emptyMap(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (this.matcher.matches(request)) { renderLogout(request, response); } else { filterChain.doFilter(request, response); } } private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException { String page = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + " <head>\n" + " <meta charset=\"utf-8\">\n" + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n" + " <meta name=\"description\" content=\"\">\n" + " <meta name=\"author\" content=\"\">\n" + " <title>Confirm Log Out?</title>\n" + " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n" + " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n" + " </head>\n" + " <body>\n" + " <div class=\"container\">\n" + " <form id=\"logoutForm\" class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n" // + " <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + renderHiddenInputs(request) // + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n" + "<input type='hidden' name='redirect_uri' value="+request.getParameter("redirect_uri")+"/>" + "<script>document.getElementById('logoutForm').submit()</script>" + " </form>\n" + " </div>\n" + " </body>\n" + "</html>"; response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(page); } /** * Sets a Function used to resolve a Map of the hidden inputs where the key is the * name of the input and the value is the value of the input. Typically this is used * to resolve the CSRF token. * @param resolveHiddenInputs the function to resolve the inputs */ public void setResolveHiddenInputs( Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) { Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); this.resolveHiddenInputs = resolveHiddenInputs; } private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) { sb.append("<input name=\"").append(input.getKey()).append("\" type=\"hidden\" value=\"").append(input.getValue()).append("\" />\n"); } return sb.toString(); } }
上述代码中的荧光绿色的是注释掉的两行代码,这两行代码是说显示的让用户确认退出登录的提示,这里给注释掉,让用户看不到退出提醒
上述代码中的荧光黄色的是新添加的代码,给退出登录的表单加了个id,然后在表单里写了个隐藏域,name= redirect_uri,值从request里获取,用于自定义退出成功Handler里,可以重定向到该路径。最后新增一个JavaScript脚本,自动提交表单。
如果你就想给用户一个退出提示,可以重写这个表单的样式。
2,下面自定义退出登录成功处理器
3,配置退出成功处理器
实验
启动四个微服务
访问客户端应用 http://admin.nb.com:8080/index/
点击去登录,跳转到了认证服务器的登录页面
登录成功,回调到客户端应用admin,点击获取订单信息,获取到了订单信息
点击退出登录,先是在客户端应用将session失效,然后再去认证服务器上做退出登录操作()
然后又跳转到了客户端应用的index页
总结
本篇解决了上篇遗留的问题(点击退出登录只是在客户端应用做session失效操作,当再次点击登录后,由于认证服务器的session还有效,用户不用输入用户名密码直接就登录了,给人的感觉是没有彻底退出去)。
本节在客户端应用做退出操作的同时,也在认证服务器上将session失效掉,让用户彻底退出登录。思路是在点击【退出登录】按钮的同时做两件事,一是让客户端应用的session失效,然后再发一个请求到认证服务器的 /logout 端点,这是spring OAuth自带的退出登录过滤器,同时并携带一个redirect_uri参数,让认证服务器退出登录之后,知道跳转到客户端应用去。否则认证服务器默认的退出逻辑是,退出后跳转到了认证服务器的首页,由于没有做首页,所以返回了一个404,我们重写了退出登录类org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter ,让退出登录表单自动提交,实现了退出成功handler,重定向到了客户端应用退出时携带过来的redirect_uri。
本篇代码github : https://github.com/lhy1234/springcloud-security/tree/chapt-5-3-sso-session 如果帮到了你,给个小星星吧
欢迎关注个人公众号一起交流学习: