认证和SSO(三)-基于session的SSO存在的问题之session问题

1、两个session,三个有效期

  在上一节,实现的其实是基于session的sso,在该方案中有两个session,一个是客户端应用的session,一个是认证服务器的sessioin。一共有三个有效期,两个session的有效期,还有一个token令牌的有效期。他们的作用是如下:

1.1、客户端应用session的有效期,控制多长时间跳转一次认证服务器。

1.2、认证服务器session的有效期,控制多长时间需要用户输入一次用户名密码。

1.3、token有效期,控制登陆一次能访问多长时间微服务。

2、处理退出的用户体验问题

  因为有这两个session,我们目前的退出逻辑只是将客户端应用的session失效掉,但是并没有将认证服务器的session失效掉,修改推出逻辑,退出时客户端应用和认证服务器两个session都失效掉。

2.1、index.xml,/logout是springsecurity提供的退出方法

    //退出
    function logout() {
        $.get("/logout",function(){});
        //客户端session失效后,将认证服务器session也失效掉
        location.href = "http://auth.caofanqi.cn:9020/logout";
    }

2.2、登陆后点击退出后会跳转到认证服务器进行退出如下

2.3、点击Log Out,提示已经退出,跳转到认证服务器的/login?logout,是springsecurity默认的退出成功路径

2.4、再次输入用户名密码后,是如下情况

2.5、如果我们想要退出成功后跳转回我们自己的首页,可以做如下修改

  2.5.1、index.html添加重定向url

    //退出
    function logout() {
        $.get("/logout",function(){});
        //客户端session失效后,将认证服务器session也失效掉,添加重定向url
        location.href = "http://auth.caofanqi.cn:9020/logout?redirect_uri=http://web.caofanqi.cn:9000";
    }

  2.5.2、因为springsecurity默认处理退出请求的是DefaultLogoutPageGeneratingFilter这个过滤器类,我们在我们自己的类路径下,写一个一模一样的类(包名类名相同),来替代spring原有的(因为java的类加载机制)。修改代码中的html片段,使其能够自动提交,并携带redirect_uri参数。

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;

/**
 * Generates a default log out page.
 * 对spring security 提供的过滤器进行自定义
 *
 */
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)
                + "        <input type=hidden name=redirect_uri value="+request.getParameter("redirect_uri")+">"
//                + "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n"
                +          "<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();
    }
}

  2.5.3、WebSecurityConfig安全配置配置类,自定义退出成功处理器

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().and()
            .httpBasic().and()
            .logout()
            .logoutSuccessHandler(logoutSuccessHandler());
    }

    /**
     * 自定义退出成功处理器
     */
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        return (request, response, authentication) -> {
            String redirectUri = request.getParameter("redirect_uri");
            if (StringUtils.isNotBlank(redirectUri)) {
                response.sendRedirect(redirectUri);
            }
        };
    }

  2.5.4、启动各项目,登陆、获取订单详情、退出、再次登陆,到我们正常的页面(图略)。

3、认证服务器使用spring-session实现session共享

  认证服务器,在生产环境中,需要是高可用的,会集群部署,各节点需要进行session共享,我们使用spring-session来实现。

  spring-session为我们提供了多种实现方式,有redis,jdbc等,我们使用jdbc来实现。

3.1、pom中引入依赖

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-jdbc</artifactId>
        </dependency>

3.2、在spring-session-jdbc中提供了各种数据库的建表语句,copy出来执行即可,这里使用mysql会创建两张表。

3.3、application.yml配置文件

server:
  port: 9020
  servlet:
    session:
      #session超时时间设置
      timeout: 2592000

spring:
  application:
    name: auth-server
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/study-security?characterEncoding=UTF-8&useSSL=false
    username: root
    password: root
  #speing-session相关配置
  session:
    #指定存储类型,这里使用JDBC
    store-type: JDBC

3.4、启动各项目进行登陆,数据库中多出了记录

3.5、将webApp、认证服务器重启,再次刷新页面,不用再次登陆了,说明我们的认证服务器是高可用的了。

 

spring-session官方文档:https://docs.spring.io/spring-session/docs/2.2.1.RELEASE/reference/html5/

spring-session官方示例:https://github.com/spring-projects/spring-session/tree/2.2.1.RELEASE/spring-session-samples

 

 

项目源码:https://github.com/caofanqi/study-security/tree/dev-web-sso-session1

 

 

 

posted @ 2020-02-07 01:52  caofanqi  阅读(1346)  评论(0编辑  收藏  举报