述
在之前我们已经实现了三种方式的用户登录:
- 用户名密码登录
- 短信验证码登录
- 第三方社交网站登录
前面两种是使用表单提交的方式登录,后面一种是走 OAuth 流程登录, 但是不管是哪种类型的登陆,最后的用户认证信息都会放到 session 中去
对于session的一些配置,比如失效时间, 失效后的处理等该如何配置,下面来看一下
session失效时间
session的超时时间可以在 application.yml
中,加入以下配置:1
2
3server:
session:
timeout: 10
这里的单位是秒, 设置10秒, 在系统实际的运行过程中,10秒过后其实并不会失效
看下源码, 他这里会做一个分钟的转换,如果小于1分钟,默认就是1分钟
如果不做任何配置的话,默认是30分钟的
session超时配置
首先看一下简单的实现
在浏览器的配置 BrowserSecurityConfig
中,加入以下配置:1
2
3.and()
.sessionManagement()
.invalidSessionUrl("/session/invalid")
这里是配置 session 失效后跳转的 url, 这个可以是一个页面,或者是一个接口,这个需要自己去实现,记得加到不需要授权的请求中去
1 | @GetMapping("/session/invalid") |
session并发控制
很多情况下,系统中,一个用户只能在一个地方登录,在两个地方登录的话会把之前的踢下线,或者说是限制登录,下面看一下这两种情况应该如何处理
第一种是新的客户端登录后,旧的客户端踢掉,下线,需要在 BrowserSecurityConfig
中加入以下配置
1 | // 限制同一个用户只能有一个session登录 |
这里有两种解决方案,第一个 expiredUrl()
方法,可以跳转到一个请求里面去,还有一种是 expiredSessionStrategy()
方法,这里可以配置一个策略类, 看一下这个类的代码:1
2
3
4
5
6
7
8public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
// 该对象能获取到访问失效前的url地址
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write("session并发登录");
}
}
这里要继承 SessionInformationExpiredStrategy
然后重写 onExpiredSessionDetected()
,方法的参数中可以拿到之前的访问失效前的请求信息
再来看下第二种的并发解决方案,配置如下:1
2.maximumSessions(1)
.maxSessionsPreventsLogin(true)
第一个配置是,限制用户只能有一个session登录,然后下面的 .maxSessionsPreventsLogin(true)
表示当 session 达到最大后,阻止后续登录的行为
代码重构
上面代码中,配置都是直接写死在配置类里面了, 这些配置都是可以由调用方去控制的,所以下面来重构一下代码,把这些都做成可配置的
首先需要写一个配置类 SessionProperties
,因为这些东西都是可以由调用放去决定的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Data
public class SessionProperties {
/**
* session失效时跳转的地址
*/
private String sessionInvalidUrl = SecurityConstants.DEFAULT_SESSION_INVALID_URL;
/**
* 同一个用户在系统中的最大session数,默认1
*/
private int maximumSessions = 1;
/**
* 达到最大session时是否阻止新的登录请求,默认为false,不阻止,新的登录会将老的登录失效掉
*/
private boolean maxSessionsPreventsLogin;
}
然后把 SessionProperties
加到浏览器的配置中去
新建一个类 AbstractSessionStrategy
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60@Slf4j
public class AbstractSessionStrategy {
/**
* 跳转的url
*/
private String destinationUrl;
/**
* 重定向策略
*/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/**
* 跳转前是否创建新的session
*/
private boolean createNewSession = true;
private ObjectMapper objectMapper = new ObjectMapper();
public AbstractSessionStrategy(String invalidSessionUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
this.destinationUrl = invalidSessionUrl;
}
protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (createNewSession) {
request.getSession();
}
Object result = buildResponseContent(request);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
private Object buildResponseContent(HttpServletRequest request) {
String message = "session已失效";
if(isConcurrency()){
message = message + ",有可能是并发登录导致的";
}
Map<String, Object> map = new HashMap<>(1);
map.put("message", message);
return map;
}
/**
* session失效是否是并发导致的
* @return
*/
protected boolean isConcurrency() {
return false;
}
public void setCreateNewSession(boolean createNewSession) {
this.createNewSession = createNewSession;
}
}
这里主要是 onSessionInvalid
方法,就是session失效后的返回
然后下面有两个子类, 第一个是session过期的配置:1
2
3
4
5
6
7
8
9
10
11
12
13public class DefaultInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {
public DefaultInvalidSessionStrategy(String invalidSessionUrl) {
super(invalidSessionUrl);
}
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
onSessionInvalid(request, response);
}
}
然后是一个被踢下线的配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class DefaultExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {
public DefaultExpiredSessionStrategy(String invalidSessionUrl) {
super(invalidSessionUrl);
}
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent eventØ) throws IOException, ServletException {
onSessionInvalid(eventØ.getRequest(), eventØ.getResponse());
}
@Override
protected boolean isConcurrency() {
return true;
}
}
把这两个类通过 @Bean
注入到 Spring 容器中, 新建 BrowserSecurityBeanConfig
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Configuration
public class BrowserSecurityBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(InvalidSessionStrategy.class)
public InvalidSessionStrategy invalidSessionStrategy(){
return new DefaultInvalidSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
}
@Bean
@ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
public SessionInformationExpiredStrategy sessionInformationExpiredStrategy(){
return new DefaultExpiredSessionStrategy(securityProperties.getBrowser().getSession().getSessionInvalidUrl());
}
}
这里也是加一个 @ConditionalOnMissingBean
也可以交给调用方去实现
最后 BrowserSecurityConfig
的配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;
@Override
protected void configure(HttpSecurity http) throws Exception {
// ... 省略其他配置
.and()
.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions())
.maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
// ... 省略其他配置
}
集群环境的session管理
在生产环境中,项目部署一般都是集群部署的, 假如说我现在有个项目,部署了两个实例,分别在A服务器上和B服务器上, 这时候一个登录请求经过网关的复载均衡发送到了A服务器上面, 然后用户登录之后,session的信息是记录在了A服务器上,然后用户又发送一个其他的请求,经过网关,将请求转发到了B服务器上面,但是B服务器拿不到session的信息, 所以B服务器会认为用户没有登录,然后引导用户再去登录
在集群的环境中,session一般都是放到一个公共的地方,不管有多少服务都是从一个地方去取session, 下面看一下如何实现这样的配置
我们之前有引入过 spring-session
的配置,然后配置文件中的配置是:1
2
3spring:
session:
store-type: none
none,就表示是使用原生j2ee单机服务器session这种模式,就是我们之前一直用的
然后他支持的几种方式如下:
最常用的就是用 redis 做 session 管理,因为所有请求都需要拿session,然后 redis 中的key本来就可以设置过期时间,不需要我们自己去维护
配置方式
直接修改 application.yml
中的配置1
2
3spring:
session:
store-type: redis
然后根据自己的环境,加入redis的配置就好了
启动项目,登录在redis中就会生成以 spring:session开头的数据