spring-security-oauth2(十五 ) 单机集群session管理

单机session管理

目前为止我们已经主要实现了三种登录

  • 用户+密码登录(表单)
  • 手机号+短信登录(表单)
  • 社交账单登录(oauth授权)

它们都有一个共同点,用户认证成功的信息都是放在session中,下面我们处理session要面对的几个问题。

1.session超时处理

     超时时间如何设置

     失效路径策略配置

2.session并发控制

   用户在a机器已经登录,又在b机器登录,是阻止在b登录,还是踢掉在a的登录?

3.session集群管理

   分布式集群部署,如果还是用服务器session的话,可能会出现登录session在a机器上,而请求在b机器上,这样就会出现问题。

session超时

springboot2.x的session超时设置已修改,请参看boot官方文档https://docs.spring.io/springboot/docs/2.0.3.RELEASE/reference/html/common-application-properties.html

关于spring官方文档的查阅,请参看下面这篇博客

spring系列官方文档查阅

超时配置如下:

# Tomcat
server:
  #port: 8070 qq回调端口要求80  也可以做接口转掉
  port: 80
  connection-timeout: 5000ms
  servlet:
    session:
      timeout: 60  #默认单位是秒  不配置默认半小时失效

实现session超时提醒

com.rui.tiger.auth.browser.config.BrowserSecurityConfig#configure,同时要记得对失效路径放行

.userDetailsService(userDetailsService)
   .and()
.sessionManagement()
.invalidSessionUrl("/session/invalid")//session失效地址

com.rui.tiger.auth.browser.controller.BrowserRequireController#sessionInvalid

/**
	 * session失效
	 * @return
	 */
	@GetMapping("/session/invalid")
	@ResponseStatus(HttpStatus.UNAUTHORIZED)
	public SimpleResponse sessionInvalid(){
		String sessionInvalidTipMessage="session已失效请重新登录";
		return new SimpleResponse(sessionInvalidTipMessage);

	}

测试下一分钟的失效,登录成功后一分钟后再操作,

spring-security-oauth2(十五 ) 单机集群session管理_第1张图片

session并发控制

com.rui.tiger.auth.browser.config.BrowserSecurityConfig#configure

package com.rui.tiger.auth.browser.config;

import com.rui.tiger.auth.browser.session.TigerExpiredSessionStrategy;
import com.rui.tiger.auth.core.config.AbstractChannelSecurityConfig;
import com.rui.tiger.auth.core.config.CaptchaSecurityConfig;
import com.rui.tiger.auth.core.config.SmsAuthenticationSecurityConfig;
import com.rui.tiger.auth.core.properties.SecurityConstants;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.social.security.SpringSocialConfigurer;

import javax.sql.DataSource;

/**
 * 浏览器security配置类
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {

	@Autowired
	private SecurityProperties securityProperties;
	@Autowired
	private DataSource dataSource;
	@Autowired
	private UserDetailsService userDetailsService;
	@Autowired
	private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;//短信登陆配置
	@Autowired
	private CaptchaSecurityConfig captchaSecurityConfig;//验证码配置
	@Autowired
	private SpringSocialConfigurer tigerSpringSocialConfigurer;

	/**
	 * 密码加密解密
	 *
	 * @return
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	/**
	 * 记住我持久化数据源
	 * JdbcTokenRepositoryImpl  CREATE_TABLE_SQL 建表语句可以先在数据库中执行
	 *
	 * @return
	 */
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
		jdbcTokenRepository.setDataSource(dataSource);
		//第一次会执行CREATE_TABLE_SQL建表语句 后续会报错 可以关掉
		//jdbcTokenRepository.setCreateTableOnStartup(true);
		return jdbcTokenRepository;
	}

	/**
	 * 核心配置
	 * @param http
	 * @throws Exception
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		/**
		 * 表单密码配置
		 */
		applyPasswordAuthenticationConfig(http);

		http
				.apply(captchaSecurityConfig)
					.and()
				.apply(smsAuthenticationSecurityConfig)
					.and()
				.apply(tigerSpringSocialConfigurer)
					.and()
				.rememberMe()
				.tokenRepository(persistentTokenRepository())
				.tokenValiditySeconds(securityProperties.getBrowser().getRemberMeSeconds())
				.userDetailsService(userDetailsService)
					.and()
				.sessionManagement()
				.invalidSessionUrl("/session/invalid")//session失效跳转地址
				.maximumSessions(1)//最大session并发数
				.maxSessionsPreventsLogin(false)//true达到并发数后阻止登录,false 踢掉之前的登录
				.expiredSessionStrategy(new TigerExpiredSessionStrategy())//并发策略
				.and()
					.and()
				.authorizeRequests()
				.antMatchers(
						SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,//权限认证
						SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,//手机
						securityProperties.getBrowser().getLoginPage(),//登录页面
						SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*",//  /captcha/* 验证码放行
						securityProperties.getBrowser().getSignupUrl(),
						//这个第三方自定义权限 后续抽离出去 可配置
						"/user/regist",
						"/index.html",
						"/session/invalid")
				.permitAll()
				.anyRequest()
				.authenticated()
					.and()
				.csrf().disable();

	}

}

1.开启测试,chrome浏览器登录成功,并访问用户认证信息

2.360浏览器再重新登录,并访问用户认证信息成功,这时已经踢掉chrome上的用户了

3.再次访问chrome浏览器出现

spring-security-oauth2(十五 ) 单机集群session管理_第2张图片

ok测试成功。

spring-security-oauth2(十五 ) 单机集群session管理_第3张图片

设置为true后,直接阻止后面的登录

spring-security-oauth2(十五 ) 单机集群session管理_第4张图片

代码重构

前面的代码只是能满足功能,下面我们进行一下重构,主要是消除重复的字典值,以及session的可配置,同时提示要支持返回json或html,直接上代码。

常量字典添加默认失效界面

 

package com.rui.tiger.auth.core.properties;

/**
 * @author CaiRui
 * @date 2019-02-27 08:44
 */
public class SessionProperties {


	private int maximumSessions=1;//session最大并发数

	private boolean maxSessionsPreventsLogin;//默认false 会踢掉之前已经登录的信息

	private String invalidSessionUrl=SecurityConstants.DEFAULT_SESSION_INVALID_URL;//默认失效界面


	public int getMaximumSessions() {
		return maximumSessions;
	}

	public void setMaximumSessions(int maximumSessions) {
		this.maximumSessions = maximumSessions;
	}

	public boolean isMaxSessionsPreventsLogin() {
		return maxSessionsPreventsLogin;
	}

	public void setMaxSessionsPreventsLogin(boolean maxSessionsPreventsLogin) {
		this.maxSessionsPreventsLogin = maxSessionsPreventsLogin;
	}

	public String getInvalidSessionUrl() {
		return invalidSessionUrl;
	}

	public void setInvalidSessionUrl(String invalidSessionUrl) {
		this.invalidSessionUrl = invalidSessionUrl;
	}
}
SecurityConstants 添加默认失效地址
/**
 * session失效默认跳转地址
 */
public static final String DEFAULT_SESSION_INVALID_URL = "/tiger-session-invalid.html";

session配置加到浏览器配置中

spring-security-oauth2(十五 ) 单机集群session管理_第5张图片

默认失效界面可以再配置文件中自定义实现

spring-security-oauth2(十五 ) 单机集群session管理_第6张图片

 

session失效及并发登录处理类

package com.rui.tiger.auth.browser.session;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * session失效父类
 * (过期和并发失效共同业务逻辑处理)
 *
 * @author CaiRui
 * @date 2019-02-27 09:10
 */
@Slf4j
public class AbstractSessionInvalidStrategy {

	/**
	 * 跳转的url
	 */
	private String destinationUrl;
	/**
	 * 跳转之前是否创建新的session
	 */
	private boolean createNewSession = true;
	/**
	 * 默认跳转策略
	 */
	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

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

	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";
			log.info("session失效,跳转到"+targetUrl);
			redirectStrategy.sendRedirect(request,response,targetUrl);
		} else {
			String message = "session已失效";
			if(isConcurrency()){
				message = message + ",有可能是并发登录导致的";
			}
			response.setStatus(HttpStatus.UNAUTHORIZED.value());
			response.setContentType("application/json;charset=UTF-8");
			response.getWriter().write(message);
		}
	}

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

	/**
	 * Determines whether a new session should be created before redirecting (to
	 * avoid possible looping issues where the same session ID is sent with the
	 * redirected request). Alternatively, ensure that the configured URL does
	 * not pass through the {@code SessionManagementFilter}.
	 *
	 * @param createNewSession defaults to {@code true}.
	 */
	public void setCreateNewSession(boolean createNewSession) {
		this.createNewSession = createNewSession;
	}


}

并发失效

package com.rui.tiger.auth.browser.session;

import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 并发失效策略
 * @author CaiRui
 * @date 2019-02-26 18:23
 */
public class TigerExpiredSessionStrategy extends AbstractSessionInvalidStrategy implements SessionInformationExpiredStrategy {

	public TigerExpiredSessionStrategy(String destinationUrl) {
		super(destinationUrl);
	}

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

	/**
	 * 并发导致的失效
	 * @return
	 */
	protected boolean isConcurrency() {
		return true;
	}
}

过期失效

package com.rui.tiger.auth.browser.session;

import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 过期失效策略
 * @author CaiRui
 * @date 2019-02-27 09:12
 */
public class TigerInvalidSessionStrategy extends AbstractSessionInvalidStrategy implements InvalidSessionStrategy {

	public TigerInvalidSessionStrategy(String destinationUrl) {
		super(destinationUrl);
	}

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

}

配置类可以覆盖自定义实现

package com.rui.tiger.auth.browser.config;

import com.rui.tiger.auth.browser.session.TigerExpiredSessionStrategy;
import com.rui.tiger.auth.browser.session.TigerInvalidSessionStrategy;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

/**
 * 失效默认实现
 * 自定义重写可以覆盖此实现
 * @author CaiRui
 * @date 2019-02-27 12:15
 */
@Configuration
public class BrowserSecurityBeanConfig {
	@Autowired
	private SecurityProperties securityProperties;

	@Bean
	@ConditionalOnMissingBean(InvalidSessionStrategy.class)
	public InvalidSessionStrategy invalidSessionStrategy(){
		return new TigerInvalidSessionStrategy(securityProperties.getBrowser().getSession().getInvalidSessionUrl());
	}

	@Bean
	@ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
	public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
		return new TigerExpiredSessionStrategy(securityProperties.getBrowser().getSession().getInvalidSessionUrl());
	}
}

 

集群session管理(redis)

 

spring-security-oauth2(十五 ) 单机集群session管理_第7张图片

springSecurity默认是基于session管理的框架,分布式部署中会出现session不共享的问题 ,可以用redis来解决。

下面我们来开始改造session基于redis的支持 引入jar包

依赖:特别注意:spring-session:1.3.3.RELEASE在高版本的spring boot autoconfig中已经不支持了;引入下面依赖


 
     org.springframework.boot
     spring-boot-starter-data-redis

    org.springframework.session
    spring-session-data-redis

配置文件修改 部分代码如下

#数据源
spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://my.yunout.com:3306/tiger_study?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root
    # 配置Druid连接池
    type: com.alibaba.druid.pool.DruidDataSource
  session:
    store-type: redis
    # 单位秒 默认最短一分钟 默认半小时
    timeout: 300
  redis:
    host: my.yunout.com
    port: 6379
    password: kruiredis0130
    database: 0

前面的验证码也要进行改造,redis不能讲BufferdImage序列化

spring-security-oauth2(十五 ) 单机集群session管理_第8张图片

com.rui.tiger.auth.core.captcha.AbstractCaptchaProcessor#save

/**
	 * 保存验证码到session中
	 * @param request
	 * @param captcha
	 */
	private void save(ServletWebRequest request, C captcha) {
		//redis不支持bufferImage序列化
		CaptchaVo captchaVo=new CaptchaVo(captcha.getCode(),captcha.getExpireTime());
		sessionStrategy.setAttribute(request, CAPTCHA_SESSION_KEY +getCondition().getCode(),captchaVo);
	}

ok 我们再同一浏览器中分别启动8070和8090端口来开启测试

1.登录localhost:8070/tiger-login.html并访问/user/me

 

2.登录后看redis这里已经有spring-session相关信息了

spring-security-oauth2(十五 ) 单机集群session管理_第9张图片

3. 8090端口启动项目  访问 http://localhost:8090/user/me 同样可以拿到认证信息

ok 说明redis切换成功了,下篇我们处理退出登录处理。 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(spring-security-oauth2(十五 ) 单机集群session管理)