單機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官方文檔的查閱,請參看下面這篇博客
超時配置如下:
# 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切換成功了,下篇我們處理退出登錄處理。