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

《Java Web 开发中基于 Spring Boot + JWT 的权限认证实战:从登录鉴权到接口安全落地》

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

Java Web 开发中基于 Spring Boot + JWT 的权限认证实战:从登录鉴权到接口安全落地

在 Java Web 项目里,认证和授权几乎是绕不过去的一道坎。很多人一开始会觉得:
“登录成功后给个 token,不就完了吗?”

但真到项目里,问题会接二连三冒出来:

  • token 放哪里更合适?
  • 接口怎么区分“未登录”和“没权限”?
  • JWT 里到底该放哪些信息?
  • token 过期怎么处理?
  • Spring Boot 里拦截器、过滤器、Spring Security 到底怎么选?

这篇文章我尽量不空谈概念,而是带你从一个可运行的 Spring Boot + JWT 示例出发,完成一套常见的权限认证流程:

  1. 用户登录,服务端签发 JWT
  2. 客户端请求接口时携带 JWT
  3. 服务端校验 token,解析用户身份
  4. 基于角色控制接口访问权限
  5. 处理过期、伪造、权限不足等常见场景

这套方案很适合中小型后台系统、管理平台、前后端分离项目。
如果你已经会写 Spring Boot 接口,但想把“接口安全”这件事真正落地,这篇会比较适合你。


一、背景与问题

传统基于 Session 的登录机制有几个现实问题:

  • 前后端分离后,跨域和状态维护更麻烦
  • 服务横向扩容时,Session 共享需要额外方案
  • 移动端、Web、多端接入时不够灵活

JWT(JSON Web Token)的优势在于:

  • 无状态:服务端不需要保存登录会话
  • 天然适合前后端分离
  • 易于携带用户身份信息
  • 可扩展:能放角色、用户 ID、签发时间等声明

但 JWT 也不是“用了就安全”。
我见过不少项目里把 JWT 用成了“只是换了个字符串的 Session”,甚至把敏感信息直接塞进去,风险非常大。

所以这篇重点不只是“怎么生成 token”,而是:

  • 怎么在 Spring Boot 里把 JWT 流程串起来
  • 怎么做基础权限控制
  • 怎么避开常见坑
  • 怎么让方案更接近生产可用

二、前置知识与环境准备

1. 技术栈

本文示例使用:

  • JDK 8+
  • Spring Boot 2.7.x
  • Spring Security
  • jjwt 0.11.5
  • Maven

2. 适用场景

适合:

  • 管理后台
  • 内部业务系统
  • RESTful API
  • 前后端分离项目

不太适合直接照搬的场景:

  • 超高安全级别系统
  • 需要单点登录(SSO)
  • 复杂 OAuth2 授权体系
  • 强一致的登录态撤销需求

三、核心原理

先把全链路看清楚,后面代码会顺很多。

1. 登录鉴权流程

flowchart TD
    A[用户提交用户名密码] --> B[Spring Boot 登录接口]
    B --> C{用户名密码是否正确}
    C -- 否 --> D[返回 401/登录失败]
    C -- 是 --> E[生成 JWT]
    E --> F[客户端保存 Token]
    F --> G[请求受保护接口时携带 Authorization: Bearer Token]
    G --> H[JWT 过滤器校验 Token]
    H --> I{Token 是否有效}
    I -- 否 --> J[返回 401/Token 无效或过期]
    I -- 是 --> K[写入 SecurityContext]
    K --> L[进入 Controller]
    L --> M{是否具备所需角色}
    M -- 否 --> N[返回 403/权限不足]
    M -- 是 --> O[返回业务数据]

2. JWT 的组成

JWT 由三部分组成:

  • Header:声明类型和签名算法
  • Payload:承载业务声明
  • Signature:签名,防篡改

格式如下:

header.payload.signature

常见 payload 字段:

  • sub:主题,通常用用户名
  • iat:签发时间
  • exp:过期时间
  • 自定义字段:如 userIdroles

注意:JWT 不是加密,只是 Base64Url 编码后再签名。
所以不要把密码、身份证号、银行卡号这类敏感信息放进去。

3. 认证和授权的区别

这个概念很多人写代码时容易混:

  • 认证(Authentication):你是谁
    比如校验用户名密码、校验 JWT 是否有效
  • 授权(Authorization):你能访问什么
    比如只有 ADMIN 才能访问删除接口

4. Spring Security 在这里扮演什么角色

我们这里用 Spring Security 做两件事:

  • 接管登录后的用户身份上下文
  • 对接口做基于角色的访问控制

JWT 本身只负责“令牌表示身份”,
真正把“当前请求对应哪个用户、具有什么权限”接起来,还是靠 Spring Security。


四、项目结构设计

先给一个简化后的结构,后面代码按这个来:

src/main/java/com/example/jwtdemo
├── JwtDemoApplication.java
├── config
│   └── SecurityConfig.java
├── controller
│   ├── AuthController.java
│   └── UserController.java
├── dto
│   ├── LoginRequest.java
│   └── LoginResponse.java
├── filter
│   └── JwtAuthenticationFilter.java
├── service
│   └── CustomUserDetailsService.java
└── util
    └── JwtTokenUtil.java

五、实战代码(可运行)

下面这套代码是一个最小可运行示例。
为了聚焦 JWT 认证流程,用户数据先用内存方式模拟,后续你可以很容易替换成数据库。

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-demo</artifactId>
    <version>1.0.0</version>

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

    <properties>
        <java.version>8</java.version>
        <jjwt.version>0.11.5</jjwt.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>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
</project>

2. 启动类

JwtDemoApplication.java

package com.example.jwtdemo;

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

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

3. DTO

LoginRequest.java

package com.example.jwtdemo.dto;

public class LoginRequest {

    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

LoginResponse.java

package com.example.jwtdemo.dto;

public class LoginResponse {

    private String token;
    private String tokenType = "Bearer";

    public LoginResponse(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public String getTokenType() {
        return tokenType;
    }
}

4. JWT 工具类

JwtTokenUtil.java

package com.example.jwtdemo.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class JwtTokenUtil {

    // 至少 256 bit,下面是 Base64 编码后的示例密钥
    private static final String SECRET_KEY = "bXktc3VwZXItc2VjcmV0LWtleS0xMjM0NTY3ODkwMTIzNDU2Nzg5MA==";
    private static final long EXPIRATION = 1000 * 60 * 60; // 1小时

    private SecretKey getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    public String generateToken(UserDetails userDetails) {
        List<String> roles = userDetails.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .claim("roles", roles)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }

    public boolean isTokenExpired(String token) {
        return extractClaims(token).getExpiration().before(new Date());
    }

    public boolean validateToken(String token, UserDetails userDetails) {
        String username = extractUsername(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    public Claims extractClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

5. 用户服务

这里用内存用户做演示。
真实项目里通常是查数据库,然后把角色权限映射成 GrantedAuthority

CustomUserDetailsService.java

package com.example.jwtdemo.service;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final PasswordEncoder passwordEncoder;

    public CustomUserDetailsService(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if ("admin".equals(username)) {
            return new User(
                    "admin",
                    passwordEncoder.encode("123456"),
                    Arrays.asList(
                            new SimpleGrantedAuthority("ROLE_ADMIN"),
                            new SimpleGrantedAuthority("ROLE_USER")
                    )
            );
        }

        if ("user".equals(username)) {
            return new User(
                    "user",
                    passwordEncoder.encode("123456"),
                    Arrays.asList(
                            new SimpleGrantedAuthority("ROLE_USER")
                    )
            );
        }

        throw new UsernameNotFoundException("用户不存在");
    }
}

这里有个小提醒:
示例里每次加载用户时都重新 encode("123456"),在演示场景下没问题。
如果你接数据库,应该存已经加密好的密码,而不是运行时现算。


6. JWT 认证过滤器

这个过滤器负责:

  • 从请求头取出 Authorization
  • 解析 Bearer Token
  • 校验 token
  • 把认证信息写入 SecurityContext

JwtAuthenticationFilter.java

package com.example.jwtdemo.filter;

import com.example.jwtdemo.service.CustomUserDetailsService;
import com.example.jwtdemo.util.JwtTokenUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenUtil jwtTokenUtil;
    private final CustomUserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil,
                                   CustomUserDetailsService userDetailsService) {
        this.jwtTokenUtil = jwtTokenUtil;
        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);
            try {
                username = jwtTokenUtil.extractUsername(token);
            } catch (Exception e) {
                // 这里先放行,最终由 Spring Security 统一处理未认证请求
            }
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            var userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtTokenUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
}

7. Spring Security 配置

SecurityConfig.java

package com.example.jwtdemo.config;

import com.example.jwtdemo.filter.JwtAuthenticationFilter;
import com.example.jwtdemo.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final CustomUserDetailsService userDetailsService;

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

    @Bean
    public SecurityFilterChain filterChain(org.springframework.security.config.annotation.web.builders.HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .userDetailsService(userDetailsService)
                .authorizeRequests()
                .antMatchers("/auth/login").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

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

8. 登录接口

这里使用 AuthenticationManager 校验用户名密码。
校验成功后签发 JWT。

AuthController.java

package com.example.jwtdemo.controller;

import com.example.jwtdemo.dto.LoginRequest;
import com.example.jwtdemo.dto.LoginResponse;
import com.example.jwtdemo.util.JwtTokenUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

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

    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;

    public AuthController(AuthenticationManager authenticationManager,
                          JwtTokenUtil jwtTokenUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenUtil = jwtTokenUtil;
    }

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

        String token = jwtTokenUtil.generateToken((org.springframework.security.core.userdetails.UserDetails) authentication.getPrincipal());
        return new LoginResponse(token);
    }
}

9. 受保护接口

UserController.java

package com.example.jwtdemo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

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

@RestController
public class UserController {

    @GetMapping("/user/profile")
    @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
    public Map<String, Object> userProfile(Authentication authentication) {
        Map<String, Object> result = new HashMap<>();
        result.put("message", "用户信息访问成功");
        result.put("username", authentication.getName());
        result.put("authorities", authentication.getAuthorities());
        return result;
    }

    @GetMapping("/admin/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public Map<String, Object> adminDashboard(Authentication authentication) {
        Map<String, Object> result = new HashMap<>();
        result.put("message", "管理员面板访问成功");
        result.put("username", authentication.getName());
        result.put("authorities", authentication.getAuthorities());
        return result;
    }
}

六、调用链路时序图

看到这里,建议你再看一眼请求时序,很多细节会更清晰。

sequenceDiagram
    participant C as Client
    participant A as AuthController
    participant S as Spring Security
    participant J as JwtTokenUtil
    participant F as JwtAuthenticationFilter
    participant U as UserController

    C->>A: POST /auth/login 用户名密码
    A->>S: AuthenticationManager.authenticate()
    S-->>A: 认证成功
    A->>J: generateToken()
    J-->>A: JWT
    A-->>C: 返回 token

    C->>F: GET /user/profile + Authorization: Bearer xxx
    F->>J: 解析并校验 token
    J-->>F: token 有效
    F->>S: 写入 SecurityContext
    S-->>U: 放行请求
    U-->>C: 返回受保护资源

七、逐步验证清单

下面我们实际测一遍。

1. 启动应用

运行 Spring Boot 项目,默认端口 8080

2. 登录获取 token

使用 admin 登录

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

返回类似:

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

使用 user 登录

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

3. 访问普通用户接口

curl http://localhost:8080/user/profile \
  -H "Authorization: Bearer 你的token"

期望:

  • admin 可以访问
  • user 也可以访问

4. 访问管理员接口

curl http://localhost:8080/admin/dashboard \
  -H "Authorization: Bearer 你的token"

期望:

  • admin 可以访问
  • user 返回 403

5. 不带 token 测试

curl http://localhost:8080/user/profile

期望:

  • 返回 401 或未认证响应

八、权限模型怎么落地更合理

很多项目里,权限控制不是简单的“管理员/普通用户”二选一。
更常见的是下面这个模型:

  • 用户(User)
  • 角色(Role)
  • 权限(Permission)

关系一般是:

  • 用户绑定多个角色
  • 角色绑定多个权限
  • 接口根据角色或权限进行校验

可以抽象成下面这样:

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

    class Role {
      +Long id
      +String roleCode
      +String roleName
    }

    class Permission {
      +Long id
      +String permCode
      +String permName
    }

    User --> Role : n..n
    Role --> Permission : n..n

在实际项目里,我更建议:

  • JWT 中只放必要身份信息:userIdusernameroles
  • 细粒度权限可以从数据库或缓存读取
  • 不要把完整权限树都塞进 token,token 会迅速膨胀

什么时候用角色控制,什么时候用权限控制?

  • 角色控制:简单后台、页面级访问控制,足够直接
  • 权限控制:按钮级、接口级、资源级控制,更灵活

例如:

  • ROLE_ADMIN
  • sys:user:list
  • sys:user:delete

对于复杂系统,建议角色 + 权限结合使用。


九、常见坑与排查

这部分很重要,我自己做项目时,这些坑基本都踩过。

1. 明明带了 token,接口还是 401

常见原因

  • 请求头不是 Authorization
  • 前缀不是 Bearer
  • token 已过期
  • JWT 签名密钥不一致
  • 过滤器没有加入 Spring Security 链
  • token 解析异常被吞掉后没有日志

排查建议

先打印这几个点:

System.out.println("Authorization=" + request.getHeader("Authorization"));
System.out.println("username=" + username);
System.out.println("authentication=" + SecurityContextHolder.getContext().getAuthentication());

重点确认:

  • token 是否真的被取到
  • 用户名是否从 token 里成功解析
  • SecurityContext 是否已写入认证信息

2. 登录总是失败,提示密码错误

常见原因

PasswordEncoder 没配对。

比如:

  • 数据库存的是 BCrypt
  • 你却用明文比对
  • 或者每次写死了一个新密码串

正确做法

数据库里保存加密后的密码,例如:

String encoded = passwordEncoder.encode("123456");
System.out.println(encoded);

存进去后,登录时由 Spring Security 自动调用:

passwordEncoder.matches(rawPassword, encodedPassword)

真实项目里不要把明文密码写在代码里,这里只是示意。


3. 返回 403,不是 401

很多人看到 403 会以为“没登录”,其实不一定。

  • 401 Unauthorized:通常表示未认证
  • 403 Forbidden:通常表示已认证,但没权限

比如 user 带着合法 token 去访问 /admin/dashboard,这就该是 403。


4. token 里角色有,接口还是没权限

这是 Spring Security 里一个非常常见的细节:

  • hasRole("ADMIN") 实际匹配的是 ROLE_ADMIN
  • 所以 authority 一般要写成 ROLE_ADMIN

也就是说:

new SimpleGrantedAuthority("ROLE_ADMIN")

而不是:

new SimpleGrantedAuthority("ADMIN")

除非你统一改成 hasAuthority("ADMIN") 这套写法。


5. token 过期后体验很差

用户请求一个接口,突然 401 了,前端如果没处理,会非常割裂。

常见改造思路

  • access token 短时有效,比如 30 分钟
  • refresh token 长时有效,比如 7 天
  • access token 过期后,用 refresh token 换新 token

这篇先不展开完整 refresh token 实现,但如果你的系统面向正式用户,建议尽快补上。


6. 想做“退出登录”,JWT 却是无状态的,怎么办?

这是 JWT 的典型争议点之一。

无状态意味着服务端不存会话,
那你就没法像 Session 一样“删掉服务器里的登录状态”。

常见方案

  • 前端删除本地 token
  • 服务端维护 token 黑名单(Redis)
  • 缩短 access token 有效期
  • 配合 refresh token 做可控续签

如果你的系统对“强制下线”“立刻失效”要求很高,
那就别迷信纯 JWT 无状态方案,适当引入 Redis 是很常见的做法。


十、安全/性能最佳实践

这一节尽量说能直接落地的建议。

1. 不要在 JWT 中放敏感信息

不要放:

  • 明文密码
  • 手机号、身份证号
  • 银行卡等隐私信息
  • 过多业务数据

建议只放:

  • 用户唯一标识
  • 用户名
  • 角色标识
  • 签发时间、过期时间

2. 一定使用 HTTPS

JWT 如果在 HTTP 明文传输中被截获,对方就能直接拿去冒用。
所以:

  • 生产环境必须 HTTPS
  • 不要把 token 暴露在 URL 参数里
  • 优先放在请求头中

3. 设置合理过期时间

不要贪图省事把 token 设成 30 天甚至永久有效。

推荐思路:

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

边界条件要看业务:

  • 内部系统可稍长
  • 高敏感系统应更短

4. 密钥管理不要硬编码

示例里为了演示写在代码中。
生产环境建议:

  • 放到配置中心或环境变量
  • 定期轮换
  • 区分不同环境的密钥
  • 不同服务不要共用同一套签名密钥

例如:

application.yml

jwt:
  secret: ${JWT_SECRET}
  expiration: 3600000

5. 统一异常返回

默认情况下,Spring Security 的未认证、无权限响应可能不够友好。
实际项目建议自定义:

  • AuthenticationEntryPoint:处理 401
  • AccessDeniedHandler:处理 403

这样前端更容易统一接入。

一个简化示例:

package com.example.jwtdemo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class SecurityExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         org.springframework.security.core.AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", "未认证或Token无效");

        response.getWriter().write(objectMapper.writeValueAsString(result));
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json;charset=UTF-8");

        Map<String, Object> result = new HashMap<>();
        result.put("code", 403);
        result.put("message", "权限不足");

        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

然后在 SecurityConfig 中接入。


6. 认证信息尽量走缓存或精简查询

每次请求都查数据库,会给认证链路带来不必要压力。

常见优化方式:

  • token 中放基础角色信息,减少重复查库
  • 用户权限变更不频繁时,配合 Redis 缓存权限
  • 对高频接口减少重复解析与复杂装配

不过也别走极端:
如果你把太多信息放进 token,虽然少查库了,但 token 会变大、更新也更麻烦。

平衡点通常是:

  • token 存基础身份
  • 复杂权限走缓存

7. 对关键接口增加二次校验

像下面这些高风险操作:

  • 删除数据
  • 导出敏感数据
  • 修改核心配置
  • 给用户赋权

建议除了 JWT 登录态,还做额外保护:

  • 操作审计日志
  • 二次密码确认
  • 短信/邮箱验证码
  • 防重放校验
  • 限流

JWT 解决的是“你是谁”,不是“你做这件事一定安全”。


十一、一个更贴近生产的改造方向

如果你准备把示例往生产上靠,我建议按这个顺序演进:

第一步:把内存用户替换成数据库

  • 用户表
  • 角色表
  • 用户角色关联表

第二步:加入统一异常处理

  • 401 / 403 JSON 统一返回
  • token 异常日志更清晰

第三步:加入 refresh token

  • access token 短期有效
  • refresh token 负责续签

第四步:支持登出与黑名单

  • Redis 存已失效 token
  • 关键用户支持强制下线

第五步:做细粒度权限控制

  • @PreAuthorize("hasAuthority('sys:user:list')")
  • 后台菜单、按钮、接口一体化控制

十二、边界条件:什么时候不建议只用 JWT

虽然 JWT 很流行,但也不是所有系统都该默认用它。

以下情况建议你慎重:

1. 你需要强实时撤销登录态

比如:

  • 管理员强制踢人下线
  • 修改密码后所有设备立刻失效

这时单靠纯无状态 JWT 不够,通常要配合 Redis 黑名单或会话中心。

2. 你要做复杂的第三方授权

比如:

  • 微信登录
  • GitHub 登录
  • 企业统一身份平台
  • 多系统单点登录

这类需求往往更适合 OAuth2 / OIDC 体系。

3. 你的系统权限模型极复杂

如果权限变化频繁、细粒度到资源实例级别,
JWT 里固化权限未必是最佳方案,可能更适合“短 token + 服务端动态鉴权”。


十三、总结

我们这篇做了这样一套完整链路:

  • 用 Spring Boot 提供登录接口
  • 用 Spring Security 负责认证上下文与权限控制
  • 用 JWT 表示用户登录身份
  • 通过过滤器解析 token 并写入 SecurityContext
  • 对用户接口、管理员接口做角色保护
  • 说明了常见坑、排查方法和生产实践建议

如果你现在正准备在项目里落地,我给你的可执行建议是:

  1. 先把最小链路跑通:登录、带 token 访问、角色控制
  2. 再补统一异常处理:明确 401 和 403
  3. 生产上尽快补 refresh token 和密钥管理
  4. 高安全场景别迷信纯 JWT 无状态,该上 Redis 黑名单就上
  5. JWT 只放必要信息,别把它当“小型数据库”

最后说句经验话:
认证鉴权这件事,代码量未必最大,但一旦设计随意,后面补救成本非常高。与其上线后被 401、403、权限串用折腾,不如一开始就把认证链路理清楚。

如果你是第一次完整接 Spring Boot + JWT,我建议你把文中的代码亲手敲一遍,再自己加两个角色、加一个权限接口,你会理解得更扎实。


分享到:

上一篇
《Java开发踩坑实战:线程池参数配置不当引发性能抖动与任务堆积的排查与优化》
下一篇
《Spring Boot 中基于 Spring Cache 与 Redis 构建高可用多级缓存的实战指南》