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

Posted 我俩绝配

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud微服务安全实战_5-2_基于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>\\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 如果帮到了你,给个小星星吧

 

 

欢迎关注个人公众号一起交流学习:

 

 

以上是关于Spring Cloud微服务安全实战_5-2_基于session的SSO的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud微服务安全实战_6-1_微服务之间的通讯安全之概述

Spring Cloud微服务安全实战_4-10_用spring-cloud-zuul-ratelimit做限流

Spring Cloud微服务安全实战_3-5_API安全之常见问题

Spring Cloud微服务安全实战_3-8_API安全之登录

Spring Cloud微服务安全实战_3-4_API安全机制之认证

Spring Cloud微服务安全实战_5-5_refresh token失效处理