SpringBoot登录方案和session失效、信息更新问题

本文探讨了单机服务中登录功能的常见问题及解决方案。主要问题包括Session过期和用户信息更新不及时。通过引入Token机制和拦截器,解决了Session过期问题,确保用户关闭浏览器前保持登录状态。对于用户信息更新问题,建议在Session中仅存储用户ID,每次请求时查询最新用户信息,确保状态实时更新。此外,还讨论了X天免登录功能的实现思路。

文章目录

前言

登录功能可以说是老生常谈了,相信这是每个后端必备的基本功,从最基本的session存储登录信息,到分布式环境下的JWT、redis session,每个方案各有优缺点,即使是单机服务最常用的session存储也有一些不足,本文就从单机服务的登录功能中分析痛点,提出我个人的解决方案。

Session的不足

登录功能最基础的做法就是在验证用户密码之后,将查询到的用户信息都储存在session中,这么做会带来以下问题:

  • session有过期时间(tomcat默认是30分钟),用户挂着页面闲置一段时间后就会退出登录状态,影响用户体验
  • 用户信息储存在session中,用户信息被他人更改后无法及时更新session中的信息(如管理员封禁用户)

解决思路

Session过期问题

对于session过期的问题,我们可以通过在Cookie中添加额外的token来解决。

登录过程

Talk is cheap, show me the code.

@PostMapping("/token")
public R<?> login(@RequestBody LoginBody loginBody, HttpSession session, HttpServletResponse response) {
// 省略:验证用户登录信息
// 随机生成token
String token = UUID.randomUUID().toString();
// 省略:将token和用户id储存到redis或其他缓存中
// 构建cookie
ResponseCookie.from("Authorization", token)
.httpOnly(true)
.path("/")
.build();
// 将cookie添加到响应头中
response.addHeader("Set-Cookie", token);
return R.success("登录成功");
}

然后我们定义一个拦截器,当用户请求API时,对其进行检查,如果没有用户信息则尝试使用token登录。

登录检查过程

public class DynamicAuthInterceptor implements HandlerInterceptor {
private UserService userService;
public DynamicAuthInterceptor(UserService userService) {
this.userService = userService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
HttpSession session = request.getSession();
// 若session中无用户信息则需动态登录
if (session.getAttribute(AppConstants.SESSION_USER_KEY) == null && request.getCookies() != null) {
for (var cookie : request.getCookies()) {
if (cookie.getName().equals("Authorization")) {
login(request, response, cookie.getValue()); // 进行动态登录
}
}
}
return true;
}
// 动态登录方法
private void login(HttpServletRequest request, HttpServletResponse response, String token) {
// 省略:从redis或其他缓存中查找token及用户id
String uid = RedisUtils.getValue(token);
// 根据用户id判断用户是否存在,获取用户信息
if (uid != null) {
User user = userService.selectById(uid);
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 设置登录状态
request.getSession().setAttribute("user", user);
// 省略:重新生成token并添加到缓存中、cookie中
}
}
}

然后将拦截器添加到SpringMVC中。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserService userService;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加动态登录拦截器
registry.addInterceptor(new DynamicAuthInterceptor(userService)).addPathPatterns("/**");
}
}

这样,请求进入controller前,拦截器就会尝试获取token并登录,保证了用户关闭浏览器前一直保持登录状态。

用户信息更新问题

试想这么一个场景:

某论坛,管理员将一名乱发广告的用户封禁之后,由于用户还处于登录状态,从session获取到的用户信息是封禁前的,于是用户依旧大摇大摆发着广告❌

于是开发人员着手解决这个问题,想要在封禁时顺便清除该用户的session?恐怕不太可行,不如在登录时session中只储存用户的id,用户访问需要鉴权的API时,由后端查询用户的信息,这样就能实时判断用户的状态。于是画面变成了:

某论坛,管理员将一名乱发广告的用户封禁之后,由于用户还处于登录状态,用户尝试发广告,后端查询到用户被封禁,于是禁止发送,世界清静了✅

这个方案并不是完美的,每次请求API需要多查询一次用户信息,对性能比较敏感的场景可以加上缓存。代码实现比较简单,这里就不给出了。

X天免登录功能

这是一个功能需求,实现思路其实和第一个问题大差不差,主要就是token存活时长不同的区别,以及在刷新token时需要判断token属于X天免登录还是属于临时免登录(用于设置新token的生命周期),我一开始打算通过cookie过期时间来判断,可是cookie的过期时间信息并不会发送到后端,只好从其他方面入手。

最后我选择在生成token时为token加上特殊前缀来判断,如temp表示这是一个临时token,随着浏览器关闭而删除。

后话

由于是第一次写后端相关的教程,在语言组织、文章结构等方面不太熟悉,还请大家见谅,对于文章/代码有疑惑的地方可以在评论区留言,或者在github上查看完整的拦截器代码:BlackHoleStation/DynamicAuthInterceptor.java

下一篇文章应该是写基于Spring AOP的鉴权实现了,对于想要进行简单有效的角色鉴权又不想引入spring security、shiro等框架的人来说值得一看!