在执行认证过程之前,Spring Security将运行SecurityContextPersistenceFilter过滤器负责存储安请求之间的全上下文,
上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session作为存储器。
对于session管理,有三种:

  1. session超时处理:session有效的时间,超时后删除
  2. session并发控制:同个用户登录,是强制退出前一个登录,还是禁止后一个登录。
  3. 集群session管理:默认session是放在单个服务器的单个应用里,在集群中,会出现在一个节点应用登录后,session只能在该节点使用。另一个节点不能使用其他节点的session,还会需要登录,所以需要集群共用一个session

2.1、session超时

设置Session的超时,很简单,只需要在配置文件application.yml配置即可,如下为设置50秒:

  • Springboot2.0前的版本:

    1. spring:
    2. session:
    3. timeout: 50
  • Springboot2.0后的版本:

    server:
    servlet:
      session:
        timeout: 50
    

    上面设置Session失效时间为50s,
    实际源码TomcatEmbeddedServletContainerFactory类内部会取1分钟。源码内部转成分钟,然后设置给tomcat原生的StandardContext,所以一般设置为60秒的整数倍。

2.2、session超时处理

可以直接配置跳转,但是如果是前后端分离的话就不适合配置跳转的方式
image.png

2.2.1、超时跳转URL

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // session无效时跳转的url
            http.sessionManagement().invalidSessionUrl("/session/invalid");
            http.authorizeRequests()
                // 需要放行条跳转的url
                .antMatchers("/session/invalid").permitAll()
                .anyRequest().authenticated()
        }
    }
}

2.2.2、超时处理器

session无效时的处理策略,优先级比上面的高

抽象类

多个session处理类,定义抽象类

public abstract class AbstractSessionStrategy {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * 跳转的url
     */
    private String redirectUrl;
    /**
     * 重定向策略
     */
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    /**
     * 跳转前是否创建新的session
     */
    private boolean createNewSession = true;


      public AbstractSessionStrategy(String url) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(url), "url must start with '/' or with 'http(s)'");
        this.redirectUrl = url;
    }

    /**
     * session 无效处理
     *
     * @param request
     * @param response
     *
     * @throws IOException
     */
    protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if(createNewSession) {
            request.getSession();
        }

        String sourceUrl = request.getRequestURI();
        String targetUrl;

        if(StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
            targetUrl = destinationUrl + ".html";
            logger.info("session失效,跳转到" + targetUrl);
            redirectStrategy.sendRedirect(request, response, targetUrl);
        } else {
            String message = ResponseData.failure(CommResponseEnum.USER1023);
            if(isConcurrency()) {
                message = ResponseData.failure(CommResponseEnum.USER1024);
            }
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(message);
        }

    }

    /**
     * session失效是否是并发导致的
     *
     * @return
     */
    protected boolean isConcurrency() {
        return false;
    }

    public void setCreateNewSession(boolean createNewSession) {
        this.createNewSession = createNewSession;
    }

}

实现类

定义超时处理类,因为使用了构造类,所以无法使用@Component注解直接注入spring,
需要在配置类中进行注入

public class MyInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {

    public MyInvalidSessionStrategy(String invalidSessionUrl) {
        super(invalidSessionUrl);
    }

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException {
        onSessionInvalid(request, response);
    }

}

配置类

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @ConditionalOnMissingBean(InvalidSessionStrategy.class)
    public InvalidSessionStrategy invalidSessionStrategy() {
        return new MyInvalidSessionStrategy("/index");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置session无效处理策略
        http.sessionManagement().invalidSessionStrategy(invalidSessionStrategy());
        http.authorizeRequests()
            .antMatchers("/session/invalid").permitAll()
            .anyRequest().authenticated()
    }
}

2.3、session并发控制

默认下,我们可以在不同浏览器同时登录同一个用户,这样就会保存了多个Session
而有时,我们需要只能在一处地方登录,其他地方的登录就让前一个失效或不能登录。

2.3.1、后登录致前登录失效(踢下线)

在一个浏览器登录后,再到另一个浏览器登录,再回到前一个登录刷新页面,登录失效。
只需要设置最大session数为1即可

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
             // 设置session无效处理策略
            .invalidSessionStrategy(invalidSessionStrategy)
            // 设置同一个用户只能有一个登陆session
            .maximumSessions(1);
        http.authorizeRequests()
            .anyRequest().authenticated();
    }
}

上面设置maximumSessions设置为1后,只能有一个登录Session,
多个登录,后一个会把前一个登录的Sesson失效。
我们也可以自定义失效返回信息,有两种
image.png

1、失效跳转URL

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                .invalidSessionStrategy(invalidSessionStrategy)
                .maximumSessions(1)
                // 其他地方登录session失效处理URL
                .expiredUrl("/login");
        http.authorizeRequests()
            .anyRequest().authenticated();
    }
}

2、失效处理器

配置过期策略

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Bean
    @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
    public SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
        return new MyExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                .invalidSessionStrategy(invalidSessionStrategy)
                .maximumSessions(1)
                // 其他地方登录session失效处理策略
                .expiredSessionStrategy(sessionInformationExpiredStrategy());
        http.authorizeRequests()
                .anyRequest().authenticated()
    }
}

抽象类

参考上文抽象类

实现类

定义超时处理类,因为使用了构造类,所以无法使用@Component注解直接注入spring,
需要在配置类中进行注入

public class MyExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {

    public MyExpiredSessionStrategy(String invalidSessionUrl) {
        super(invalidSessionUrl);
    }

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
        onSessionInvalid(event.getRequest(), event.getResponse());
    }

    @Override
    protected boolean isConcurrency() {
        return true;
    }

}

2.3.2、前登录禁后登录(已登录禁止再次登录)

有时,我们在一个地方登录正在操作,不能被打断,这时就要禁止在其他地方登录导致当前的登录Session失效

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    @Bean
    @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
    public SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
        return new MyExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
            .invalidSessionStrategy(invalidSessionStrategy)
            .maximumSessions(1)
            // 设置为true,即禁止后面其它人的登录 
            .maxSessionsPreventsLogin(true)
            .expiredSessionStrategy(sessionInformationExpiredStrategy());
        http.authorizeRequests()
            .anyRequest().authenticated()
    }
}

禁止后登录后,可以通过如下方式判断异常并进行用户通知
在自定义的认证异常处理器中进行处理

@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private SecurityProperties securityProperties;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        log.info("登录失败");
        if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            if (exception instanceof SessionAuthenticationException){
                response.getWriter().write("用户已在其它地方登录,禁止当前登录...");
            }
            response.setContentType("application/json;charset=UTF-8");
            if(exception.getMessage().equals("坏的凭证")) {
                response.getWriter().write("账号或密码有误!");
            } else {
                response.getWriter().write(exception.getMessage());
            }
        } else {
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

2.4、session共享

参考:《11、session共享管理》

2.5、Logout注销

image.png

2.5.1、配置注销接口

配置注销接口和注销成功跳转接口

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登录的url, 默认为/logout
                .logoutUrl("/logout2")
                // 退出成功跳转URL,注意该URL不需要权限验证
                .logoutSuccessUrl("/logout/success").permitAll()
    }
}

2.5.2、配置注销处理器

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Bean
    @ConditionalOnMissingBean(LogoutSuccessHandler.class)
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new MyLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登录的url, 默认为/logout
                .logoutUrl("/logout2")
             // 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll
                //.logoutSuccessUrl("/logout/success").permitAll()
             //退出登录成功处理器
                .logoutSuccessHandler(logoutSuccessHandler())
    }
}

实现

@Slf4j
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    private String signOutUrl;

    public MyLogoutSuccessHandler(String signOutUrl) {
        this.signOutUrl = signOutUrl;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        log.info("退出成功");

        if(StringUtils.isBlank(signOutUrl)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString("退出成功"));
        } else {
            response.sendRedirect(request.getContextPath() + signOutUrl);
        }
    }
}

2.5.3、退出成功删除Cookie

默认退出后不会删除Cookie。可配置退出后删除:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout()
            // 退出登录的url, 默认为/logout
            .logoutUrl("/logout2")
            // 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll
            //.logoutSuccessUrl("/logout/success").permitAll()
            //退出登录成功处理器
            .logoutSuccessHandler(logoutSuccessHandler)
            // 退出登录删除指定的cookie
            .deleteCookies("JSESSIONID")
    }
}