接口鉴权演进:从 Session 到 JWT 再到 OAuth2

背景与动机

任何一个对外提供能力的后端系统,绕不开一个问题:这个请求到底是谁、有没有权限访问?早期系统用户少、服务单体化,这个问题很简单——登录后在服务端记一笔会话就够了。但随着业务演进,我们通常会遇到三类痛点:

  1. 水平扩容难:传统 Session 默认存内存,多实例部署后用户登录态在 A 机器、请求落到 B 机器就失效,要么做 Session 复制,要么上集中式存储。
  2. 前后端分离与多端:前端不再是服务端渲染,移动端、小程序、第三方合作伙伴都要调同一套接口,Cookie 在跨域和原生场景里很别扭。
  3. 第三方接入与授权:当业务需要让「别的公司/应用」在用户授权下访问我们的数据(比如「用 GitHub 登录」),简单的账号密码模型完全不够用。

于是鉴权方案沿着 单体 Session → 无状态 JWT → 标准授权框架 OAuth2 的路径演进。本文用一条「用户登录→访问受保护接口→开放给第三方」的故事线,讲清楚三种方案各自的适用边界和落地代码。

核心概念与技术选型

三种方案不是替代关系,而是不同场景下的取舍:

维度 Session JWT OAuth2
状态位置 服务端(有状态) 客户端(无状态) 授权服务器颁发令牌(无状态)
扩容友好 需集中存储(Redis) 天然友好 天然友好
跨域/多端 依赖 Cookie 任意 Header 携带 任意 Header 携带
第三方授权 不支持 不擅长 原生支持
吊销难度 易(删服务端记录) 难(需黑名单/短过期) 中(可吊销授权)

选型建议:内部单体系统、强管控的后台优先 Session;需要无状态、跨端、自研多服务的 BFF/网关层优先 JWT;需要开放平台、第三方登录、委托授权时上 OAuth2。实际项目里三者常共存:内部用 JWT,对外合作走 OAuth2。

分步实现过程

1. 最朴素的 Session 方案(Spring Boot)

登录成功后,把用户信息塞进 HttpSession,后续请求靠拦截器校验。

// 登录接口:服务端在会话中保存登录态
@PostMapping("/login")
public String login(String username, String password, HttpSession session) {
    User user = userService.verify(username, password);
    if (user == null) throw new RuntimeException("账号或密码错误");
    // 关键:服务端持有会话状态
    session.setAttribute("uid", user.getId());
    session.setAttribute("role", user.getRole());
    return "登录成功";
}

// 拦截器校验:每次请求从服务端取会话
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
    HttpSession session = req.getSession(false);
    if (session == null || session.getAttribute("uid") == null) {
        resp.setStatus(401);
        return false;
    }
    return true;
}

为支持多实例,把 Session 存到 Redis:

# application.yml:用 Redis 集中存储 Session
spring:
  session:
    store-type: redis
  redis:
    host: 10.138.1.165
    port: 6379

2. 无状态 JWT 方案

把登录态编码进令牌,服务端不保存,靠签名校验。用 java-jwt 生成与校验:

// 登录:签发 JWT(设置 2 小时过期 + 防重放 jti)
public String issueToken(User user) {
    Date now = new Date();
    return JWT.create()
            .withSubject(user.getId().toString())
            .withClaim("role", user.getRole())
            .withIssuedAt(now)
            .withExpiresAt(new Date(now.getTime() + 2 * 3600 * 1000))
            .withJWTId(UUID.randomUUID().toString()) // jti,用于黑名单吊销
            .sign(Algorithm.HMAC256("你的密钥"));
}

// 拦截器:解析并校验签名与过期,无需查库
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
    String token = req.getHeader("Authorization").replace("Bearer ", "");
    try {
        DecodedJWT jwt = JWT.require(Algorithm.HMAC256("你的密钥")).build().verify(token);
        req.setAttribute("uid", jwt.getSubject());
        return true;
    } catch (JWTVerificationException e) {
        resp.setStatus(401);
        return false;
    }
}

3. OAuth2 第三方授权(以授权码模式为例)

当「旅游 App」想让用户用「微信」登录时,流程是:重定向到授权服务器 → 用户同意 → 拿 code → 换 access_token。用 Spring Security 一行配置即可对接:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(e -> e.baseUri("/oauth2/authorize"))
                .redirectionEndpoint(e -> e.baseUri("/login/oauth2/code/*"))
            )
            .authorizeRequests(a -> a
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

客户端配置(application.yml)登记第三方(以 GitHub 为例):

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: 你的client_id
            client-secret: 你的client_secret
            scope: read:user

授权成功后,框架自动用 access_token 拉取用户信息并建立本地会话,业务代码无需关心第三方细节。

踩坑、边界条件与权衡取舍

  1. JWT 无法即时吊销:JWT 过期前服务端无法强制失效。对策:缩短过期时间(如 15 分钟)+ 刷新令牌(refresh token)续期;或对关键操作维护 Redis 黑名单(jti 校验)。黑名单会让「无状态」打折扣,需评估。
  2. 密钥与算法陷阱:JWT 常见攻击是用 alg: none 绕过签名,或把密钥写死在代码里。务必用非对称算法(RS256)把公钥分发给药理端,私钥只在签发端保存;密钥定期轮换。
  3. Session 扩容并非只能上 Redis:若实例数少,可用粘性会话(IP 哈希)降低复杂度,但实例重启会掉登录,需权衡可用性。
  4. OAuth2 的 scope 粒度:授权范围太粗会过度授权,太细又增加交互成本。建议按「资源 + 操作」设计 scope(如 order:readorder:write)。
  5. Token 泄露风险:Bearer Token 一旦泄露等同于账号被盗。必须全程 HTTPS,前端避免把 token 存 localStorage(易遭 XSS),优先存内存或 HttpOnly Cookie。
  6. 不要把 JWT 当万能:JWT 适合放用户 ID、角色等非敏感声明,绝不放密码、身份证号等敏感数据——因为 JWT 只是签名未加密(除非用 JWE)。

小结与延伸阅读

鉴权的演进本质是「状态放置」与「信任边界」的权衡:Session 把状态放服务端、管控强但扩容重;JWT 把状态交给客户端、扩容轻但吊销难;OAuth2 把「授权」标准化,解决第三方协作。落地时不必二选一——内部服务用 JWT 做无状态网关鉴权,对外合作走 OAuth2,是最常见也最稳妥的组合。

延伸阅读

  • RFC 6749(OAuth 2.0 授权框架)与 RFC 7519(JWT)
  • Spring Security 官方文档的 Servlet OAuth2 Client 章节
  • 「The OAuth 2.0 Authorization Framework: Bearer Token Usage」了解令牌使用规范
  • 关于 JWT 吊销的「黑名单 vs 短过期 + 刷新」两种工程实践对比
Logo

一站式 AI 云服务平台

更多推荐