Spring Cloud security services combat micro _5-2_ session-based SSO

Previous to OAuth2 authorization mode password mode transformed into an authorization code mode, and realized the lower end of the separation architecture before and after a session-based micro-services SSO. Users click on the login client, the login page will jump to the authentication server login, the login is successful, the authentication server to the callback method callback client application, and carry the authorization code, the client holding an authorization code to the authentication server exchange access_token, after the client to get the access_token keep their session, it assumes the user has successfully logged in.

 On top of this process is a session-based SSO , which has three lifetime:

  1, session of the validity of the client application, controlling how long jump once the authentication server

  2, session authentication server is valid, control how long it takes the user to enter a user name and password

  3, access_token valid, log in once to access controls how long the micro-services

Articles mentioned above, currently there are still a series of problems, such as click to exit, but the client application session failed out, and not the session authentication server failure, the user logs out, click on the login button redirected to the authentication server, authentication server because the session has not failed, so the authentication server will automatically call back to the client, the client performance is directly and logged, giving the user the impression that point the exit button, but did not make way. Below you to solve this problem, the idea is simple, click on the Exit button, while the session client and the authentication server expire. Let's start writing code.

Log processing logic

 Exit button process:

 1, the session own client application failure  

2, the session authentication server failure,

Thus, the exit button is clicked again, after the failure of the client session, and jump to the authentication server, which is the default authentication server to a prompt

  Click OK, pages remain in the default login page authentication server:

 Enter your user name (casual), password (123456 authentication server hardcoded), click 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 如果帮到了你,给个小星星吧

Guess you like

Origin www.cnblogs.com/lihaoyang/p/12149861.html