Java Web 架构、接口设计与认证拦截

本文聚焦 Java Web 后端开发中的接口语义、分层架构、统一响应、异常处理、JWT 认证以及 Filter / Interceptor 拦截机制,帮助你建立从 HTTP 接口设计到认证拦截链路的整体认知。

第一章 Web 与 HTTP 基础

1.1 HTTP 协议基础

HTTP 请求报文通常包含:

  • 请求行:方法(GET/POST/PUT/DELETE)、URL、协议版本
  • 请求头Content-TypeAuthorization
  • 请求体: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 /deptsGET /getDepts
查询单个部门GET /depts/1GET /findDeptById?id=1
新增部门POST /deptsPOST /addDept
修改部门PUT /depts/1POST /updateDept
删除部门DELETE /depts/1GET /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 设计建议

  • 路径表示资源,不要在路径里混入 getaddupdatedelete 这类动词。
  • 操作交给 HTTP 方法表达,例如查询用 GET,新增用 POST
  • 路径层级不要过深,通常 2~3 层即可,避免接口过度嵌套。
  • 统一返回结构,便于前后端协作与异常处理。
  • 优先使用 JSON 作为前后端交互格式,适合前后端分离项目。

第二章 后端分层架构

为了让代码清晰、职责明确,后端项目通常采用三层结构:

  • Controller(控制层):接收请求,校验和转换参数,返回响应结果。
  • Service(业务逻辑层):封装业务规则、事务与流程编排。
  • Dao / Mapper(数据访问层):负责 SQL 执行与数据库交互。

这种分层的价值主要体现在:

  • 职责清晰:接口处理、业务规则、数据访问分别维护。
  • 方便测试:业务逻辑可以脱离 Web 层单独测试。
  • 利于扩展:接口变化通常不必直接改动数据访问代码。
  • 便于协作:多人开发时边界更稳定。

典型调用链路如下:

Text
HTTP Request
  -> Controller
    -> Service
      -> Mapper / Dao
        -> Database

在前后端分离项目中,可以把这一章理解为“系统结构”,而把 Spring MVC 的参数接收和路由映射理解为“进入 Controller 之前的 Web 入口能力”。


第三章 统一响应与全局异常处理

3.1 统一响应结果

前端通常希望接口始终返回稳定的 JSON 结构,因此项目中常定义统一响应对象:

Java
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

Java
@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:放业务字段,如 userIdexp
  • Signature:服务端签名,防止篡改。

注意

  • Payload 只是编码,不是加密。
  • 不要在 JWT 中放密码、手机号、身份证号等敏感信息。
  • JWT 适合放“身份标识”,不适合放“强业务状态”。

2. Java 生态下的实现

下面示例按 Spring Boot 3.x + JDK 17 语境编写,使用的是新版 jjwt API。旧版常见的 signWith(SignatureAlgorithm, "secret")Jwts.parser().setSigningKey(...)

签发 Token

Java
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();

验签与解析

Java
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 天建议服务端保存状态

刷新流程

  1. 登录成功,签发 accessToken + refreshToken
  2. 前端请求接口时携带 accessToken
  3. accessToken 过期后,调用 POST /auth/refresh
  4. 服务端校验 refreshToken 后重新签发新 accessToken
  5. 刷新失败则要求重新登录。

为什么不能直接“延长原 JWT”

  • exp 参与签名,过期时间一变,本质上就是重新签发。
  • accessToken 设计成短效,是为了降低泄露风险。

示例 1:登录后签发双 Token

Java
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

Java
@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 步:

  1. 登录签发:校验账号密码,返回 Token。
  2. 请求携带:前端把 Token 放进 Authorization: Bearer <token>
  3. 统一鉴权:网关、过滤器或拦截器统一验签;通过后把 userId 写入上下文,失败直接返回 401/403

第五章 Filter 与 Interceptor

在 Web 开发中,有些通用逻辑(如登录校验、权限验证、跨域处理、日志记录)如果写在每个 Controller 中会非常冗余。此时通常使用过滤器(Filter)拦截器(Interceptor)做统一拦截。

5.1 过滤器(Filter)的使用

Filter 是 Java Web(Servlet 规范)原生组件,它可以拦截所有进入 Web 容器的请求。

Java
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 的请求。

第一步:定义拦截器

Java
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");
    }
}

第二步:注册拦截器

Java
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 PatternSpring 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,构建稳定的认证拦截链路。