zoukankan      html  css  js  c++  java
  • 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 如果帮到了你,给个小星星吧

  • 相关阅读:
    笔记44 Hibernate快速入门(一)
    tomcat 启用https协议
    笔记43 Spring Security简介
    笔记43 Spring Web Flow——订购披萨应用详解
    笔记42 Spring Web Flow——Demo(2)
    笔记41 Spring Web Flow——Demo
    Perfect Squares
    Factorial Trailing Zeroes
    Excel Sheet Column Title
    Excel Sheet Column Number
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/12149861.html
Copyright © 2011-2022 走看看