SpringBoot中使用OAuth2 认证与 JWT Token — 概念、原理与实践

SpringBoot中使用OAuth2 认证与 JWT Token --- 概念、原理与实践


一、什么是认证与授权

1.1 基本概念

概念 英文 含义 类比
认证(Authentication) AuthN 你是谁?验证身份 刷工卡进公司大门
授权(Authorization) AuthZ 你能做什么?验证权限 进门后哪些楼层/房间你能进

1.2 传统认证方式的问题

复制代码
传统 Session 方式:
  用户登录 → 服务器创建 Session → 返回 SessionId(Cookie)
  后续请求携带 Cookie → 服务器查 Session 确认身份

问题:
  1. 微服务架构下,用户请求可能到不同服务器,Session 不共享
  2. 需要 Session 共享方案(Redis Session),增加复杂度
  3. 服务间调用也需要传递身份信息
Token 方式(OAuth2 + JWT):
  用户登录 → 认证中心签发 Token → 返回给客户端
  后续请求携带 Token → 各服务用公钥验证签名即可确认身份
  
优点:
  1. 无状态:服务器不需要存储 Session
  2. 自包含:Token 中携带用户信息,不需要查数据库
  3. 跨服务:任何服务都能验证 Token

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、OAuth2 协议

2.1 是什么

OAuth2 是一个授权框架标准(RFC 6749),定义了客户端如何获取访问令牌(Access Token)的流程。

2.2 角色定义

角色 说明
Resource Owner 资源拥有者(用户)
Client 客户端应用
Authorization Server 认证服务器
Resource Server 资源服务器

2.3 授权模式

模式 场景
密码模式(Password) 用户名+密码直接换 Token
授权码模式(Authorization Code) 第三方应用授权(如微信登录)
客户端模式(Client Credentials) 服务间无用户参与的调用
刷新令牌模式(Refresh Token) 用旧 Token 换新 Token

2.4 密码模式完整流程

复制代码
前端/客户端                    认证服务器                      资源服务器
    │                            │                              │
    │─── POST /oauth/token ────→│                              │
    │    grant_type=password     │                              │
    │    username=test        │                              │
    │    password=123456         │                              │
    │    Authorization: Basic    │                              │
    │    (clientId:clientSecret) │                              │
    │                            │                              │
    │                            ├── 验证用户名密码              │
    │                            ├── 验证 clientId/Secret       │
    │                            ├── 生成 JWT Token(RSA私钥签名)│
    │                            │                              │
    │←── 返回 Token ─────────────│                              │
    │    {                       │                              │
    │      "access_token":"eyJ..",│                              │
    │      "token_type":"bearer",│                              │
    │      "expires_in":53640    │                              │
    │    }                       │                              │
    │                            │                              │
    │─── GET /api/xxx/xxx ─────────────────────────────────→│
    │    Authorization: Bearer eyJ...                           │
    │                            │                              │
    │                            │    ├── 用RSA公钥验证签名      │
    │                            │    ├── 检查Token是否过期      │
    │                            │    ├── 从Token中提取用户信息  │
    │                            │    └── 执行业务逻辑           │
    │                            │                              │
    │←── 返回业务数据 ────────────────────────────────────────│

三、JWT(JSON Web Token)

3.1 结构

JWT 由三部分组成,用 . 分隔:

复制代码
eyJhbGciOiJSUzI1NiJ9.eyJtZW1iZXJfaWQiOjIxNTIyNyxxxxxxxx.签名部分

Header.Payload.Signature
Header(头部)
json 复制代码
{
  "alg": "RS256",     // 签名算法:RSA + SHA-256
  "typ": "JWT"        // 令牌类型
}

→ Base64Url 编码 → eyJhbGciOiJSUzI1NiIsInxxxxx

Payload(载荷)
json 复制代码
{
  "user_name": "test",
  "user_id": 1,
  "member_id": 215227,
  "parent_id": 215227,
  "scope": ["all"],
  "exp": 1758640298,          // 过期时间(Unix时间戳)
  "authorities": ["ROLE_USER"],
  "jti": "53d5b75b-7a9c...",  // Token唯一标识
  "client_id": "demo_1"
}

→ Base64Url 编码 → eyJtZW1iZXJfaWQiOxxxx

Signature(签名)
复制代码
签名 = RSA_SHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey  // 认证中心的RSA私钥
)

→ Base64Url 编码 → dGhpcyBpcyBzaWduYXR1cmU...

3.2 为什么安全

复制代码
篡改攻击:
  攻击者修改 Payload 中的 member_id → 签名不匹配 → 验证失败

伪造攻击:
  攻击者没有私钥 → 无法生成合法签名 → 验证失败

窃取攻击:
  Token 被窃取 → 有时效性(exp),过期后失效
  → 使用 HTTPS 防止传输过程被窃取

3.3 RSA 非对称加密

复制代码
认证中心(签发方):持有【私钥】
  → 用私钥签名生成 Token

各微服务(验证方):持有【公钥】
  → 用公钥验证签名真伪
  → 公钥可以公开分发,不影响安全性
  → 任何服务都能独立验证 Token,不需要调认证中心

四、本地获取 Token 的完整流程

4.1 请求格式

复制代码
POST https://127.0.0.1/oauth/token

Headers:
  Authorization: Basic ZGVtb19jbG91ZF9hcGlfZ2F0ZXdheToxMjM0NTY3OA==
                       ↑ Base64(clientId:clientPassword)
                       ↑ Base64("demo_cloud_api_gateway:12345678")
  Content-Type: application/x-www-form-urlencoded

Body (form-urlencoded):
  grant_type=password
  username=test
  password=123456

4.2 响应格式

json 复制代码
{
  "access_token": "eyJhbGciOiJSUzI1xxxxx.eyJtZW1iZXJfaWQiOjIxNTIyNy...",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJSUzI1NiIsIxxxxxx25hbWUi...",
  "expires_in": 53640,
  "scope": "all",
  "member_id": 215227,
  "parent_id": 215227,
  "user_id": 1103671701,
  "user_name_cn": "easyhandle_test",
  "jti": "53d5b75b-7a9c-495c-83xxxx"
}

4.3 使用 Token 调业务接口

复制代码
GET http://localhost:3012/api/page/xxx/xxx-xxx/get-order-id?...

Headers:
  Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
                 ↑ 注意:Bearer 和 token 之间有一个空格

五、资源服务器验证流程

5.1 Spring Security OAuth2 验证链

复制代码
HTTP 请求到达
  │
  ├─→ OAuth2AuthenticationProcessingFilter
  │     ├── 从 Header 提取 "Bearer xxx" → 获取 token 字符串
  │     └── 调用 TokenStore 验证 token
  │
  ├─→ JwtAccessTokenConverter(JWT 验证器)
  │     ├── 用 RSA 公钥验证签名 → 签名合法?
  │     ├── 检查 exp 字段 → Token 过期了?
  │     ├── 检查 resource_ids → 当前服务有权接受此 Token?
  │     └── 解析 Payload → 提取 user_id/member_id 等信息
  │
  ├─→ SecurityContextHolder
  │     └── 将认证信息放入当前线程的安全上下文
  │
  └─→ Controller 方法执行
        └── 通过 JwtTokenUtil.getMemberId() 获取当前用户信息

5.2 配置文件中的公钥

yaml 复制代码
# application.yml
security:
  jwt:
    signing-key: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkxxxxxx6fhMDz...
      -----END PUBLIC KEY-----
    resource-ids: test

六、通用示例代码

6.1 获取 Token(认证中心客户端)

java 复制代码
/**
 * OAuth2 Token 获取工具.
 * 用于:服务间调用时获取系统级 Token.
 */
@Component
public class OAuth2TokenClient {

    @Value("${auth.url}")
    private String authUrl;

    @Value("${auth.client-id}")
    private String clientId;

    @Value("${auth.client-password}")
    private String clientPassword;

    @Value("${auth.username}")
    private String username;

    @Value("${auth.password}")
    private String password;

    private final RestTemplate restTemplate = new RestTemplate();

    /**
     * 使用密码模式获取 Access Token.
     */
    public String getAccessToken() {
        // 1. 构建 Basic Auth Header(clientId:clientPassword 的 Base64)
        String credentials = clientId + ":" + clientPassword;
        String basicAuth = "Basic " + Base64.getEncoder()
            .encodeToString(credentials.getBytes(StandardCharsets.UTF_8));

        // 2. 构建请求头
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", basicAuth);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // 3. 构建请求体(form-urlencoded)
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "password");
        body.add("username", username);
        body.add("password", password);

        // 4. 发送请求
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.postForEntity(
            authUrl + "/oauth/token", request, Map.class);

        // 5. 提取 access_token
        Map<String, Object> responseBody = response.getBody();
        return (String) responseBody.get("access_token");
    }
}

6.2 资源服务器配置

java 复制代码
    /**
     * JWT Token 存储器.
     * 用公钥验证签名,解析 Payload 中的用户信息.
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * JWT 转换器:配置 RSA 公钥.
     * 认证中心用私钥签名,资源服务器用公钥验证.
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(publicKey);  // 设置 RSA 公钥
        return converter;
    }
}

6.3 从 Token 中获取当前用户信息

java 复制代码
/**
 * JWT Token 用户信息工具类.
 * 从 Spring Security 上下文中提取 Token 中携带的用户信息.
 */
public class JwtTokenUtil {

    /**
     * 获取当前登录用户的会员ID.
     * 从 JWT Payload 中的 member_id 字段获取.
     */
    public static Long getMemberId() {
        Map<String, Object> additionalInfo = getAdditionalInfo();
        Object memberId = additionalInfo.get("member_id");
        return memberId != null ? Long.valueOf(memberId.toString()) : null;
    }

    /**
     * 获取当前登录用户ID.
     */
    public static Long getUserId() {
        Map<String, Object> additionalInfo = getAdditionalInfo();
        Object userId = additionalInfo.get("user_id");
        return userId != null ? Long.valueOf(userId.toString()) : null;
    }

    /**
     * 获取当前登录用户名.
     */
    public static String getUserName() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            return authentication.getName();
        }
        return null;
    }

    /**
     * 从 Security 上下文获取 Token 中的附加信息(additional_info).
     */
    private static Map<String, Object> getAdditionalInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return Collections.emptyMap();
        }
        Object details = authentication.getDetails();
        if (details instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails oauthDetails = (OAuth2AuthenticationDetails) details;
            Object decoded = oauthDetails.getDecodedDetails();
            if (decoded instanceof Map) {
                return (Map<String, Object>) decoded;
            }
        }
        return Collections.emptyMap();
    }
}

6.4 使用示例

java 复制代码
@RestController
public class DeliveryController {

    /**
     * 查询当前用户的发货单列表.
     * Token 中携带了 memberId,无需前端传入.
     */
    @GetMapping("/api/page/xxx/delivery/list")
    public Result<List<DeliveryDto>> listDelivery() {
        // 从 Token 中获取当前用户的 memberId
        Long memberId = JwtTokenUtil.getMemberId();
        if (memberId == null) {
            throw new UnauthorizedException("未登录");
        }
        // 用 memberId 查询该用户的发货单
        List<DeliveryDto> list = deliveryService.listByMemberId(memberId.intValue());
        return Result.success(list);
    }
}

6.5 Token 刷新机制

java 复制代码
/**
 * Token 刷新示例.
 * access_token 有效期短(14小时),过期后用 refresh_token 换新的.
 */
@Component
public class TokenRefreshService {

    @Value("${auth.url}")
    private String authUrl;

    @Value("${auth.client-id}")
    private String clientId;

    @Value("${auth.client-password}")
    private String clientPassword;

    /**
     * 使用 refresh_token 获取新的 access_token.
     * 无需用户重新输入密码.
     */
    public String refreshToken(String refreshToken) {
        String credentials = clientId + ":" + clientPassword;
        String basicAuth = "Basic " + Base64.getEncoder()
            .encodeToString(credentials.getBytes(StandardCharsets.UTF_8));

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", basicAuth);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "refresh_token");     // 刷新模式
        body.add("refresh_token", refreshToken);      // 用旧的 refresh_token

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<Map> response = restTemplate.postForEntity(
            authUrl + "/oauth/token", request, Map.class);

        return (String) response.getBody().get("access_token");
    }
}

6.6 服务间调用携带 Token

java 复制代码
/**
 * 微服务间 Feign 调用时自动携带 Token.
 */
@Component
public class FeignTokenInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求的 Security 上下文中获取 Token
        ServletRequestAttributes attributes =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String token = request.getHeader("Authorization");
            if (token != null && !token.isEmpty()) {
                // 将当前请求的 Token 透传给下游服务
                template.header("Authorization", token);
            }
        }
    }
}

// Feign 客户端定义
@FeignClient(value = "xxx-service-order",
    fallbackFactory = OrderFeignFallbackFactory.class)
public interface OrderFeign {

    @PostMapping("/api/inner/xxx/management/get-order-status-info")
    RestControllerResult<List<OrderInfoDto>> getOrderStatusInfo(
        @RequestBody List<GetOrderStatusInfoParamsDto> params);
    // Feign 调用时自动携带 Authorization Header(由 FeignTokenInterceptor 注入)
}

七、Token 生命周期

复制代码
┌────────────────────────────────────────────────────────────────┐
│                     Token 生命周期                               │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  用户登录 → 认证中心签发 Token(有效期14小时)                    │
│      │                                                         │
│      ▼                                                         │
│  access_token 有效期间:                                        │
│    - 前端每次请求携带 Token                                      │
│    - 各服务用公钥验证签名                                        │
│    - 从 Payload 获取用户信息                                     │
│      │                                                         │
│      ▼                                                         │
│  access_token 即将过期(前端检测 expires_in):                   │
│    - 用 refresh_token 换新的 access_token                       │
│    - 无需用户重新登录                                           │
│      │                                                         │
│      ▼                                                         │
│  refresh_token 也过期:                                         │
│    - 用户需要重新登录                                           │
│    - 前端跳转登录页                                             │
│                                                                │
└────────────────────────────────────────────────────────────────┘

八、安全要点

要点 说明
HTTPS 必须 Token 在网络传输中必须加密,防止被截获
Token 不存 localStorage 容易被 XSS 攻击窃取,优先用 httpOnly Cookie
短有效期 access_token 有效期不宜过长(本项目14小时)
签名验证 每次请求都验签名,防止 Token 被篡改
最小权限 Token 中的 scope/authorities 限制可访问的资源
私钥保护 RSA 私钥只在认证中心,不分发给任何其他服务
resource_ids 每个服务只接受自己 resourceId 的 Token

九、关键设计总结

设计要点 实现方式 收益
无状态认证 JWT 自包含用户信息 服务器不存 Session,水平扩展无压力
非对称加密 RSA 私钥签名 + 公钥验签 私钥不出认证中心,安全性高
Token 透传 Feign Interceptor 自动携带 服务间调用无需手动传 Token
用户信息提取 SecurityContextHolder + @Version 解析 业务代码一行获取 memberId
配置化公钥 application.yml 配置 不同环境使用不同密钥对
刷新机制 refresh_token 换新 token 用户无感续期,体验好
权限控制 security.matchers 配置 声明式定义哪些接口需要/不需要认证
相关推荐
dogstarhuang1 小时前
Kimi K3 本地部署实战:从 1.56TB 权重到推理服务的完整成本分析
java·人工智能·后端·ai·开源·接口·程序员创富
星栈1 小时前
我以为 TS7.0 只是换个版本号,结果编译快了 9 倍,也踩了 5 个坑
后端·typescript·node.js
就不掉头发1 小时前
C++函数模板
java·开发语言·c++
Csvn1 小时前
📊 SQL 入门 Day 15:日期与字符串函数 — SQL 中的数据处理瑞士军刀
后端·sql
用户84298142418101 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
snow@li1 小时前
Vue Axios封装与SpringBoot Payload封装全景关联分析(前后端数据交互底层闭环)
前端·vue.js·spring boot
进击的丸子1 小时前
虹软人脸SDK 调用常见问题和最佳实践指南
后端
用户298698530141 小时前
Word 转 PDF 的 3 种自动化实现:从桌面操作到后端服务集成
java·人工智能·后端