认证授权的演进:从Session到JWT到OAuth2.0与OIDC的完整路径

认证授权的演进:从Session到JWT到OAuth2.0与OIDC的完整路径

一、场景痛点与技术挑战

认证授权是每个Web应用的基石。
但从单体到微服务的演进中,传统方案层层暴露短板。

Session机制在微服务环境下遇到瓶颈。
单体应用Session存本地内存,天然可用。
微服务多实例部署,Session无法跨节点共享。
Session集中存储引入Redis依赖,增加故障点。
SessionID Cookie在跨域场景被浏览器拦截。
移动端对Cookie支持不完善,维护成本高。

JWT解决了分布式共享问题但带来新挑战。
Token自包含,无需服务端存储状态。
但Token一旦签发无法撤销。
用户修改密码后旧Token仍然有效。
Token体积大(1-4KB),每次请求都传输。
JWT Payload明文Base64,敏感信息不能放进去。

OAuth2.0解决第三方授权但实现复杂。
四种授权模式各有适用场景。
Authorization Code、Implicit、Password、Client Credentials。
Implicit模式已废弃,Password模式不推荐。
安全漏洞:CSRF、Token泄露、Redirect URI伪造。
OIDC在OAuth2.0之上增加了身份层。
ID Token、UserInfo Endpoint、Standard Claims。
但OIDC规范复杂,实现细节容易出错。

二、核心原理与架构设计

Session机制

Session是最早的认证方案。
用户登录后服务端创建Session对象。
SessionID通过Cookie返回客户端。
后续请求携带Cookie,服务端校验SessionID。
服务端存储完整的用户状态。

Session局限:

  • 无法跨节点共享(微服务痛点)
  • Cookie跨域受限(CORS场景)
  • 移动端Cookie维护困难
  • CSRF攻击风险(Cookie自动携带)

JWT机制

JWT(JSON Web Token)是自包含Token。
三段式结构:Header.Payload.Signature。
Header:算法类型和Token类型。
Payload:用户声明(claims),如sub、exp、iat。
Signature:用密钥对Header+Payload签名。

JWT优势:

  • 无状态,服务端不存储Session
  • 跨服务共享,微服务天然适用
  • 移动端友好,无需Cookie

JWT局限:

  • 无法撤销,签发后直到过期才失效
  • Payload明文,不能存储敏感信息
  • 体积大,每次请求传输1-4KB

OAuth2.0授权框架

OAuth2.0解决第三方委托授权。
用户授权第三方应用访问自己的资源。
不需要把密码给第三方。

核心角色:

  • Resource Owner:用户
  • Client:第三方应用
  • Authorization Server:授权服务器
  • Resource Server:资源服务器

Authorization Code模式(推荐)。
最安全的授权模式。
用户在授权服务器登录并授权。
授权服务器返回Authorization Code。
Client用Code换取Access Token。
Token不经过用户浏览器,减少泄露风险。

OIDC身份层

OIDC在OAuth2.0之上增加身份信息。
OAuth2.0只管授权,不管身份。
OIDC新增ID Token(JWT格式)。
ID Token包含用户身份声明:sub、name、email。
UserInfo Endpoint提供完整用户信息。
Standard Claims统一身份属性定义。

三、生产级代码实现

JWT签发与验证

"""JWT认证模块生产级实现""" import time import hashlib import hmac import json import base64 from dataclasses import dataclass from typing import Optional @dataclass class JWTConfig: secret: str algorithm: str = "HS256" access_token_expire: int = 900 # 15分钟 refresh_token_expire: int = 86400 # 24小时 issuer: str = "auth-service" audience: str = "api-service" class JWTManager: """JWT签发与验证管理器""" def __init__(self, config: JWTConfig): self.config = config def _b64url_encode(self, data: bytes) -> str: """Base64URL编码(无填充)""" return base64.urlsafe_b64encode(data).rstrip(b"=").decode() def _b64url_decode(self, data: str) -> bytes: """Base64URL解码""" padding = 4 - len(data) % 4 data += "=" * padding return base64.urlsafe_b64decode(data) def _sign(self, header_b64: str, payload_b64: str) -> str: """HMAC-SHA256签名""" message = f"{header_b64}.{payload_b64}" signature = hmac.new( self.config.secret.encode(), message.encode(), hashlib.sha256, ).digest() return self._b64url_encode(signature) def issue_access_token(self, user_id: str, roles: list[str] = None) -> str: """签发Access Token""" now = int(time.time()) header = {"alg": self.config.algorithm, "typ": "JWT"} payload = { "sub": user_id, "iss": self.config.issuer, "aud": self.config.audience, "iat": now, "exp": now + self.config.access_token_expire, "roles": roles or [], "type": "access", } header_b64 = self._b64url_encode(json.dumps(header).encode()) payload_b64 = self._b64url_encode(json.dumps(payload).encode()) signature = self._sign(header_b64, payload_b64) return f"{header_b64}.{payload_b64}.{signature}" def issue_refresh_token(self, user_id: str) -> str: """签发Refresh Token""" now = int(time.time()) header = {"alg": self.config.algorithm, "typ": "JWT"} payload = { "sub": user_id, "iss": self.config.issuer, "iat": now, "exp": now + self.config.refresh_token_expire, "type": "refresh", "jti": hashlib.sha256( f"{user_id}:{now}".encode() ).hexdigest()[:16], } header_b64 = self._b64url_encode(json.dumps(header).encode()) payload_b64 = self._b64url_encode(json.dumps(payload).encode()) signature = self._sign(header_b64, payload_b64) return f"{header_b64}.{payload_b64}.{signature}" def verify_token(self, token: str, expected_type: str = "access") -> Optional[dict]: """验证Token""" try: parts = token.split(".") if len(parts) != 3: return None header_b64, payload_b64, signature_b64 = parts # 验签 expected_sig = self._sign(header_b64, payload_b64) if signature_b64 != expected_sig: return None # 解码Payload payload = json.loads(self._b64url_decode(payload_b64)) # 验证过期 if payload.get("exp", 0) < int(time.time()): return None # 验证类型 if payload.get("type") != expected_type: return None # 验证发行者 if payload.get("iss") != self.config.issuer: return None return payload except Exception: return None

OAuth2.0 Authorization Code流程

"""OAuth2.0授权服务器实现""" import hashlib import secrets import time from dataclasses import dataclass, field from typing import Optional @dataclass class OAuthClient: client_id: str client_secret: str redirect_uris: list[str] grant_types: list[str] = field(default_factory=lambda: ["authorization_code"]) scopes: list[str] = field(default_factory=lambda: ["read", "write"]) @dataclass class AuthCode: code: str client_id: str user_id: str redirect_uri: str scopes: list[str] created_at: float expires_in: int = 600 # 10分钟 @dataclass class TokenPair: access_token: str refresh_token: str expires_in: int token_type: str = "Bearer" scope: str = "" class OAuth2Server: """OAuth2.0授权服务器""" def __init__(self, jwt_manager: JWTManager): self.jwt_manager = jwt_manager self.clients: dict[str, OAuthClient] = {} self.auth_codes: dict[str, AuthCode] = {} self.revoked_refresh: set[str] = set() def register_client(self, client: OAuthClient): """注册OAuth客户端""" self.clients[client.client_id] = client def validate_redirect_uri(self, client_id: str, redirect_uri: str) -> bool: """校验Redirect URI""" client = self.clients.get(client_id) if not client: return False return redirect_uri in client.redirect_uris def generate_auth_code(self, client_id: str, user_id: str, redirect_uri: str, scopes: list[str]) -> str: """生成Authorization Code""" # Code必须随机且不可预测 code = secrets.token_urlsafe(32) auth_code = AuthCode( code=code, client_id=client_id, user_id=user_id, redirect_uri=redirect_uri, scopes=scopes, created_at=time.time(), ) self.auth_codes[code] = auth_code return code def exchange_code_for_token(self, code: str, client_id: str, client_secret: str, redirect_uri: str) -> Optional[TokenPair]: """用Authorization Code换取Token""" auth_code = self.auth_codes.get(code) if not auth_code: return None # Code一次性使用,交换后立即删除 del self.auth_codes[code] # 校验Client身份 client = self.clients.get(client_id) if not client or client.client_secret != client_secret: return None # 校验Redirect URI一致性 if auth_code.redirect_uri != redirect_uri: return None # 校验Code过期 if time.time() > auth_code.created_at + auth_code.expires_in: return None # 签发Token access_token = self.jwt_manager.issue_access_token( auth_code.user_id, roles=scopes ) refresh_token = self.jwt_manager.issue_refresh_token( auth_code.user_id ) return TokenPair( access_token=access_token, refresh_token=refresh_token, expires_in=self.jwt_manager.config.access_token_expire, scope=" ".join(auth_code.scopes), ) def refresh_access_token(self, refresh_token: str, client_id: str, client_secret: str) -> Optional[TokenPair]: """用Refresh Token刷新Access Token""" # 验证Refresh Token payload = self.jwt_manager.verify_token(refresh_token, "refresh") if not payload: return None # 检查是否被撤销 jti = payload.get("jti", "") if jti in self.revoked_refresh: return None # 校验Client client = self.clients.get(client_id) if not client or client.client_secret != client_secret: return None # 签发新Access Token new_access = self.jwt_manager.issue_access_token( payload["sub"] ) new_refresh = self.jwt_manager.issue_refresh_token( payload["sub"] ) # 撤销旧Refresh Token self.revoked_refresh.add(jti) return TokenPair( access_token=new_access, refresh_token=new_refresh, expires_in=self.jwt_manager.config.access_token_expire, )

OIDC身份层扩展

"""OIDC身份层实现""" @dataclass class OIDCClaims: """OIDC标准Claims""" sub: str # 用户唯一标识 name: str # 全名 email: str # 邮箱 email_verified: bool picture: str = "" # 头像URL locale: str = "zh-CN" class OIDCProvider(OAuth2Server): """OIDC身份提供者:OAuth2.0 + ID Token""" # 用户信息存储(生产环境用数据库) user_profiles: dict[str, OIDCClaims] = {} def register_user(self, user_id: str, claims: OIDCClaims): """注册用户身份信息""" self.user_profiles[user_id] = claims def issue_id_token(self, user_id: str, client_id: str, nonce: str = "") -> str: """签发ID Token(JWT格式)""" claims = self.user_profiles.get(user_id) if not claims: return "" now = int(time.time()) header = {"alg": "HS256", "typ": "JWT"} payload = { "iss": self.jwt_manager.config.issuer, "sub": claims.sub, "aud": client_id, "iat": now, "exp": now + self.jwt_manager.config.access_token_expire, "nonce": nonce, # 防重放攻击 "name": claims.name, "email": claims.email, "email_verified": claims.email_verified, "picture": claims.picture, "locale": claims.locale, } header_b64 = self.jwt_manager._b64url_encode(json.dumps(header).encode()) payload_b64 = self.jwt_manager._b64url_encode(json.dumps(payload).encode()) signature = self.jwt_manager._sign(header_b64, payload_b64) return f"{header_b64}.{payload_b64}.{signature}" def userinfo_endpoint(self, access_token: str) -> Optional[dict]: """UserInfo Endpoint:返回完整用户信息""" payload = self.jwt_manager.verify_token(access_token) if not payload: return None user_id = payload.get("sub") claims = self.user_profiles.get(user_id) if not claims: return None return { "sub": claims.sub, "name": claims.name, "email": claims.email, "email_verified": claims.email_verified, "picture": claims.picture, "locale": claims.locale, } def exchange_code_full(self, code: str, client_id: str, client_secret: str, redirect_uri: str, nonce: str = "") -> Optional[dict]: """OIDC完整Token交换:Access + Refresh + ID Token""" token_pair = self.exchange_code_for_token( code, client_id, client_secret, redirect_uri ) if not token_pair: return None # 验证Code获取用户ID # 从access_token payload提取sub access_payload = self.jwt_manager.verify_token(token_pair.access_token) user_id = access_payload.get("sub", "") id_token = self.issue_id_token(user_id, client_id, nonce) return { "access_token": token_pair.access_token, "refresh_token": token_pair.refresh_token, "id_token": id_token, "expires_in": token_pair.expires_in, "token_type": "Bearer", "scope": token_pair.scope, }

四、性能优化与工程实践

JWT Token体积优化。
Payload只放必要claims:sub、exp、iat、type。
角色和权限信息不放JWT,从数据库按需查询。
Token体积从4KB降到500B。
每次请求Header传输开销减少90%。

JWT撤销机制。
JWT自身无法撤销,需要补充方案。
方案一:短过期+Refresh Token轮换。
Access Token过期15分钟。
过期后用Refresh Token获取新Access Token。
用户修改密码时撤销所有Refresh Token。
方案二:Token黑名单。
Redis存储已撤销Token的jti。
验证Token时先查黑名单。
黑名单TTL等于Token剩余过期时间。
过期后自动从黑名单删除,避免无限增长。

OAuth2.0安全实践。
Authorization Code必须一次性使用。
交换后立即删除,防止重放攻击。
Redirect URI精确匹配,不支持通配符。
防止Redirect URI伪造攻击。
Client Secret不能暴露在浏览器端。
Public Client(SPA)用PKCE扩展代替Secret。

OIDC安全实践。
ID Token必须验证nonce参数。
nonce防重放攻击,每次授权请求唯一。
ID Token的aud必须等于client_id。
防止Token被其他Client误用。
ID Token的iss必须等于授权服务器issuer。
UserInfo Endpoint需要Access Token鉴权。

跨域Cookie替代方案。
移动端和SPA不用Cookie。
Access Token存在localStorage或内存。
Refresh Token存在Secure Storage。
Authorization Header: Bearer
比Cookie更灵活,无CORS限制。

微服务间Token传递。
Gateway验证Access Token后转发请求。
内部服务间用Service Token而非用户Token。
Service Token是服务身份标识,不含用户信息。
减少内部服务间的Token传播开销。

五、总结与技术提炼

  1. Session→JWT→OAuth2.0→OIDC是认证授权的自然演进。
    Session适合单体,JWT适合微服务分布式。
    OAuth2.0解决第三方委托授权。
    OIDC在OAuth2.0之上增加标准化身份层。

  2. JWT自包含但无法撤销。
    短过期(15分钟)+Refresh Token轮换是主流方案。
    密码修改时撤销所有Refresh Token。
    Token黑名单用Redis+TTL自动清理。

  3. OAuth2.0 Authorization Code是最安全模式。
    Code一次性使用,Token不经过浏览器。
    Redirect URI精确匹配防伪造。
    Public Client用PKCE代替Client Secret。

  4. OIDC ID Token是标准化的身份声明。
    sub唯一标识、nonce防重放、aud限Client。
    UserInfo Endpoint提供完整用户信息。

  5. Token体积优化只放必要claims。
    sub+exp+iat+type约500B,角色按需查询。
    微服务内部用Service Token而非用户Token。

  6. 跨域场景用Bearer Header替代Cookie。
    Authorization Header无CORS限制。
    移动端和SPA用localStorage/Secure Storage。
    Gateway验证后转发,内部服务间用Service Token。