java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot OAuth2认证与JWT

SpringBoot中OAuth2认证与JWT Token的核心概念与实践方案

作者:霸道流氓气质

本文主要为大家详细介绍了SpringBoot中OAuth2认证与JWT Token的核心概念与实践方案,带你从零掌握密码模式、授权码模式、Token刷新机制等核心流程,感兴趣的小伙伴可以了解下

一、什么是认证与授权

1.1 基本概念

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

1.2 传统认证方式的问题

传统 Session 方式:

问题:

1. 微服务架构下,用户请求可能到不同服务器,Session 不共享

2. 需要 Session 共享方案(Redis Session),增加复杂度

3. 服务间调用也需要传递身份信息

Token 方式(OAuth2 + JWT):

优点:

1. 无状态:服务器不需要存储 Session

2. 自包含:Token 中携带用户信息,不需要查数据库

3. 跨服务:任何服务都能验证 Token

二、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(头部)

{
  "alg": "RS256",     // 签名算法:RSA + SHA-256
  "typ": "JWT"        // 令牌类型
}

→ Base64Url 编码 → eyJhbGciOiJSUzI1NiIsInxxxxx

Payload(载荷)

{
  "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 → 签名不匹配 → 验证失败

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

窃取攻击:

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 响应格式

{
  "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 配置文件中的公钥

# application.yml
security:
  jwt:
    signing-key: |
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkxxxxxx6fhMDz...
      -----END PUBLIC KEY-----
    resource-ids: test

六、通用示例代码

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

/**
 * 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 资源服务器配置

    /**
     * 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 中获取当前用户信息

/**
 * 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 使用示例

@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 刷新机制

/**
 * 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

/**
 * 微服务间 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 配置声明式定义哪些接口需要/不需要认证

以上就是SpringBoot中OAuth2认证与JWT Token的核心概念与实践方案的详细内容,更多关于SpringBoot OAuth2认证与JWT的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文