在 Web 应用中,我们经常需要对某些接口进行登录状态校验:
- 公共接口(如登录、注册)→ 无需登录;
- 业务接口(如查询订单、修改资料)→ 必须登录。
传统做法是在每个 Controller 方法里手动判断 session 或 token,但这样代码重复、难以维护。
更好的方式是:用自定义注解 + Spring 拦截器,实现声明式鉴权。
一、定义自定义注解 @Login
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Login { // 默认值为 YES,表示需要登录;设为 NO 则跳过校验 YesOrNo value() default YesOrNo.YES; }
配套枚举:
public enum YesOrNo { YES, NO }
✅ 这样设计的好处:
- 大多数接口默认需登录(安全优先);
- 仅对少数公开接口显式标注
@Login(YesOrNo.NO)。
二、实现 Spring MVC 拦截器
注意:这里使用的是
HandlerInterceptor(Spring MVC 拦截器),而非 ServletFilter。它能直接获取到 Controller 方法的元信息(如注解),更适合做方法级鉴权。
import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 只处理 Controller 方法(排除静态资源等) if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Login login = handlerMethod.getMethodAnnotation(Login.class); // 如果方法上有 @Login(NO),放行 if (login != null && YesOrNo.NO.equals(login.value())) { return true; } // 否则:检查是否已登录 if (!UserAuthHelper.isAuthenticated(request)) { // 未登录,返回 JSON 错误 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.write(JSON.toJSONString(ResultMessage.error("请先登录"))); writer.flush(); return false; // 中断请求 } return true; // 放行 } }
关键点说明:
HandlerMethod:可获取被调用的 Controller 方法,进而读取其上的注解;UserAuthHelper.isAuthenticated():封装了登录状态判断逻辑(如从 Session、Token、Redis 中验证用户);- 返回
false:中断请求链,不再执行 Controller 方法; - 统一错误格式:返回 JSON 而非跳转页面,适配前后端分离架构。
三、注册拦截器
在 Spring Boot 中配置:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns("/login", "/static/**", "/error"); // 排除公开路径 } }
💡 即使有
@Login(NO),也可以通过excludePathPatterns提前放行高频公开接口,提升性能。
四、在 Controller 中使用
@RestController @RequestMapping("/api/user") public class UserController { // 公开接口:无需登录 @Login(YesOrNo.NO) @PostMapping("/filter") public ResultMessage filter(String companyId, String code) { // 业务逻辑 return ResultMessage.ok("查询成功", data); } // 默认需登录(可省略 @Login) @GetMapping("/profile") public ResultMessage profile() { // 自动校验登录状态 return ResultMessage.ok("获取成功", userInfo); } }
✅ 优势:
- 无需在每个方法写
if (!login) return error;;- 权限规则集中管理,修改只需调整拦截器;
- 语义清晰,一眼看出接口是否需要登录。
五、对比:拦截器 vs 过滤器(Filter)
| 特性 | Filter(Servlet) | HandlerInterceptor(Spring MVC) |
| 执行时机 | 更早(在 DispatcherServlet 之前) | 在 Spring MVC 流程中 |
| 能否获取 Controller 方法信息 | ❌ 不能 | ✅ 能(通过 HandlerMethod) |
| 是否支持 Spring 注入 | ❌ 需手动获取 Bean | ✅ 可直接注入 Service/Bean |
| 适用场景 | 编码、日志、全局异常 | 方法级权限、参数预处理 |
📌 结论:做方法级注解鉴权,优先选 拦截器。
六、扩展方向
- 角色鉴权:扩展
@Login(role = "ADMIN"); - 多端支持:区分 Web / App / 小程序 的 token 校验逻辑;
- 自动续期:在拦截器中刷新 token 有效期;
- 结合 Spring Security:作为轻量级替代方案,或与之互补。
总结
通过 自定义注解 + Spring 拦截器,我们实现了:
- 声明式登录控制;
- 零侵入业务代码;
- 高可维护性与可读性。
这种模式不仅适用于登录鉴权,还可用于权限、限流、审计等横切关注点,是构建整洁、健壮 Web 系统的重要实践。