Java Web 架构、接口设计与认证拦截
本文聚焦 Java Web 后端开发中的接口语义、分层架构、统一响应、异常处理、JWT 认证以及 Filter / Interceptor 拦截机制,帮助你建立从 HTTP 接口设计到认证拦截链路的整体认知。
第一章 Web 与 HTTP 基础
1.1 HTTP 协议基础
HTTP 请求报文通常包含:
- 请求行:方法(GET/POST/PUT/DELETE)、URL、协议版本
- 请求头:
Content-Type、Authorization等 - 请求体:POST/PUT 提交的 JSON 或表单
HTTP 响应报文通常包含:
- 响应行:协议版本、状态码(200 OK、404 Not Found 等)
- 响应头
- 响应体:返回的具体内容(HTML、JSON 等)
1.2 RESTful 风格设计
RESTful 是一种面向资源(Resource)的接口设计风格,核心思想不是“动词 + 路径”,而是通过 URL 表示资源、通过 HTTP 方法表示操作。
1. 资源与路径设计
- 路径中使用名词表示资源,而不是动词。
- 一般使用复数名词表示一类资源,例如
/users、/orders、/depts。 - 某个具体资源通过路径参数标识,例如
/users/1、/orders/1001。
| 操作语义 | 推荐写法 | 不推荐写法 |
|---|---|---|
| 查询部门列表 | GET /depts | GET /getDepts |
| 查询单个部门 | GET /depts/1 | GET /findDeptById?id=1 |
| 新增部门 | POST /depts | POST /addDept |
| 修改部门 | PUT /depts/1 | POST /updateDept |
| 删除部门 | DELETE /depts/1 | GET /deleteDept?id=1 |
2. 常见 HTTP 方法语义
| 方法 | 典型语义 | 是否幂等 | 是否常用于请求体 |
|---|---|---|---|
GET | 查询资源 | 是 | 否 |
POST | 新增资源 / 执行非幂等操作 | 否 | 是 |
PUT | 全量更新资源 | 是 | 是 |
PATCH | 局部更新资源 | 通常视业务而定 | 是 |
DELETE | 删除资源 | 是 | 否 |
幂等性是 RESTful 中很重要的概念:同一个请求执行一次和执行多次,结果是否一致。
GET /depts/1:查 1 次和查 10 次,结果语义一致,因此是幂等的。PUT /depts/1:把部门名更新为“研发部”,重复执行结果仍一致,因此通常是幂等的。POST /depts:每调用一次都可能新增一条记录,因此通常不是幂等的。
3. 状态码与 RESTful 的配合
RESTful 不只是路径规范,还强调合理表达响应语义。常见状态码如下:
200 OK:查询成功、修改成功。201 Created:创建资源成功。204 No Content:删除成功但无需返回响应体。400 Bad Request:请求参数错误。401 Unauthorized:未认证,通常表示未登录或 token 无效。403 Forbidden:已认证但无权限访问。404 Not Found:资源不存在。500 Internal Server Error:服务器内部异常。
4. RESTful 设计建议
- 路径表示资源,不要在路径里混入
get、add、update、delete这类动词。 - 操作交给 HTTP 方法表达,例如查询用
GET,新增用POST。 - 路径层级不要过深,通常 2~3 层即可,避免接口过度嵌套。
- 统一返回结构,便于前后端协作与异常处理。
- 优先使用 JSON 作为前后端交互格式,适合前后端分离项目。
第二章 后端分层架构
为了让代码清晰、职责明确,后端项目通常采用三层结构:
- Controller(控制层):接收请求,校验和转换参数,返回响应结果。
- Service(业务逻辑层):封装业务规则、事务与流程编排。
- Dao / Mapper(数据访问层):负责 SQL 执行与数据库交互。
这种分层的价值主要体现在:
- 职责清晰:接口处理、业务规则、数据访问分别维护。
- 方便测试:业务逻辑可以脱离 Web 层单独测试。
- 利于扩展:接口变化通常不必直接改动数据访问代码。
- 便于协作:多人开发时边界更稳定。
典型调用链路如下:
HTTP Request
-> Controller
-> Service
-> Mapper / Dao
-> Database
在前后端分离项目中,可以把这一章理解为“系统结构”,而把 Spring MVC 的参数接收和路由映射理解为“进入 Controller 之前的 Web 入口能力”。
第三章 统一响应与全局异常处理
3.1 统一响应结果
前端通常希望接口始终返回稳定的 JSON 结构,因此项目中常定义统一响应对象:
public class Result<T> {
private Integer code; // 1-成功, 0-失败
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 1;
r.msg = "success";
r.data = data;
return r;
}
public static Result<String> error(String msg) {
Result<String> r = new Result<>();
r.code = 0;
r.msg = msg;
r.data = null;
return r;
}
}
统一返回结构的好处是:
- 前端更容易统一处理成功与失败分支
- 接口文档风格一致
- 异常处理可以统一兜底
3.2 全局异常处理
使用 @RestControllerAdvice 可以集中处理异常,避免在每个 Controller 中重复编写 try-catch:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
e.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}
实际项目中建议进一步细分:
- 参数校验异常单独处理
- 业务异常单独处理
- 系统异常统一兜底并记录日志
第四章 身份认证与会话管理
HTTP 无状态,后端必须额外设计“登录状态的保存与校验”。
4.1 认证架构的演进与本质考量
认证的本质就是状态管理。
| 方案 | 怎么做 | 优点 | 缺点 |
|---|---|---|---|
Cookie | 浏览器保存键值对,后续请求会自动携带 | 适合保存少量状态,浏览器支持天然 | 有大小限制,且存在跨域和安全限制 |
Session | 服务端存 Session,客户端通常用 Cookie 携带 JSESSIONID | 实现简单,适合单体项目 | 分布式下 Session 共享麻烦,跨端能力弱 |
Token | 服务端签发 Token,客户端放请求头 | 跨域友好,适合分布式 | 状态撤销、续期、下线控制更复杂 |
注意:
Cookie是浏览器存储机制,本身不是服务端会话方案。Session是服务端机制,Cookie只是常见载体。- 单体项目常见
Session。 - 分布式、移动端、微服务更常见
Token。 - 企业系统最终多是混合方案,不会只追求“绝对无状态”。
4.2 深度解析 JWT (JSON Web Token) 机制
JWT 是一种自包含、可验签的 Token。
1. 结构与安全设计
JWT = Header.Payload.Signature
Header:声明算法和类型。Payload:放业务字段,如userId、exp。Signature:服务端签名,防止篡改。
注意:
Payload只是编码,不是加密。- 不要在 JWT 中放密码、手机号、身份证号等敏感信息。
- JWT 适合放“身份标识”,不适合放“强业务状态”。
2. Java 生态下的实现
下面示例按Spring Boot 3.x + JDK 17语境编写,使用的是新版jjwtAPI。旧版常见的signWith(SignatureAlgorithm, "secret")、Jwts.parser().setSigningKey(...)
签发 Token
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
SecretKey key = Keys.hmacShaKeyFor(
"your-secret-key-your-secret-key-123456".getBytes(StandardCharsets.UTF_8)
);
String jwt = Jwts.builder()
.claims(claims) // 注入非敏感业务上下文
.expiration(new Date(System.currentTimeMillis() + 3600 * 1000))
.signWith(key)
.compact();
验签与解析
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
try {
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jwt)
.getPayload(); // 若遭篡改或已过期,将抛出异常
} catch (JwtException e) {
// 阻断请求,返回 401 Unauthorized
}
3. JWT 过期刷新原理(行业标准实践)
短效 accessToken 访问接口,长效 refreshToken 负责换新。
| Token | 用途 | 建议有效期 | 说明 |
|---|---|---|---|
accessToken | 访问业务接口 | 15 分钟 ~ 2 小时 | 泄露后风险窗口更短 |
refreshToken | 调用刷新接口 | 7 天 ~ 30 天 | 建议服务端保存状态 |
刷新流程:
- 登录成功,签发
accessToken + refreshToken。 - 前端请求接口时携带
accessToken。 accessToken过期后,调用POST /auth/refresh。- 服务端校验
refreshToken后重新签发新accessToken。 - 刷新失败则要求重新登录。
为什么不能直接“延长原 JWT”:
exp参与签名,过期时间一变,本质上就是重新签发。accessToken设计成短效,是为了降低泄露风险。
示例 1:登录后签发双 Token
Map<String, Object> accessClaims = new HashMap<>();
accessClaims.put("userId", user.getId());
accessClaims.put("username", user.getUsername());
SecretKey accessKey = Keys.hmacShaKeyFor(
"access-secret-key-access-secret-key-123456".getBytes(StandardCharsets.UTF_8)
);
String accessToken = Jwts.builder()
.claims(accessClaims)
.expiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.signWith(accessKey)
.compact();
Map<String, Object> refreshClaims = new HashMap<>();
refreshClaims.put("userId", user.getId());
refreshClaims.put("type", "refresh");
SecretKey refreshKey = Keys.hmacShaKeyFor(
"refresh-secret-key-refresh-secret-key-123456".getBytes(StandardCharsets.UTF_8)
);
String refreshToken = Jwts.builder()
.claims(refreshClaims)
.expiration(new Date(System.currentTimeMillis() + 7L * 24 * 60 * 60 * 1000))
.signWith(refreshKey)
.compact();
示例 2:刷新接口重新签发 accessToken
@PostMapping("/auth/refresh")
public Result<String> refreshToken(@RequestBody Map<String, String> body) {
String refreshToken = body.get("refreshToken");
try {
Claims claims = Jwts.parser()
.verifyWith(refreshKey)
.build()
.parseSignedClaims(refreshToken)
.getPayload();
Integer userId = (Integer) claims.get("userId");
String type = (String) claims.get("type");
if (!"refresh".equals(type)) {
return Result.error("无效的 refreshToken");
}
// 企业项目中通常还会补:查用户状态、查 Redis 中的 refreshToken 是否仍然有效
Map<String, Object> newAccessClaims = new HashMap<>();
newAccessClaims.put("userId", userId);
String newAccessToken = Jwts.builder()
.claims(newAccessClaims)
.expiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
.signWith(accessKey)
.compact();
return Result.success(newAccessToken);
} catch (JwtException e) {
return Result.error("refreshToken 已失效,请重新登录");
}
}
落地建议:
accessToken短效,尽量只存用户标识、角色等非敏感信息。refreshToken建议存入 Redis 或数据库,便于撤销和设备管理。- 高安全场景下,每次刷新时同时轮换新的
refreshToken。 - Web 端更推荐把
refreshToken放入HttpOnly Cookie,降低被 JS 窃取的风险。
4.3 架构级选型对比:JWT (纯无状态) vs Redis+Token (中心化状态)
JWT 性能高,Redis + Token 可控性强。
| 维度 | JWT (纯无状态) | Redis + Token (中心化状态) |
|---|---|---|
| 工作方式 | 靠验签识别身份 | 靠 Redis 中的会话状态识别身份 |
| 状态可控性 | 弱,难以立即下线 | 强,删除 Redis 即可失效 |
| 续期实现 | 复杂,通常用双 Token | 简单,直接续 Redis TTL |
| 性能 | 高,少一次外部 I/O | 较高,但依赖 Redis |
怎么选:
- C 端、高并发、微服务内部鉴权:优先
JWT。 - B 端、后台管理、多端同步、强制下线:优先
Redis + Token。 - 多数企业项目:采用
JWT + Redis混合方案。
4.4 企业级认证与鉴权链路设计
标准链路只记 3 步:
- 登录签发:校验账号密码,返回 Token。
- 请求携带:前端把 Token 放进
Authorization: Bearer <token>。 - 统一鉴权:网关、过滤器或拦截器统一验签;通过后把
userId写入上下文,失败直接返回401/403。
第五章 Filter 与 Interceptor
在 Web 开发中,有些通用逻辑(如登录校验、权限验证、跨域处理、日志记录)如果写在每个 Controller 中会非常冗余。此时通常使用过滤器(Filter)或拦截器(Interceptor)做统一拦截。
5.1 过滤器(Filter)的使用
Filter 是 Java Web(Servlet 规范)原生组件,它可以拦截所有进入 Web 容器的请求。
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("1. Filter 拦截到了请求,可以在这里做前置校验...");
chain.doFilter(request, response);
System.out.println("5. Filter 处理响应,在响应返回给前端之前执行...");
}
}
5.2 拦截器(Interceptor)的使用
Interceptor 是 Spring MVC 提供的机制,它主要拦截发送到 Controller 的请求。
第一步:定义拦截器
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("2. Interceptor: preHandle,Controller 方法执行前校验...");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("3. Interceptor: postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
System.out.println("4. Interceptor: afterCompletion");
}
}
第二步:注册拦截器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
5.3 Filter 的路径规则
Filter 使用的是 Servlet URL Pattern,和 Interceptor 不是一套规则。
| 写法 | 含义 | 说明 |
|---|---|---|
/* | 拦截当前应用下几乎所有请求 | Filter 中最常见的“全拦截”写法 |
/api/* | 拦截 /api 相关请求 | 常用于统一处理某一类接口 |
*.jsp | 拦截指定扩展名请求 | 属于 Servlet 风格写法 |
注意:
@WebFilter(urlPatterns = "/*")在 Filter 中通常表示全局拦截。- Filter 不按 Spring MVC 的
/**语义来理解。 - 不要把 Filter 的路径规则和 Interceptor 的路径规则混在一起记。
5.4 Interceptor 的路径规则
Interceptor 使用的是 Spring MVC 路径匹配规则。
| 拦截路径 | 含义 | 举例 |
|---|---|---|
/* | 一级路径 | 能匹配 /depts、/emps、/login;不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配 /depts、/depts/1、/depts/1/2 |
/depts/* | /depts 下的一级路径 | 能匹配 /depts/1;不能匹配 /depts/1/2 |
/depts/** | /depts 下的任意级路径 | 能匹配 /depts、/depts/1、/depts/1/2 |
注意:
- Filter 看 Servlet 规则。
- Interceptor 看 Spring 规则。
/**是 Interceptor 场景里最常见的全路径匹配写法。
5.5 Filter 与 Interceptor 的区别
| 维度 | 过滤器 (Filter) | 拦截器 (Interceptor) |
|---|---|---|
| 所属规范 | Servlet 规范 | Spring MVC 机制 |
| 拦截范围 | 几乎所有请求 | 只拦截 Controller 请求 |
| 依赖环境 | 依赖 Web 容器 | 依赖 Spring MVC |
| 路径规则 | Servlet URL Pattern | Spring MVC 路径匹配 |
| Bean 注入 | 默认不如拦截器直接 | 可以直接注入 Spring Bean |
| 适用场景 | 全局跨域、字符编码、静态资源相关处理 | 登录校验、权限控制、业务日志 |
| 执行顺序 | Filter 前半段 -> Interceptor preHandle -> Controller -> Interceptor postHandle / afterCompletion -> Filter 后半段 | 处于 Filter 与 Controller 之间 |
实践中可以这样理解:
- 更靠近 Web 容器的通用处理,优先考虑 Filter。
- 更靠近 Controller 的业务拦截,优先考虑 Interceptor。
- JWT 登录校验 通常更适合写在 Interceptor 中。因为 JWT 登录校验本质上更偏“ 业务身份校验 ”,而不是“底层请求处理”。
本文小结
- 使用 HTTP 与 RESTful 设计接口,明确资源、方法与状态码语义。
- 通过三层架构降低耦合,稳定 Controller、Service、Mapper 边界。
- 通过统一响应与全局异常处理提升接口一致性。
- 使用 JWT 实现无状态认证。
- 根据场景选择 Filter 与 Interceptor,构建稳定的认证拦截链路。