首页 / Spring Boot 入门教程 / REST API 认证:JWT 与 OAuth2

Spring Boot 入门教程

REST API 认证:JWT 与 OAuth2

本教程共 48 篇 · 第 34 篇 · 更新于 2026-08-13 · 约 5 分钟阅读

Spring BootJWTOAuth2资源服务器BCryptNimbus无状态认证

本节目标:理解无状态认证的原理,会用 Spring Security 签发和校验 JWT,认识 OAuth2 客户端、资源服务器和授权服务器三个角色。

为什么 REST API 需要无状态认证

上一节的表单登录依赖两样东西:服务端 Session 和浏览器 Cookie。

前后端分离后,这套组合开始别扭:

  • 手机 App 没有 Cookie 机制
  • 多实例部署时,Session 要额外共享存储
  • 跨域请求带 Cookie 有诸多限制

无状态认证换个思路:客户端每次请求自己带凭证,服务端验完即走,不保存会话。

JWT 就是最流行的凭证格式。你认识它之后,会发现整个认证链路都围绕「签发、传递、验证」展开。

JWT 长什么样

JWT(JSON Web Token)由三部分组成,用点分隔:

eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ6aGFuZ3NhbiJ9.8m5T...签名
  • Header:算法和类型,Base64URL 编码
  • Payload:claims,用户信息、过期时间
  • Signature:用私钥对前两段签名,防篡改

服务端验签通过就信任内容。所以 JWT 天生无状态,适合 REST API。

Warning

JWT 内容是 Base64 编码,不是加密。任何拿到 token 的人都能解码看 Payload。别把密码、手机号等敏感信息放进去。

密码存储:BCrypt

认证绕不开密码存储。明文存密码是事故,加盐哈希是底线。

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

createDelegatingPasswordEncoder() 创建委派编码器,存储格式带算法前缀:

{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

校验时它根据前缀自动选算法。将来换算法,老密码不用重存。这也是 Spring Boot 4.x 里的默认 PasswordEncoder。

BCrypt 的特点:自带盐、可调强度($2a$10$ 里的 10 是迭代成本)。同样的密码每次加密结果都不同,彩虹表直接失效。

自己签发 JWT:JwtEncoder 与 JwtDecoder

Spring Security 官方用 Nimbus JOSE 实现 JWT,随资源服务器 starter 一起引入:

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

签发方配置:生成 RSA 密钥对,提供 JwtEncoder / JwtDecoder Bean。

package com.example.demo.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
public class JwtConfig {

    @Bean
    public KeyPair keyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);
        return generator.generateKeyPair();
    }

    @Bean
    public RSAKey rsaKey(KeyPair keyPair) {
        return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
                .privateKey(keyPair.getPrivate())
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource(RSAKey rsaKey) {
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder(RSAKey rsaKey) throws Exception {
        return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build();
    }
}

写一个登录接口,认证通过后签发 token:

package com.example.demo.web;

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Instant;

@RestController
public class AuthController {

    private final JwtEncoder jwtEncoder;

    public AuthController(JwtEncoder jwtEncoder) {
        this.jwtEncoder = jwtEncoder;
    }

    @PostMapping("/authenticate")
    public JwtResponse authenticate(Authentication authentication) {
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plusSeconds(1800))
                .subject(authentication.getName())
                .claim("scope", "message:read") // 业务 scope,openid 属 OIDC 概念
                .build();
        String token = jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
        return new JwtResponse(token);
    }

    record JwtResponse(String token) {
    }
}

/authenticate 由 Spring Security 先做 HTTP Basic 认证,Authentication 参数直接拿到当前用户。

跑起来验证:

# 登录拿 token
curl -u admin:admin123 -X POST http://localhost:8080/authenticate

# 带 token 访问受保护接口
curl -H "Authorization: Bearer <上一步返回的token>" http://localhost:8080/api/orders
Tip

用 jjwt 库也可以,坐标 io.jsonwebtoken:jjwt-apijjwt-impljjwt-jackson。官方路线推荐 spring-security-oauth2-jose,和 Spring Security 集成最顺。

资源服务器校验 JWT

校验方不持有私钥,只验签。配置 jwk-set-uriissuer-uri,指向签发方的公钥地址:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: "http://localhost:8080"

安全配置开启 JWT 资源服务器模式:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/authenticate").authenticated()
            .anyRequest().authenticated()
        )
        .httpBasic(withDefaults())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()))
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    return http.build();
}

这段配置要和上面的 AuthController 放在同一个应用里组合使用:/authenticate 走 HTTP Basic 认证换 token,其余接口用 Bearer token 访问。

SessionCreationPolicy.STATELESS 表示不创建 Session。客户端请求时带:

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Note

生产环境一般由独立的授权服务器签发 token,资源服务器通过 jwk-set-uri(JWK Set 地址)拉公钥验证,双方不共享密钥。
还可以用 spring.security.oauth2.resourceserver.jwt.audiences 限定 JWT 的 aud 声明,防止 token 被别的服务复用。

OAuth2 客户端登录

第三方登录(微信、GitHub、Google)走 OAuth2 授权码流程。先认清 OAuth2 里的角色:

  • 资源所有者:用户本人
  • 客户端:你的应用
  • 授权服务器:第三方账号体系
  • 资源服务器:提供用户信息的接口

流程一句话:用户去授权服务器确认「允许这个应用登录」,授权服务器发授权码,应用拿授权码换 token,再用 token 取用户信息。

Spring Boot 对常见提供商有内置默认值:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: "你的 client-id"
            client-secret: "你的 client-secret"

配置类加 oauth2Login

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(withDefaults());
    return http.build();
}

访问应用任意页面,自动跳转 GitHub 授权页。授权后回调 /login/oauth2/code/github 完成登录。

自定义提供商时,用 spring.security.oauth2.client.provider.<id>.issuer-uri 指向 OIDC 发现地址,授权、token、JWKS 端点自动拉取。

OAuth2 与 JWT 的关系

两者经常一起出现,但它们是两层东西:

  • OAuth2 是授权框架,定义「怎么授权、怎么换 token」
  • JWT 是 token 的一种格式,OAuth2 的 token 可以是 JWT,也可以是不透明字符串

类比:OAuth2 是银行流程,JWT 是其中一种钞票样式。流程定了,钞票可以换版。

选型时记住:JWT 自包含、可离线校验,适合资源服务器分散的场景;不透明 token 需要授权服务器在线校验(introspection),撤销容易、泄露面小。

Spring Security 两种都支持:JWT 用 jwk-set-uri / issuer-uri,不透明 token 用 spring.security.oauth2.resourceserver.opaquetoken.introspection-uri 配内省地址。

授权服务器

需要自己搭授权服务器时,用 Spring Authorization Server 项目:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

注册客户端:

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client:
            registration:
              client-id: "abcd"
              client-secret: "{noop}secret1"
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
              redirect-uris:
                - "http://127.0.0.1:8080/login/oauth2/code/my-client"
              scopes:
                - "openid"
                - "profile"
            require-authorization-consent: true

client-secret 的写法要能被当前 PasswordEncoder 匹配,所以演示里用 {noop},生产要换成加密值。

token 默认有过期时间:授权码 5 分钟、访问令牌 5 分钟、刷新令牌 60 分钟,都可以调。访问令牌过期后用刷新令牌换新的,用户不用重新登录。

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client:
            token:
              authorization-code-time-to-live: 5m
              access-token-time-to-live: 5m
              refresh-token-time-to-live: 60m
Note

版本对照:1.x 时代的 @EnableAuthorizationServer@EnableResourceServer(tutorialspoint 老教程常用)已随旧 OAuth2 模块移除。
现在分别由 Spring Authorization Server 和资源服务器自动配置取代,授权码流程、token 签发全部内置。

初学者常栽的坑

  • JWT 里塞敏感信息:Payload 可解码,只放用户 ID 和角色
  • 秘钥写死在代码里:用环境变量或配置中心,定期轮换
  • token 过期时间设一天:泄露窗口太大,短过期加刷新令牌是常规做法
  • 资源服务器配了 jwk-set-uri 又配 issuer-uri:二选一即可,同时配可能互相干扰
  • 忘记 STATELESS:JWT 场景还开 Session,白占内存
  • 混淆 OAuth2 和 JWT:授权框架和凭证格式是两层,别混着用

本节小结

  • 无状态认证不存 Session,JWT 是主流凭证格式
  • 密码用 {bcrypt} 前缀的委派编码器存储
  • 签发用 JwtEncoder,校验用资源服务器加 jwk-set-uri / issuer-uri
  • 第三方登录用 OAuth2 Client,oauth2Login 一行开启
  • 自建授权服务器用 spring-boot-starter-oauth2-authorization-server