目前为止我们已经主要实现了三种登录
它们都有一个共同点,用户认证成功的信息都是放在session中,下面我们处理session要面对的几个问题。
1.session超时处理
超时时间如何设置
失效路径策略配置
2.session并发控制
用户在a机器已经登录,又在b机器登录,是阻止在b登录,还是踢掉在a的登录?
3.session集群管理
分布式集群部署,如果还是用服务器session的话,可能会出现登录session在a机器上,而请求在b机器上,这样就会出现问题。
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 #默认单位是秒 不配置默认半小时失效
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);
}
测试下一分钟的失效,登录成功后一分钟后再操作,
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());
}
}
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序列化
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切换成功了,下篇我们处理退出登录处理。