access token 和refresh token

access token 和refresh token

Access Token 与 Refresh Token:核心概念、区别及实战用法

在认证授权场景(如 API 调用、系统登录)中,Access Token(访问令牌) 和 Refresh Token(刷新令牌) 是一套 “安全且高效” 的令牌机制,核心目的是:在保障系统安全的前提下,减少用户重复登录的频率,提升体验。

一、核心定义与核心作用

1. Access Token(访问令牌)

  • 本质:短期有效的 “通行凭证”,相当于用户登录后拿到的 “临时门禁卡”。
  • 核心作用:作为调用受保护资源(如 API 接口、用户数据)的直接凭证。客户端(如前端、第三方服务)每次请求需携带它,服务端验证通过后才允许访问。
  • 关键特性:
    • 有效期极短(通常 10 分钟~2 小时),过期后立即失效;
    • 包含核心权限信息(如 “允许查询数据”“禁止修改数据”),但不包含用户敏感信息(如密码);
    • 设计目标是 “短期可用、快速失效”,降低泄露风险。

2. Refresh Token(刷新令牌)

  • 本质:长期有效的 “刷新凭证”,相当于用户的 “永久续约凭证”。
  • 核心作用:Access Token 过期后,无需用户重新登录,客户端可通过 Refresh Token 向认证服务器申请新的 Access Token。
  • 关键特性:
    • 有效期很长(通常 7 天~30 天,甚至更久);
    • 仅用于 “刷新 Access Token”,不能直接访问业务资源;
    • 安全性要求极高,通常存储在服务器端(或客户端的安全存储,如移动端 Keychain),避免泄露。

二、为什么需要 “双令牌”?(核心设计逻辑)

如果只使用 Access Token:
  • 若 Access Token 有效期长 → 泄露后风险极高(攻击者可长期访问资源);
  • 若 Access Token 有效期短 → 用户需要频繁登录,体验极差。
“双令牌” 机制完美解决这个矛盾:
  1. Access Token 短期有效:即使泄露,攻击者只能在短时间内滥用;
  2. Refresh Token 长期有效:用户无需频繁登录,过期后才需重新验证;
  3. 职责分离:Access Token 管 “访问资源”,Refresh Token 管 “续期 Access Token”,安全性和体验兼顾。

三、核心区别对比表

特性Access TokenRefresh Token
核心用途 直接访问受保护资源(如 API) 申请新的 Access Token(续期)
有效期 短期(10min~2h) 长期(7 天~30 天)
安全性要求 中等(短期失效,风险低) 极高(长期有效,泄露则后果严重)
存储位置 客户端(前端 localStorage/cookie、后端内存) 优先服务器端存储(数据库),客户端仅存加密后的凭证
携带场景 每次业务请求(如 /api/data、/case/export) 仅 Access Token 过期时,向认证接口请求续期
包含信息 用户身份标识、权限范围(如 role:admin) 仅包含 “刷新权限” 相关标识(不包含业务权限)
泄露影响 短期风险(攻击者临时访问资源) 长期风险(攻击者可无限续期 Access Token)

四、典型工作流程(以 API 调用为例)

以你之前关注的 “DeepSeek API 调用”“案件报告导出接口” 为例,双令牌的完整流程如下:
plaintext
 
 
┌───────────┐       ┌───────────────┐       ┌───────────────┐
│  客户端    │       │ 认证服务器     │       │ 业务服务器     │
└─────┬─────┘       └───────┬───────┘       └───────┬───────┘
      │                     │                     │
1. 登录请求 →─────────────>│                     │
   (用户名+密码/验证码)   │                     │
      │                     │                     │
2. 发放双令牌 ←─────────────│                     │
   (Access Token + Refresh Token)               │
      │                     │                     │
3. 业务请求 →────────────────────────────────────>│
   (携带 Access Token)                          │
      │                     │                     │
4. 验证令牌 →──────────────>│                     │
      │                     │                     │
5. 验证通过 ←──────────────│                     │
      │                     │                     │
6. 返回业务数据 ←────────────────────────────────────│
      │                     │                     │
7. Access Token 过期 →────────────────────────────>│
   (携带失效 Token)                             │
      │                     │                     │
8. 返回“Token 过期” ←────────────────────────────│
      │                     │                     │
9. 刷新 Token 请求 →─────────────>│                     │
   (携带 Refresh Token)        │                     │
      │                     │                     │
10. 验证 Refresh Token →──────────│                     │
      │                     │                     │
11. 发放新 Token ←─────────────│                     │
   (新 Access Token + 可选新 Refresh Token)        │
      │                     │                     │
12. 重新发起业务请求 →────────────────────────────>│
   (携带新 Access Token)                          │
 

关键步骤说明:

  • 步骤 1-2:用户首次登录后,认证服务器验证身份,发放双令牌(Access Token 有效期 30 分钟,Refresh Token 有效期 7 天);
  • 步骤 3-6:客户端携带 Access Token 调用业务接口(如导出案件报告),业务服务器验证 Token 有效后返回数据;
  • 步骤 7-11:Access Token 过期后,客户端用 Refresh Token 申请新 Token,无需用户重新登录;
  • 若 Refresh Token 也过期(如用户 7 天未操作),则客户端引导用户重新登录。

五、Spring Boot 实战:双令牌实现(核心代码)

以下是 Spring Boot 中基于 JWT(JSON Web Token) 实现双令牌的核心代码(生产级简化版),适配 API 接口的认证与续期。

1. 依赖引入(JWT 工具包)

xml
 
 
<!-- pom.xml -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
 

2. 配置类(JWT 密钥、有效期)

java
 
运行
 
 
 
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
    // Access Token 密钥(生产环境需用环境变量注入,避免硬编码)
    private String accessSecret = "your-access-secret-key-32bytes-long";
    // Refresh Token 密钥(需与 Access Token 密钥不同)
    private String refreshSecret = "your-refresh-secret-key-32bytes-long";
    // Access Token 有效期(30 分钟,单位:秒)
    private long accessExpire = 1800;
    // Refresh Token 有效期(7 天,单位:秒)
    private long refreshExpire = 604800;

    // Getter + Setter
    public String getAccessSecret() { return accessSecret; }
    public void setAccessSecret(String accessSecret) { this.accessSecret = accessSecret; }
    public String getRefreshSecret() { return refreshSecret; }
    public void setRefreshSecret(String refreshSecret) { this.refreshSecret = refreshSecret; }
    public long getAccessExpire() { return accessExpire; }
    public void setAccessExpire(long accessExpire) { this.accessExpire = accessExpire; }
    public long getRefreshExpire() { return refreshExpire; }
    public void setRefreshExpire(long refreshExpire) { this.refreshExpire = refreshExpire; }
}
 

3. JWT 工具类(生成 / 验证 Token)

java
 
运行
 
 
 
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtUtils {

    @Autowired
    private JwtConfig jwtConfig;

    // 生成 Access Token(包含用户ID、角色)
    public String generateAccessToken(Long userId, String role) {
        SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getAccessSecret().getBytes());
        return Jwts.builder()
                .claim("userId", userId) // 自定义字段:用户ID
                .claim("role", role)     // 自定义字段:角色权限
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getAccessExpire() * 1000)) // 过期时间
                .signWith(key)           // 签名
                .compact();
    }

    // 生成 Refresh Token(仅包含用户ID,用于续期)
    public String generateRefreshToken(Long userId) {
        SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getRefreshSecret().getBytes());
        return Jwts.builder()
                .claim("userId", userId)
                .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getRefreshExpire() * 1000))
                .signWith(key)
                .compact();
    }

    // 验证 Access Token 有效性
    public boolean validateAccessToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getAccessSecret().getBytes());
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token); // 解析失败则抛出异常
            return true;
        } catch (Exception e) {
            return false; // Token 过期、签名错误、格式错误均返回 false
        }
    }

    // 验证 Refresh Token 有效性
    public boolean validateRefreshToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getRefreshSecret().getBytes());
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 从 Access Token 中获取用户ID
    public Long getUserIdFromAccessToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getAccessSecret().getBytes());
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.get("userId", Long.class);
    }

    // 从 Refresh Token 中获取用户ID
    public Long getUserIdFromRefreshToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(jwtConfig.getRefreshSecret().getBytes());
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
        return claims.get("userId", Long.class);
    }
}
 

4. 认证接口(登录 + 刷新 Token)

java
 
运行
 
 
 
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
public class AuthController {

    @Autowired
    private JwtUtils jwtUtils;

    // 1. 登录接口(发放双令牌)
    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        // 步骤1:验证用户名密码(实际场景需查询数据库)
        boolean isAuthValid = "admin".equals(request.getUsername()) && "123456".equals(request.getPassword());
        if (!isAuthValid) {
            throw new RuntimeException("用户名或密码错误");
        }

        // 步骤2:生成双令牌(假设用户ID=1,角色=admin)
        Long userId = 1L;
        String accessToken = jwtUtils.generateAccessToken(userId, "admin");
        String refreshToken = jwtUtils.generateRefreshToken(userId);

        // 步骤3:返回令牌(生产环境需将 refreshToken 存储到数据库,客户端仅存凭证)
        Map<String, String> result = new HashMap<>();
        result.put("accessToken", accessToken);
        result.put("refreshToken", refreshToken);
        result.put("accessExpire", String.valueOf(jwtConfig.getAccessExpire())); // 告知客户端过期时间
        return result;
    }

    // 2. 刷新 Token 接口(Access Token 过期后调用)
    @PostMapping("/refresh-token")
    public Map<String, String> refreshToken(@RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();

        // 步骤1:验证 Refresh Token 有效性
        if (!jwtUtils.validateRefreshToken(refreshToken)) {
            throw new RuntimeException("Refresh Token 无效或已过期,请重新登录");
        }

        // 步骤2:获取用户ID,生成新的 Access Token(可选刷新 Refresh Token)
        Long userId = jwtUtils.getUserIdFromRefreshToken(refreshToken);
        String newAccessToken = jwtUtils.generateAccessToken(userId, "admin");
        String newRefreshToken = jwtUtils.generateRefreshToken(userId); // 续期 Refresh Token

        // 步骤3:更新数据库中的 Refresh Token(旧的失效)
        // update user_refresh_token set token = newRefreshToken where user_id = userId;

        // 步骤4:返回新令牌
        Map<String, String> result = new HashMap<>();
        result.put("accessToken", newAccessToken);
        result.put("refreshToken", newRefreshToken);
        return result;
    }

    // 入参实体类(简化版)
    static class LoginRequest {
        private String username;
        private String password;
        // Getter + Setter
    }

    static class RefreshTokenRequest {
        private String refreshToken;
        // Getter + Setter
    }
}
 

5. 拦截器(验证 Access Token)

java
 
运行
 
 
 
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从请求头获取 Access Token(默认字段:Authorization: Bearer {token})
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("缺少 Access Token");
            return false;
        }
        String accessToken = authHeader.substring(7); // 截取 "Bearer " 后的 Token

        // 2. 验证 Token 有效性
        if (!jwtUtils.validateAccessToken(accessToken)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Access Token 无效或已过期");
            return false;
        }

        // 3. Token 有效,放行(可将用户ID存入请求属性,供业务接口使用)
        Long userId = jwtUtils.getUserIdFromAccessToken(accessToken);
        request.setAttribute("userId", userId);
        return true;
    }
}
 

6. 注册拦截器(仅拦截受保护接口)

java
 
运行
 
 
 
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**") // 拦截所有 /api 开头的业务接口
                .excludePathPatterns("/login", "/refresh-token"); // 放行登录、刷新 Token 接口
    }
}
 

六、生产环境关键注意事项

  1. 密钥安全:
    • Access Token 和 Refresh Token 的密钥必须不同,且长度≥32 字节;
    • 密钥不能硬编码在代码中,需通过环境变量(如 Docker 环境变量、K8s ConfigMap)注入;
  2. Refresh Token 存储:
    • 优先存储在服务器端数据库(如 user_refresh_token 表,关联用户 ID、令牌、过期时间);
    • 客户端仅存储加密后的 Refresh Token,避免明文泄露;
  3. 令牌撤销:
    • 用户退出登录时,需删除服务器端的 Refresh Token(使其无法续期);
    • 若怀疑令牌泄露,需提供 “强制登出” 功能,清空该用户的所有 Refresh Token;
  4. HTTPS 传输:
    • 所有令牌(Access Token、Refresh Token)必须通过 HTTPS 传输,防止中间人窃取;
  5. JWT 优化:
    • JWT 是 “自包含令牌”(无需查询数据库验证),但无法主动撤销,需结合服务器端黑名单(如 Redis 存储失效 Token);
    • 避免在 JWT 中存储敏感信息(如密码、手机号),仅存储非敏感标识(用户 ID、角色)。

七、常见使用场景

  1. API 调用认证:如 DeepSeek API、第三方支付 API,用 Access Token 验证请求合法性;
  2. 前后端分离系统:前端存储 Access Token(localStorage),Refresh Token 存储在 HttpOnly Cookie(防止 XSS 攻击);
  3. 移动端 APP:Access Token 存储在内存,Refresh Token 存储在 Keychain(iOS)/ Keystore(Android);
  4. 微服务架构:统一认证中心发放双令牌,所有微服务通过 Access Token 验证身份,无需重复登录。
总结:Access Token 是 “短期通行凭证”,Refresh Token 是 “长期续期凭证”,二者配合实现 “安全 + 便捷” 的认证机制。在 Spring Boot 中,结合 JWT + 拦截器 / 过滤器可快速实现,核心是做好密钥安全、令牌存储和过期管理。
posted @ 2025-11-21 17:01  hanease  阅读(139)  评论(0)    收藏  举报