🧰 Spring Boot 登录权限校验拦截器教程(含注解跳过机制)
📌 目录
-
功能概述
-
拦截器工作流程
-
注解使用说明
-
核心逻辑分解
-
Redis 缓存策略说明
-
企业状态校验逻辑
-
常见扩展与建议
-
总结
1️⃣ 功能概述
本教程讲解一个基于 Spring Boot 实现的登录权限拦截器 LoginInterceptor,其作用为:
-
校验用户是否已登录
-
校验 token 是否过期
-
校验用户企业信息是否合法
-
支持使用自定义注解
@IgnoreAuthentication来跳过拦截校验(白名单机制) -
登录信息通过 Redis 缓存,提高性能与并发能力
2️⃣ 拦截器工作流程
流程如下图:
┌────────────────────────┐ │ HTTP 请求进入系统 │ └──────────┬─────────────┘ │ ▼ 是否是静态资源(.do/.jsp/.ico)? │ 是 ──────► 放行 │ 否 ▼ 是否使用 @IgnoreAuthentication 注解? │ 是 ──────► 放行 │ 否 ▼ 读取 token 是否存在?(Redis + DB 校验) │ 否 ──────► 返回 401,非法 token │ 是 ▼ token 是否超时?(与最后操作时间比较) │ 是 ──────► 删除 token,返回 401 │ 否 ▼ 是否绑定企业?企业是否被禁用? │ 是 ──────► 删除 token,返回 401 │ 否 ▼ 放行
3️⃣ 注解使用说明:@IgnoreAuthentication
该自定义注解可用于方法级别或类级别,用于标记当前接口跳过登录验证。
示例:
@IgnoreAuthentication
@GetMapping("/public/test")
public AjaxResult publicApi() {
return AjaxResult.success("无需登录即可访问");
}
注解源码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreAuthentication {}
判断逻辑(代码中):
IgnoreAuthentication ignoreAuthentication = AnnotationUtils.findAnnotation(method, IgnoreAuthentication.class);
if (ignoreAuthentication == null) {
ignoreAuthentication = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), IgnoreAuthentication.class);
}
if (ignoreAuthentication != null) {
return true; // 直接放行
}
4️⃣ 核心逻辑分解
🔹 静态资源直接放行
String pathName = request.getRequestURL().toString();
if (pathName.contains(".do") || pathName.contains(".jsp") || pathName.contains(".ico") || split.length <= 3) {
return true;
}
🔹 获取当前用户
User user = AppContext.getCurrentUser();
String token = request.getHeader(AppContext.TOKEN_NAME);
若用户为 null,通过 token 尝试从 userLoginRecordService 查找用户登录记录:
UserLoginRecord userLoginRecord = userLoginRecordService.findByToken(token);
找到了就重新设置到 Redis 并放行,否则响应 token 失效:
Result result = Result.failure(ResponseCode.INVALID_TOKEN); response.getWriter().write(JsonUtil.object2JSON(result));
🔹 操作时间间隔校验(防止长期未操作的用户继续访问)
从 AdminPlatformConfiguration 配置中读取超时时间(分钟):
Integer interval = adminPlatformConfiguration.getOperationTimeInteval();
判断逻辑:
if (now.getTime() - lastTime.getTime() > 1000 * 60 * interval) { redisTemplate.delete(token); userLoginRecordService.deleteByUserId(user.getId()); // 返回 401 }
🔹 企业信息校验(主要用于 B 端多企业场景)
若用户绑定企业,需校验企业是否存在并且未被禁用:
Enterprise findEnterprise = enterpriseService.findById(user.getLastEnterpriseId());
if (findEnterprise == null || findEnterprise.getStatus().equals(1)) {
redisTemplate.delete(token);
userLoginRecordService.deleteByUserId(user.getId());
user.setLastEnterpriseId(-1);
userService.updateById(user);
// 返回 401
}
5️⃣ Redis 缓存策略说明
-
用户登录成功后,
token会作为 key,将User对象缓存至 Redis -
每次请求更新
lastTime字段并刷新缓存 -
超过配置时间(如 30 分钟)未操作则视为超时,token 清除,强制重新登录
缓存相关操作:
redisTemplate.boundValueOps(token).set(user);
清除缓存:
redisTemplate.delete(token);
6️⃣ 企业状态校验逻辑说明
企业状态的判断基于:
-
Enterprise对象是否存在 -
status != 1(1 通常表示禁用)
一旦不符合,即视为企业非法,清除 token 并强制退出登录。
7️⃣ 常见扩展与建议
| 扩展项 | 建议实现方式 |
|---|---|
| 白名单 URL 配置 | 使用配置文件定义 permitUrls,统一管理免校验路径 |
| 用户权限控制 | 拦截器 + 注解 @RequiresPermissions |
| 多平台 token 校验 | 区分 Web、App、小程序等使用场景,适配多种 token 策略 |
| 多租户隔离 | 企业 ID 或租户 ID 存入上下文,进行数据隔离控制 |
| Token 存活检测 | 使用 Redis TTL 机制,自带过期时间 |
8️⃣ 总结
本登录拦截器方案较为完整,具备以下特点:
-
✅ 支持注解白名单机制
-
✅ 支持 Redis 缓存用户信息
-
✅ 支持 token 操作超时校验
-
✅ 支持企业状态控制
-
✅ 易于扩展(如权限控制、平台隔离等)
适合用于 中大型 B 端系统 或需要进行 多租户身份校验 的场景。
@Slf4j
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Resource
private UserLoginRecordService userLoginRecordService;
@Resource
private EnterpriseService enterpriseService;
@Resource
private UserService userService;
@Resource
private RedisTemplate redisTemplate;
@Autowired
private AdminPlatformConfigurationService adminPlatformConfigurationService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String path = request.getRequestURL().toString();
String[] split = path.split("/");
// 放行静态资源或路径太短的请求
if (path.contains(".do") || path.contains(".jsp") || path.contains(".ico") || split.length <= 3) {
return true;
}
// 忽略认证注解判断
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
IgnoreAuthentication ignoreAuth = AnnotationUtils.findAnnotation(method, IgnoreAuthentication.class);
if (ignoreAuth == null) {
ignoreAuth = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), IgnoreAuthentication.class);
}
if (ignoreAuth != null) {
return true;
}
}
String token = request.getHeader("Token");
User user = AppContext.getCurrentUser();
// 用户上下文为空,尝试通过 token 找回登录记录
if (user == null) {
UserLoginRecord record = userLoginRecordService.findByToken(token);
if (record != null) {
user = userService.getById(record.getUserId());
redisTemplate.boundValueOps(token).set(user);
return true;
}
return unauthorized(response);
}
// 校验操作间隔时间
Date now = new Date();
Date lastTime = user.getLastTime();
if (lastTime == null) {
user.setLastTime(now);
redisTemplate.boundValueOps(token).set(user);
} else {
AdminPlatformConfiguration config = adminPlatformConfigurationService.selectAll();
Integer interval = config.getOperationTimeInteval();
if (interval != null && now.getTime() - lastTime.getTime() > 1000L * 60 * interval) {
redisTemplate.delete(token);
userLoginRecordService.deleteByUserId(user.getId());
return unauthorized(response);
}
user.setLastTime(now);
redisTemplate.boundValueOps(token).set(user);
}
// 校验企业状态
if (user.getLastEnterpriseId() != null && user.getLastEnterpriseId() > 0) {
Enterprise enterprise = enterpriseService.findById(user.getLastEnterpriseId());
if (enterprise == null || enterprise.getStatus().equals(1)) {
redisTemplate.delete(token);
userLoginRecordService.deleteByUserId(user.getId());
user.setLastEnterpriseId(-1);
userService.updateById(user);
return unauthorized(response);
}
}
return true;
}
private boolean unauthorized(HttpServletResponse response) throws Exception {
Result result = Result.failure(ResponseCode.INVALID_TOKEN);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write(JsonUtil.object2JSON(result));
response.getWriter().close();
response.flushBuffer();
return false;
}
}

5496

被折叠的 条评论
为什么被折叠?



