跳转到内容
123xiao | 无名键客

《Spring Boot 中基于 JWT 与 Spring Security 的前后端分离鉴权实战与权限设计》

字数: 0 阅读时长: 1 分钟

背景与问题

前后端分离之后,登录态管理这件事就不再像早期 JSP/Session 那样“顺手”了。

传统服务端 Session 模式有两个典型问题:

  1. 前后端跨域时处理麻烦
    • Cookie 传递、SameSite、跨域凭据都容易出问题。
  2. 服务扩容后会遇到状态共享
    • 多实例部署时,要么 sticky session,要么 session 共享,架构复杂度上来了。

这时候,JWT(JSON Web Token)就很自然地进入视野:服务端签发一个带声明的令牌,客户端后续请求携带该令牌,服务端校验签名后直接完成身份认证。

但真实项目里,很多人把 JWT 只当成“登录成功后发个 token”这么简单。真正难点往往在后面:

  • Spring Security 过滤链怎么接入?
  • 登录、鉴权、授权这三件事怎么拆开?
  • 角色、权限、菜单到底怎么设计?
  • token 失效、续签、登出、黑名单怎么处理?
  • 出现 401/403 时,怎么快速排查?

我当时第一次把 JWT 接到 Spring Security 里,最容易混淆的就是:认证(你是谁)授权(你能做什么)。这篇文章就从架构视角,把这套方案完整走一遍,并给出一套能跑起来的 Spring Boot 示例。


方案概览与取舍分析

先给结论:对于典型的前后端分离系统,Spring Boot + Spring Security + JWT 是一套非常主流且工程上平衡不错的方案。

方案角色划分

  • Spring Security:安全框架骨架,负责认证入口、授权判断、异常处理、过滤器链
  • JWT:令牌格式,负责跨请求携带用户身份与必要声明
  • 业务系统数据库:保存用户、角色、权限等长期数据
  • 前端:保存 access token,并在每次请求头中携带

与 Session 模式对比

方案优点缺点适用场景
Session + Cookie简单直观,服务端可控分布式共享成本高,跨域复杂单体应用、后台管理
JWT + Security无状态、适合前后端分离、扩展方便撤销难、设计不当会有安全风险移动端、前后端分离、微服务网关前置
OAuth2/OIDC标准化、生态成熟接入复杂度更高多应用统一认证、第三方登录

架构上的取舍

JWT 不是没有代价:

  • 优点

    • 无状态,服务横向扩容友好
    • 前后端解耦
    • 网关层校验方便
  • 代价

    • token 一旦签发,天然不容易立即失效
    • token 放太多信息会变大,增加传输与泄漏风险
    • 权限变更存在“旧 token 权限仍生效”的时间窗口

所以实践中建议:

  • 短时 access token
  • 敏感系统加 refresh token
  • 需要强制下线时引入黑名单或 token version

核心原理

这一部分先把链路讲透,后面代码就容易看懂了。

1. 登录认证流程

用户输入用户名密码,后端校验通过后,签发 JWT。

JWT 通常包含三部分:

  • Header:签名算法等元信息
  • Payload:用户声明,比如用户名、用户 ID、角色
  • Signature:服务端密钥签名

JWT 结构大致如下:

header.payload.signature

2. 请求鉴权流程

前端后续请求在 Header 中带上:

Authorization: Bearer <token>

服务端在 Spring Security 过滤器中完成:

  1. 解析 Authorization 请求头
  2. 校验 JWT 签名、过期时间
  3. 从 token 中提取用户身份
  4. 构造 Authentication
  5. 放入 SecurityContextHolder
  6. 后续授权器按角色/权限判断是否放行

3. 认证与授权的边界

这个边界一定要清晰:

  • 认证 Authentication

    • 证明请求是谁发起的
    • 比如用户 admin
  • 授权 Authorization

    • 决定该用户是否可以访问 /admin/user/list
    • 比如是否拥有 ROLE_ADMINuser:read

很多项目把角色直接塞进 token,然后所有权限都靠 token 里的角色判断。这样做小项目没问题,但中大型系统会遇到:

  • 权限变更不能及时生效
  • token 内容不断膨胀
  • 角色与资源绑定越来越复杂

更稳妥的做法是:

  • token 里保留最小必要身份信息
  • 权限可以根据用户 ID 动态加载,或者放入缓存

4. Spring Security 在这里做了什么

你可以把 Spring Security 理解成一条可编排的过滤链。

flowchart LR
    A[前端请求] --> B[JWT过滤器]
    B --> C{Token有效?}
    C -- 否 --> D[返回401]
    C -- 是 --> E[构造Authentication]
    E --> F[放入SecurityContext]
    F --> G[授权判断]
    G --> H{有权限?}
    H -- 否 --> I[返回403]
    H -- 是 --> J[进入Controller]

这里两个 HTTP 状态码非常重要:

  • 401 Unauthorized
    • 没登录、token 无效、token 过期
  • 403 Forbidden
    • 已登录,但权限不够

这两个状态别混了,排查时非常关键。


权限模型设计:角色不是全部

JWT 鉴权真正落地时,往往不是“能不能登录”,而是“权限模型怎么设计”。

常见模型

1. 只用角色

例如:

  • ROLE_ADMIN
  • ROLE_USER

优点是简单,缺点是粒度粗。
如果出现“用户管理只读”和“用户管理可编辑”这样的需求,就容易失控。

2. 角色 + 权限点

推荐这种:

  • 角色:一组权限的集合
  • 权限点:系统最小授权单元

例如:

  • 角色

    • ROLE_ADMIN
    • ROLE_EDITOR
  • 权限

    • user:read
    • user:create
    • user:update
    • user:delete

这种模式更适合后台管理系统和中型业务系统。

一套常用表设计

classDiagram
    class User {
      +Long id
      +String username
      +String password
      +Boolean enabled
    }

    class Role {
      +Long id
      +String code
      +String name
    }

    class Permission {
      +Long id
      +String code
      +String name
    }

    class UserRole {
      +Long userId
      +Long roleId
    }

    class RolePermission {
      +Long roleId
      +Long permissionId
    }

    User --> UserRole
    Role --> UserRole
    Role --> RolePermission
    Permission --> RolePermission

实战建议

如果是中级复杂度的系统,我建议:

  • token 中放:
    • userId
    • username
    • tokenVersion(可选)
  • 权限从缓存或数据库获取
  • controller 或 service 层使用:
    • hasRole('ADMIN')
    • hasAuthority('user:read')

这样既保持 JWT 轻量,也利于权限动态调整。


实战代码(可运行)

下面给一套简化但完整的 Spring Boot 3 示例。为了便于直接理解,我使用内存用户做演示;真正上数据库时,只需要把 UserDetailsService 换成数据库实现即可。

1. Maven 依赖

<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>jwt-security-demo</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

2. 配置文件

# src/main/resources/application.yml
server:
  port: 8080

jwt:
  secret: 12345678901234567890123456789012
  expiration: 3600000

这里的 secret 只是演示。生产环境必须使用高强度随机密钥,并通过环境变量或配置中心管理。

3. 启动类

// src/main/java/com/example/demo/JwtSecurityDemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JwtSecurityDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(JwtSecurityDemoApplication.class, args);
    }
}

4. JWT 工具类

// src/main/java/com/example/demo/security/JwtUtil.java
package com.example.demo.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
public class JwtUtil {

    private final SecretKey secretKey;
    private final long expiration;

    public JwtUtil(@Value("${jwt.secret}") String secret,
                   @Value("${jwt.expiration}") long expiration) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
        this.expiration = expiration;
    }

    public String generateToken(String username) {
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expireDate)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return getClaims(token).getSubject();
    }

    public boolean isTokenValid(String token) {
        try {
            Claims claims = getClaims(token);
            return claims.getExpiration().after(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    private Claims getClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

5. 自定义 JWT 过滤器

// src/main/java/com/example/demo/security/JwtAuthenticationFilter.java
package com.example.demo.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtUtil jwtUtil,
                                   UserDetailsService userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            if (jwtUtil.isTokenValid(token)) {
                username = jwtUtil.getUsernameFromToken(token);
            }
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                    );

            authentication.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request)
            );

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }
}

6. Security 配置

// src/main/java/com/example/demo/security/SecurityConfig.java
package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        .password(passwordEncoder.encode("123456"))
                        .roles("ADMIN")
                        .authorities("user:read", "user:create", "user:update", "user:delete")
                        .build(),
                User.withUsername("jack")
                        .password(passwordEncoder.encode("123456"))
                        .roles("USER")
                        .authorities("user:read")
                        .build()
        );
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
            throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .cors(Customizer.withDefaults())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/login").permitAll()
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .requestMatchers("/user/read").hasAuthority("user:read")
                        .requestMatchers("/user/write").hasAuthority("user:create")
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint((request, response, authException) -> {
                            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                            response.setContentType("application/json;charset=UTF-8");
                            response.getWriter().write("{\"code\":401,\"message\":\"未认证或Token无效\"}");
                        })
                        .accessDeniedHandler((request, response, accessDeniedException) -> {
                            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                            response.setContentType("application/json;charset=UTF-8");
                            response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
                        })
                );

        return http.build();
    }
}

7. 登录接口

// src/main/java/com/example/demo/controller/AuthController.java
package com.example.demo.controller;

import com.example.demo.security.JwtUtil;
import lombok.Data;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;

    public AuthController(AuthenticationManager authenticationManager,
                          JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(),
                        request.getPassword()
                )
        );

        String token = jwtUtil.generateToken(authentication.getName());

        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("tokenType", "Bearer");
        return result;
    }

    @Data
    public static class LoginRequest {
        private String username;
        private String password;
    }
}

8. 受保护接口

// src/main/java/com/example/demo/controller/TestController.java
package com.example.demo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/admin/hello")
    public String adminHello() {
        return "hello admin";
    }

    @GetMapping("/user/read")
    public String userRead() {
        return "user read success";
    }

    @PreAuthorize("hasAuthority('user:create')")
    @GetMapping("/user/write")
    public String userWrite() {
        return "user write success";
    }
}

9. 访问流程验证

登录获取 token

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

返回示例:

{
  "token": "eyJhbGciOiJIUzI1NiJ9.xxx.xxx",
  "tokenType": "Bearer"
}

用 token 访问受保护资源

curl http://localhost:8080/admin/hello \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx"

使用普通用户访问管理员接口

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"jack","password":"123456"}'

再访问:

curl http://localhost:8080/admin/hello \
  -H "Authorization: Bearer <jack的token>"

这时应返回 403。


认证到授权的时序图

这张图可以帮助你把整个链路在脑子里串起来。

sequenceDiagram
    participant Client as 前端
    participant Auth as AuthController
    participant Security as Spring Security
    participant JWT as JwtUtil
    participant API as 业务接口

    Client->>Auth: POST /auth/login 用户名+密码
    Auth->>Security: AuthenticationManager.authenticate
    Security-->>Auth: 认证通过
    Auth->>JWT: generateToken(username)
    JWT-->>Auth: JWT
    Auth-->>Client: 返回token

    Client->>API: GET /user/read + Bearer Token
    API->>Security: 进入过滤器链
    Security->>JWT: 校验token
    JWT-->>Security: token有效 + username
    Security->>Security: 构造Authentication
    Security-->>API: 放行
    API-->>Client: 返回业务数据

常见坑与排查

这一部分非常重要。很多项目不是“不会写”,而是“写完以后总是 401/403,不知道卡哪儿”。

1. 登录成功了,但访问接口仍然 401

常见原因

  • 没有带 Authorization
  • Bearer 前缀写错
  • token 已过期
  • JWT 过滤器没有加入过滤链
  • secret 不一致,导致签名校验失败

排查顺序

  1. 先看请求头是否真的带了 token
  2. 打印过滤器日志,看是否进入 JwtAuthenticationFilter
  3. 检查 SecurityContextHolder 是否被成功设置
  4. 检查 JWT 解析是否抛异常
  5. 检查接口路径是否被 permitAllauthenticated 正确匹配

我自己最常踩的一个坑是:路径匹配顺序
比如你前面写了一个过宽的规则,后面的细粒度规则就根本不生效了。

2. 明明登录了,却返回 403

这通常说明“认证成功了,但授权失败”。

常见原因

  • hasRole("ADMIN")hasAuthority("ROLE_ADMIN") 混用
  • 角色前缀理解错误
  • 用户权限没有被正确加载
  • 方法级注解 @PreAuthorize 没开启

关键区别

.hasRole("ADMIN")

本质上会匹配权限:

ROLE_ADMIN

而:

.hasAuthority("ADMIN")

就是严格匹配 ADMIN

所以如果你存的是 ROLE_ADMIN,那就:

  • 要么用 hasRole("ADMIN")
  • 要么用 hasAuthority("ROLE_ADMIN")

不要混着来。

3. 跨域后前端总是请求失败

前后端分离时很常见。

核心点

  • 服务端要开启 CORS
  • 前端要确认是否真的把 Authorization 头传出去了
  • 预检请求 OPTIONS 可能被拦截

如果你的系统是网关统一处理跨域,那应用层就不要重复写一堆冲突配置。

4. token 改了权限却不生效

这是 JWT 设计上的典型问题。

原因

如果权限直接写在 token 里,那么 token 在过期前一直有效。
即使数据库里把某角色权限收回了,老 token 仍可能继续访问。

应对方式

  • access token 设短一点,比如 15~30 分钟
  • 权限从服务端缓存加载,而不是全塞进 token
  • 加入 tokenVersion 字段,用户强制下线时递增版本

5. 过滤器执行了,但 Controller 里拿不到用户

排查:

  • 是否成功 SecurityContextHolder.getContext().setAuthentication(authentication);
  • 过滤器是否执行在 UsernamePasswordAuthenticationFilter 之前
  • 是否被后续过滤器覆盖
  • 是否异步线程中丢失了安全上下文

安全/性能最佳实践

JWT 好用,但别因为它“无状态”就放松安全设计。

安全最佳实践

1. 不要把敏感信息放入 token

不要放这些:

  • 明文密码
  • 手机号、身份证号等高敏信息
  • 过多业务字段

JWT 的 payload 只是 Base64Url 编码,不是加密。
拿到 token 的人是可以解开的。

2. access token 短时有效

推荐:

  • access token:15 分钟 ~ 2 小时
  • refresh token:7 天 ~ 30 天

如果系统是内部后台,时效可以略长;如果是高敏场景,尽量短。

3. secret 必须安全管理

  • 不要写死在代码仓库
  • 使用环境变量、KMS 或配置中心
  • 定期轮换密钥

4. 强制下线能力要提前设计

JWT 最大的工程难点不是登录,而是“撤销”。

可选方案:

  • Redis 黑名单
  • tokenVersion
  • 维护用户最后一次登出时间,与 token 签发时间比对

5. 使用 HTTPS

这是底线。
如果没有 HTTPS,token 被中间人截获后,后果和 Cookie 被盗没本质区别。

性能最佳实践

1. token 不要过大

如果把角色、权限、菜单树全塞进 token:

  • 请求头变大
  • 每次传输成本增加
  • 代理层可能遇到 header 长度限制

建议 token 只保留必要声明。

2. 权限加载加缓存

如果每个请求都查一次用户角色和权限,数据库压力会明显上升。

常见做法:

  • 用户权限放 Redis
  • 设置合理 TTL
  • 用户权限变更时主动失效缓存

3. 过滤器里逻辑尽量轻

JWT 过滤器是每个请求都经过的,不要在里面做重查询和复杂业务逻辑。
它的职责应该尽量单一:解析 token,构建认证上下文


容量与落地建议

从架构角度,再补几条实战建议。

单体后台系统

推荐:

  • JWT + Spring Security
  • 权限模型用角色 + 权限点
  • 权限缓存放本地缓存或 Redis

适合大多数管理系统。

多服务系统

推荐:

  • 网关层统一校验 token
  • 下游服务只信任网关透传的用户上下文,或自行做二次校验
  • 权限判断收敛在网关或统一鉴权服务

否则每个微服务都各写一套 Security 配置,后期维护会非常痛苦。

高安全场景

如果涉及金融、支付、强实名、核心运营权限,建议进一步增强:

  • refresh token + 设备绑定
  • IP/设备指纹风控
  • 操作二次确认
  • 审计日志
  • 更细粒度的资源级权限控制

JWT 不是万能钥匙,它解决的是身份在分布式/前后端分离场景下的高效传递,不等于整个安全体系就完成了。


总结

如果把这套方案压缩成一句话,就是:

用 Spring Security 管“安全流程”,用 JWT 管“无状态身份传递”,用角色与权限模型管“谁能访问什么”。

落地时建议优先记住这几点:

  1. 先分清认证和授权

    • 认证解决“你是谁”
    • 授权解决“你能干什么”
  2. JWT 保持轻量

    • 放用户标识,不要塞太多业务数据
  3. Spring Security 过滤链要接对

    • JWT 过滤器放在合适位置
    • 401 与 403 明确区分
  4. 权限模型不要只靠角色

    • 中型系统建议角色 + 权限点
  5. 给失效与强制下线留后路

    • 短 token、refresh token、黑名单、tokenVersion 至少要考虑一种

如果你现在要在项目里真正开始做,我会建议按这个顺序实施:

  1. 先跑通登录签发 token
  2. 再接入 JWT 过滤器
  3. 然后做接口级权限控制
  4. 最后补强刷新、登出、黑名单、缓存和审计

这样不会一下子把系统复杂度拉满,也更符合真实项目的演进路径。


分享到:

上一篇
《安卓逆向实战:基于 Frida 与 JADX 定位并绕过常见 App 签名校验逻辑》
下一篇
《从浏览器到接口:一次典型 Web 逆向中请求签名算法的定位、还原与自动化复现》