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);

	}

測試下一分鐘的失效,登錄成功後一分鐘後再操作,

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瀏覽器出現

ok測試成功。

設置爲true後,直接阻止後面的登錄

代碼重構

前面的代碼只是能滿足功能,下面我們進行一下重構,主要是消除重複的字典值,以及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配置加到瀏覽器配置中

默認失效界面可以再配置文件中自定義實現

 

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)

 

springSecurity默認是基於session管理的框架,分佈式部署中會出現session不共享的問題 ,可以用redis來解決。

下面我們來開始改造session基於redis的支持 引入jar包

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

<!--redis-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

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

#數據源
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序列化

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相關信息了

3. 8090端口啓動項目  訪問 http://localhost:8090/user/me 同樣可以拿到認證信息

ok 說明redis切換成功了,下篇我們處理退出登錄處理。 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章