• Spring Cloud微服务安全实战_5-3_基于session的SSO


    上一篇将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>
    "
                    + "<html lang="en">
    "
                    + "  <head>
    "
                    + "    <meta charset="utf-8">
    "
                    + "    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    "
                    + "    <meta name="description" content="">
    "
                    + "    <meta name="author" content="">
    "
                    + "    <title>Confirm Log Out?</title>
    "
                    + "    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    "
                    + "    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
    "
                    + "  </head>
    "
                    + "  <body>
    "
                    + "     <div class="container">
    "
                    + "      <form class="form-signin" method="post" action="" + request.getContextPath() + "/logout">
    "
                    + "        <h2 class="form-signin-heading">Are you sure you want to log out?</h2>
    "
                    + renderHiddenInputs(request)
                    + "        <button class="btn btn-lg btn-primary btn-block" type="submit">Log Out</button>
    "
                    + "      </form>
    "
                    + "    </div>
    "
                    + "  </body>
    "
                    + "</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("" />
    ");
            }
            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>
    "
                    + "<html lang="en">
    "
                    + "  <head>
    "
                    + "    <meta charset="utf-8">
    "
                    + "    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    "
                    + "    <meta name="description" content="">
    "
                    + "    <meta name="author" content="">
    "
                    + "    <title>Confirm Log Out?</title>
    "
                    + "    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    "
                    + "    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
    "
                    + "  </head>
    "
                    + "  <body>
    "
                    + "     <div class="container">
    "
                    + "      <form id="logoutForm" class="form-signin" method="post" action="" + request.getContextPath() + "/logout">
    "
    //                + "        <h2 class="form-signin-heading">Are you sure you want to log out?</h2>
    "
                    + renderHiddenInputs(request)
    //                + "        <button class="btn btn-lg btn-primary btn-block" type="submit">Log Out</button>
    "
                    +  "<input type='hidden' name='redirect_uri' value="+request.getParameter("redirect_uri")+"/>"
                    +  "<script>document.getElementById('logoutForm').submit()</script>"
                    + "      </form>
    "
                    + "    </div>
    "
                    + "  </body>
    "
                    + "</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("" />
    ");
            }
            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 如果帮到了你,给个小星星吧

  • 相关阅读:
    Zabbix使用grafana展示图形
    Jumpserver堡垒机
    ELK之使用filebeat的多行过滤插件把多行合并成一行
    [0] Enterprise Architect
    [0] LoadRunner
    [0] Axure
    [0] MVC&MVP&MVVM差异点
    [0] 传统架构与领域模式架构对比
    [转] .NET领域驱动设计—看DDD是如何运用设计模式颠覆传统架构
    [转] .NET领域驱动设计—实践(穿过迷雾走向光明)
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/12149861.html
Copyright © 2020-2023  润新知