下载工作台
Spring Boot Web 开发

JWT、限流与 OAuth2

试读上半部分 · 解锁后可读全文

第 15 章 · JWT、限流与 OAuth2 概念

本章目标:在 shop-spring-demo 使用 jjwt 实现无状态 JWT 登录与鉴权;配置 Spring SecurityJwtAuthenticationFilter;对比 Session 与 Token 方案;接入 Bucket4jResilience4j 实现 API 限流;理解 OAuth2 / OIDC 授权码流程与资源服务器角色(概念级,不连真实 IdP);与 ch14 Swagger Authorize 联调。

学时建议:5~6 小时(含 2 小时 JWT + 限流跟练)

前置ch06 Spring Security;ch13 LoginRequest / TokenVOch14 Bearer 文档配置。


15.1 Session vs JWT

对比项Session(ch06)JWT(本章)
状态服务端存 Session无状态,Claims 在 Token 内
扩展需 Session 共享(Redis)多实例只需校验签名
注销服务端销毁 Session需黑名单或短过期 + Refresh
体积Cookie 小Header 较大
适用传统 SSR、同源SPA、移动端、跨域 API
登录 POST /api/auth/login
        │
        ▼
  校验用户名密码(BCrypt)
        │
        ▼
  签发 JWT(sub, roles, exp)
        │
        ▼
客户端存 localStorage / 内存
        │
        ▼
后续请求 Header: Authorization: Bearer <jwt>
        │
        ▼
JwtAuthenticationFilter 解析 → SecurityContext
教学密钥使用 application-dev.yml 占位符;严禁提交真实 JWT_SECRET 或公司 IdP 客户端密钥。

15.2 引入 jjwt 依赖

pom.xml

<properties>
  <jjwt.version>0.12.5</jjwt.version>
</properties>

<dependencies>
  <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>

配置类 JwtProperties

shop:
  jwt:
    secret: ${JWT_SECRET:dev-only-change-me-in-production-32chars!!}
    issuer: shop-spring-demo
    access-token-ttl: PT2H    # ISO-8601 Duration

15.3 JwtService 签发与解析

package com.example.shop.security.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;

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

@Service
public class JwtService {

    private final JwtProperties props;
    private final SecretKey key;

    public JwtService(JwtProperties props) {
        this.props = props;
        this.key = Keys.hmacShaKeyFor(props.getSecret().getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(String username, List<String> roles) {
        Instant now = Instant.now();
        Instant exp = now.plus(props.getAccessTokenTtl());
        return Jwts.builder()
                .issuer(props.getIssuer())
                .subject(username)
                .claim("roles", roles)
                .issuedAt(Date.from(now))
                .expiration(Date.from(exp))
                .signWith(key)
                .compact();
    }

    public Jws<Claims> parse(String token) {
        return Jwts.parser()
                .verifyWith(key)
                .requireIssuer(props.getIssuer())
                .build()
                .parseSignedClaims(token);
    }

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

    @SuppressWarnings("unchecked")
    public List<String> extractRoles(String token) {
        return parse(token).getPayload().get("roles", List.class);
    }
}
Claim含义
sub用户名
roles角色列表 ROLE_USER, ROLE_ADMIN
exp过期时间
iss签发者,防 Token 混用

15.4 AuthController 登录

@RestController
@RequestMapping("/api/auth")
@Tag(name = "认证")
public class AuthController {

  private final AuthService authService;
  private final JwtService jwtService;

  @PostMapping("/login")
  @Operation(summary = "用户名密码登录,返回 JWT")
  public Result<TokenVO> login(@Valid @RequestBody LoginRequest req) {
      User user = authService.authenticate(req.getUsername(), req.getPassword());
      String token = jwtService.generateAccessToken(
              user.getUsername(),
              user.getAuthorities().stream()
                      .map(GrantedAuthority::getAuthority)
                      .toList());
      TokenVO vo = new TokenVO();
      vo.setAccessToken(token);
      vo.setExpiresIn(jwtService.getAccessTokenTtlSeconds());
      return Result.ok(vo);
  }
}

ch13 的 TokenVO 直接复用;注销可返回 204(客户端删 Token)或维护 Redis 黑名单(选修)。


15.5 JwtAuthenticationFilter

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String header = req.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }
        String token = header.substring(7);
        try {
            String username = jwtService.extractUsername(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails user = userDetailsService.loadUserByUsername(username);
                var auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (JwtException e) {
            res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            res.setContentType(MediaType.APPLICATION_JSON_VALUE);
            res.getWriter().write("{\"code\":40101,\"message\":\"无效或过期的 Token\"}");
            return;
        }
        chain.doFilter(req, res);
    }
}

Security 配置:

@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http,
                                       JwtAuthenticationFilter jwtFilter) throws Exception {
    return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                    .requestMatchers("/api/admin/**").hasRole("ADMIN")
                    .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
}

15.6 Refresh Token(概念与选修)

Token 类型有效期存储用途
Access Token短(15min~2h)内存调 API
Refresh Token长(7~30d)HttpOnly Cookie 或安全存储换新的 Access

选修实现:POST /api/auth/refresh 校验 Refresh Token 后签发新 Access,不在本章必做。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。