pin_drop当前位置:知识文库 ❯ 图文
后端通信:17. 请求认证与授权 - 从入门到实践详解
一、教程简介
在现代Web应用中,请求认证与授权是保障数据安全和用户隐私的核心机制。认证(Authentication)验证"你是谁",授权(Authorization)决定"你能做什么"。前端开发者需要理解各种认证方案的工作原理,正确地在HTTP请求中携带认证信息,并妥善处理Token的存储、刷新和过期等生命周期管理。
本教程将全面讲解HTTP认证、Cookie认证、Bearer Token(JWT)、OAuth 2.0等主流认证方案,以及前端如何安全地管理和使用认证信息。
二、核心概念
认证 vs 授权
主流认证方案对比
JWT(JSON Web Token)结构
代码示例
JWT = Header.Payload.Signature
Header: {"alg": "HS256", "typ": "JWT"} → Base64Url编码
Payload: {"sub": "123", "name": "张三", "exp": ...} → Base64Url编码
Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)Token生命周期
代码示例
用户登录 → 服务器验证 → 返回Access Token + Refresh Token
↓
前端存储Token → 请求时携带Token → 服务器验证Token
↓
Token过期 → 使用Refresh Token获取新Token → 继续请求
↓
Refresh Token过期 → 跳转登录页 → 重新认证三、语法与用法
HTTP认证头格式
代码示例
// Basic认证
'Authorization: Basic ' + btoa(username + ':' + password)
// Bearer Token
'Authorization: Bearer ' + accessToken
// API Key
'X-API-Key: abc123def456'
// 自定义Token
'X-Auth-Token: abc123def456'Fetch API携带认证信息
代码示例
// 方式1:在headers中携带Token
fetch('/api/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
// 方式2:携带Cookie(Session认证)
fetch('/api/data', {
credentials: 'include' // 携带Cookie
});
// 方式3:API Key
fetch('/api/data', {
headers: {
'X-API-Key': 'your-api-key'
}
});JWT解码
代码示例
// 解码JWT(不验证签名,仅读取内容)
function decodeJWT(token) {
const parts = token.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT');
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
return { header, payload };
}四、代码示例
示例1:认证方案演示
下面展示四种主流认证方案的完整实现,包括HTTP Basic Auth、Cookie + Session、Bearer Token (JWT)和OAuth 2.0的前端和后端代码。
代码示例
// ========== HTTP Basic Authentication ==========
// 前端实现
const username = 'admin';
const password = 'secret123';
const credentials = btoa(`${username}:${password}`);
fetch('/api/data', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
// 后端验证(Node.js示例)
const auth = req.headers.authorization;
if (auth && auth.startsWith('Basic ')) {
const credentials = Buffer.from(auth.slice(6), 'base64').toString();
const [user, pass] = credentials.split(':');
if (user === 'admin' && pass === 'secret123') {
// 认证通过
}
}代码示例
// ========== Cookie + Session ==========
// 前端登录
const loginResponse = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // 重要:携带Cookie
body: JSON.stringify({ username: 'admin', password: '123456' })
});
// 后续请求(Cookie自动携带)
const dataResponse = await fetch('/api/data', {
credentials: 'include' // 跨域时必须设置
});
// 后端实现(Node.js + Express Session)
const session = require('express-session');
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // 防止XSS读取
secure: true, // 仅HTTPS
sameSite: 'lax', // 防止CSRF
maxAge: 86400000 // 24小时
}
}));代码示例
// ========== Bearer Token (JWT) ==========
// 前端实现 - AuthManager类
class AuthManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
}
// 登录获取Token
async login(username, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
this.storeTokens();
}
// 存储Token
storeTokens() {
localStorage.setItem('accessToken', this.accessToken);
localStorage.setItem('refreshToken', this.refreshToken);
}
// 带认证的请求
async authenticatedFetch(url, options = {}) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
};
let response = await fetch(url, options);
// Token过期,尝试刷新
if (response.status === 401) {
const refreshed = await this.refreshAccessToken();
if (refreshed) {
options.headers['Authorization'] = `Bearer ${this.accessToken}`;
response = await fetch(url, options);
}
}
return response;
}
// 刷新Token
async refreshAccessToken() {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
if (response.ok) {
const data = await response.json();
this.accessToken = data.accessToken;
this.storeTokens();
return true;
}
} catch (e) {}
this.logout();
return false;
}
logout() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}代码示例
// ========== OAuth 2.0 授权码模式 ==========
// 步骤1:重定向到授权页面
function redirectToAuth() {
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your-client-id',
redirect_uri: 'https://your-app.com/callback',
scope: 'profile email',
state: generateRandomState() // 防CSRF
});
window.location.href =
`https://auth.example.com/authorize?${params}`;
}
// 步骤2:回调页面处理授权码
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// 验证state
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('State验证失败');
}
// 步骤3:用授权码换取Token
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://your-app.com/callback'
})
});
const tokens = await response.json();
// 存储 tokens...
}
function generateRandomState() {
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
return state;
}示例2:JWT Token管理器
JWT Token管理器提供解码器、状态监控和存储安全对比功能,帮助理解JWT的内部结构和工作原理。
代码示例
// JWT解码器核心逻辑
function decodeJWT() {
const token = document.getElementById('jwtInput').value.trim();
if (!token) return;
const parts = token.split('.');
if (parts.length !== 3) throw new Error('无效的JWT格式');
// 解码Header
const header = JSON.parse(atob(parts[0].replace(/-/g, '+').replace(/_/g, '/')));
// 解码Payload
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
// 标准声明说明
const claims = {
iss: { desc: '签发者', value: payload.iss },
sub: { desc: '主题(用户ID)', value: payload.sub },
aud: { desc: '接收者', value: payload.aud },
exp: { desc: '过期时间', value: payload.exp ? new Date(payload.exp * 1000).toLocaleString() : '-' },
nbf: { desc: '生效时间', value: payload.nbf ? new Date(payload.nbf * 1000).toLocaleString() : '-' },
iat: { desc: '签发时间', value: payload.iat ? new Date(payload.iat * 1000).toLocaleString() : '-' },
jti: { desc: 'JWT ID', value: payload.jti },
};
// 显示解码结果...
}小贴士:Token存储安全对比
localStorage:XSS风险高,CSRF风险低,持久存储,推荐度一般
sessionStorage:XSS风险高,CSRF风险低,会话级存储,推荐度一般
Cookie(httpOnly):XSS风险低,CSRF风险高,可配置持久性,推荐
内存(变量):XSS风险低,CSRF风险低,刷新丢失,最安全
示例3:Token刷新与请求拦截
双Token方案中,Access Token短期有效(如15分钟),Refresh Token长期有效(如7天)。刷新策略包括主动刷新、被动刷新和队列刷新。
代码示例
// Token刷新机制核心逻辑
class TokenManager {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.isRefreshing = false;
this.refreshSubscribers = []; // 等待刷新完成的请求队列
this.refreshTimer = null;
}
// 安排自动刷新(在过期前5分钟刷新)
scheduleRefresh() {
if (this.refreshTimer) clearTimeout(this.refreshTimer);
const payload = this.decodeToken(this.accessToken);
if (!payload || !payload.exp) return;
const expiresIn = payload.exp * 1000 - Date.now();
const refreshAt = expiresIn - 5 * 60 * 1000; // 提前5分钟
if (refreshAt > 0) {
this.refreshTimer = setTimeout(() => {
this.refreshAccessToken();
}, refreshAt);
}
}
// 刷新Access Token
async refreshAccessToken() {
if (this.isRefreshing) {
// 已在刷新中,加入等待队列
return new Promise(resolve => {
this.refreshSubscribers.push(resolve);
});
}
this.isRefreshing = true;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
if (!response.ok) throw new Error('刷新失败');
const data = await response.json();
this.accessToken = data.accessToken;
if (data.refreshToken) {
this.refreshToken = data.refreshToken;
}
this.scheduleRefresh();
// 通知所有等待的请求
this.refreshSubscribers.forEach(cb => cb(this.accessToken));
this.refreshSubscribers = [];
return this.accessToken;
} catch (error) {
this.clearTokens();
window.location.href = '/login';
throw error;
} finally {
this.isRefreshing = false;
}
}
}代码示例
// 请求/响应拦截器(类Axios实现)
class HttpClient {
constructor(baseURL, tokenManager) {
this.baseURL = baseURL;
this.tokenManager = tokenManager;
this.requestInterceptors = [];
this.responseInterceptors = [];
}
addRequestInterceptor(onFulfilled, onRejected) {
this.requestInterceptors.push({ onFulfilled, onRejected });
}
addResponseInterceptor(onFulfilled, onRejected) {
this.responseInterceptors.push({ onFulfilled, onRejected });
}
// 核心请求方法
async request(url, options = {}) {
let config = {
url: this.baseURL + url,
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
config = await this.runRequestInterceptors(config);
let response = await fetch(config.url, config);
response = await this.runResponseInterceptors(response);
return response;
}
}
// 使用示例
const tokenManager = new TokenManager();
const http = new HttpClient('https://api.example.com', tokenManager);
// 请求拦截器:添加Token
http.addRequestInterceptor((config) => {
if (tokenManager.accessToken) {
config.headers['Authorization'] = `Bearer ${tokenManager.accessToken}`;
}
return config;
});
// 响应拦截器:处理401
http.addResponseInterceptor(
async (response) => {
if (response.status === 401) {
const newToken = await tokenManager.refreshAccessToken();
response.config.headers['Authorization'] = `Bearer ${newToken}`;
return fetch(response.config.url, response.config);
}
return response;
},
(error) => {
console.error('请求失败:', error);
throw error;
}
);示例4:权限控制与路由守卫
RBAC(Role-Based Access Control)基于角色的访问控制,是最常用的权限模型。通过定义角色与权限的映射关系,实现灵活的权限管理。
代码示例
// 权限定义
const PERMISSIONS = {
'article:read': ['guest', 'user', 'editor', 'admin'],
'comment:write': ['user', 'editor', 'admin'],
'article:write': ['editor', 'admin'],
'article:delete': ['admin'],
'user:manage': ['admin'],
'system:config': ['admin']
};
// 权限检查
function hasPermission(role, permission) {
const allowedRoles = PERMISSIONS[permission];
return allowedRoles ? allowedRoles.includes(role) : false;
}
// 路由守卫
const routeGuard = {
requireAuth(to, from, next) {
const token = getToken();
if (!token) {
next({ path: '/login', query: { redirect: to.fullPath } });
return;
}
next();
},
requirePermission(permission) {
return (to, from, next) => {
const user = getCurrentUser();
if (!user) { next({ path: '/login' }); return; }
if (!hasPermission(user.role, permission)) {
next({ path: '/403' });
return;
}
next();
};
}
};
// 全局前置守卫
router.beforeEach((to, from, next) => {
const publicPages = ['/login', '/register', '/'];
const authRequired = !publicPages.includes(to.path);
if (authRequired && !getToken()) {
next({ path: '/login', query: { redirect: to.fullPath } });
} else {
next();
}
});
// 按钮级权限控制(Vue指令)
Vue.directive('permission', {
inserted(el, binding) {
const user = getCurrentUser();
if (!user || !hasPermission(user.role, binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
// 使用: <button v-permission="'article:delete'">删除</button>五、浏览器兼容性
六、注意事项与最佳实践
1. Token安全存储
代码示例
// 推荐方案:内存存储 + 持久化Refresh Token
class SecureTokenStorage {
constructor() {
this.accessToken = null; // 内存中,最安全
this.refreshTokenKey = 'refresh_token';
}
setAccessToken(token) {
this.accessToken = token;
// 不持久化Access Token
}
setRefreshToken(token) {
// Refresh Token可以持久化在httpOnly Cookie中
// 或加密后存储在localStorage
if (token) {
const encrypted = this.encrypt(token);
localStorage.setItem(this.refreshTokenKey, encrypted);
}
}
getAccessToken() {
return this.accessToken;
}
getRefreshToken() {
const encrypted = localStorage.getItem(this.refreshTokenKey);
return encrypted ? this.decrypt(encrypted) : null;
}
clear() {
this.accessToken = null;
localStorage.removeItem(this.refreshTokenKey);
}
encrypt(text) {
return btoa(encodeURIComponent(text));
}
decrypt(text) {
return decodeURIComponent(atob(text));
}
}2. 防止XSS窃取Token
代码示例
// 1. 使用httpOnly Cookie存储Token(前端无法通过JS读取)
// 后端设置:
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// 2. Content Security Policy
// Content-Security-Policy: default-src 'self'; script-src 'self'
// 3. 输入消毒
function sanitizeInput(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
// 4. 避免在URL中传递Token
// 错误:https://api.example.com/data?token=xxx
// 正确:在Authorization头中传递提示:永远不要在URL参数中传递Token,因为URL会被记录在浏览器历史、服务器日志和代理日志中。应该始终使用
Authorization请求头传递。
3. CSRF防护
代码示例
// 方案1:CSRF Token(Cookie认证时必须)
// 后端生成CSRF Token,前端在请求中携带
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/data', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(data)
});
// 方案2:使用Bearer Token代替Cookie(天然防CSRF)
// Token在Authorization头中传递,不通过Cookie自动发送
// 方案3:SameSite Cookie属性
// Set-Cookie: sid=xxx; SameSite=Strict; Secure; HttpOnly七、代码规范示例
规范的认证模块
一个规范的认证模块应该统一管理Token的存储、刷新和请求认证,提供清晰的API接口,并妥善处理各种边界情况。
代码示例
/**
* 认证管理模块
* 统一管理Token的存储、刷新和请求认证
*/
class AuthModule {
constructor(config = {}) {
this.loginUrl = config.loginUrl || '/api/auth/login';
this.refreshUrl = config.refreshUrl || '/api/auth/refresh';
this.logoutUrl = config.logoutUrl || '/api/auth/logout';
this.onAuthChange = config.onAuthChange || (() => {});
this.onTokenExpired = config.onTokenExpired || (() => {});
this._accessToken = null;
this._refreshToken = null;
this._user = null;
this._refreshTimer = null;
this._isRefreshing = false;
this._pendingRequests = [];
this._loadFromStorage();
}
get isAuthenticated() {
return !!this._accessToken;
}
get user() {
return this._user;
}
// 登录
async login(credentials) {
const response = await fetch(this.loginUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || '登录失败');
}
const data = await response.json();
this._setTokens(data.accessToken, data.refreshToken);
this._user = data.user;
this._saveToStorage();
this._scheduleRefresh();
this.onAuthChange(true);
return data;
}
// 带认证的请求
async fetch(url, options = {}) {
options.headers = {
...options.headers,
...this._getAuthHeaders()
};
let response = await fetch(url, options);
if (response.status === 401) {
const newToken = await this._refreshAccessToken();
if (newToken) {
options.headers = {
...options.headers,
...this._getAuthHeaders()
};
response = await fetch(url, options);
} else {
this.onTokenExpired();
throw new Error('认证已过期,请重新登录');
}
}
return response;
}
// 刷新Token(含并发请求队列处理)
async _refreshAccessToken() {
if (this._isRefreshing) {
return new Promise(resolve => {
this._pendingRequests.push(resolve);
});
}
this._isRefreshing = true;
try {
const response = await fetch(this.refreshUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this._refreshToken })
});
if (!response.ok) throw new Error('刷新失败');
const data = await response.json();
this._setTokens(data.accessToken, data.refreshToken || this._refreshToken);
this._saveToStorage();
this._scheduleRefresh();
this._pendingRequests.forEach(cb => cb(this._accessToken));
this._pendingRequests = [];
return this._accessToken;
} catch (error) {
this._clearTokens();
this._pendingRequests.forEach(cb => cb(null));
this._pendingRequests = [];
return null;
} finally {
this._isRefreshing = false;
}
}
_setTokens(accessToken, refreshToken) {
this._accessToken = accessToken;
this._refreshToken = refreshToken;
}
_getAuthHeaders() {
const headers = {};
if (this._accessToken) {
headers['Authorization'] = `Bearer ${this._accessToken}`;
}
return headers;
}
_scheduleRefresh() {
if (this._refreshTimer) clearTimeout(this._refreshTimer);
const payload = this._decodeToken(this._accessToken);
if (!payload || !payload.exp) return;
const expiresIn = payload.exp * 1000 - Date.now();
const refreshAt = Math.max(expiresIn - 5 * 60 * 1000, 0);
this._refreshTimer = setTimeout(() => {
this._refreshAccessToken();
}, refreshAt);
}
_decodeToken(token) {
try {
return JSON.parse(atob(token.split('.')[1]));
} catch { return null; }
}
_saveToStorage() {
if (this._refreshToken) {
sessionStorage.setItem('auth_refresh', this._refreshToken);
}
if (this._user) {
sessionStorage.setItem('auth_user', JSON.stringify(this._user));
}
}
_loadFromStorage() {
this._refreshToken = sessionStorage.getItem('auth_refresh');
const userStr = sessionStorage.getItem('auth_user');
if (userStr) {
try { this._user = JSON.parse(userStr); } catch {}
}
}
_clearTokens() {
this._accessToken = null;
this._refreshToken = null;
this._user = null;
if (this._refreshTimer) clearTimeout(this._refreshTimer);
sessionStorage.removeItem('auth_refresh');
sessionStorage.removeItem('auth_user');
}
}八、常见问题与解决方案
问题1:Token过期后用户操作丢失
代码示例
// 问题:用户填写长表单后提交,Token过期导致401,刷新后表单数据丢失
// 解决方案:在请求拦截器中保存请求配置,刷新后自动重试
async function safeFetch(url, options) {
let response = await fetch(url, options);
if (response.status === 401) {
const newToken = await refreshToken();
if (newToken) {
// 重试原请求(包括请求体)
options.headers['Authorization'] = `Bearer ${newToken}`;
response = await fetch(url, options);
}
}
return response;
}问题2:多标签页Token同步
代码示例
// 问题:标签页A刷新了Token,标签页B还在使用旧Token
// 解决方案1:监听storage事件
window.addEventListener('storage', (event) => {
if (event.key === 'auth_token_updated') {
// 从存储中重新加载Token
authModule._loadFromStorage();
}
});
// 更新Token时通知其他标签页
function updateToken(token) {
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_token_updated', Date.now().toString());
}
// 解决方案2:使用BroadcastChannel
const channel = new BroadcastChannel('auth_channel');
channel.onmessage = (event) => {
if (event.data.type === 'TOKEN_REFRESHED') {
authModule._accessToken = event.data.token;
}
};
// 刷新Token后广播
function broadcastTokenRefresh(token) {
channel.postMessage({ type: 'TOKEN_REFRESHED', token });
}问题3:并发请求导致多次刷新
代码示例
// 问题:多个请求同时401,触发多次Token刷新
// 解决方案:使用请求队列,只刷新一次
let isRefreshing = false;
let pendingRequests = [];
async function handleUnauthorized(originalRequest) {
if (isRefreshing) {
return new Promise(resolve => {
pendingRequests.push(token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
resolve(fetch(originalRequest));
});
});
}
isRefreshing = true;
try {
const newToken = await refreshToken();
pendingRequests.forEach(cb => cb(newToken));
pendingRequests = [];
return newToken;
} catch (error) {
pendingRequests = [];
throw error;
} finally {
isRefreshing = false;
}
}九、总结
请求认证与授权是Web应用安全的核心,本教程涵盖了以下内容:
-
认证方案对比:理解HTTP Basic Auth、Cookie+Session、Bearer Token(JWT)、OAuth 2.0等方案的工作原理、优缺点和适用场景
-
JWT深入理解:掌握JWT的结构(Header、Payload、Signature)、标准声明(iss、sub、exp、iat等)、编解码方法和安全注意事项
-
Token管理:学会Token的安全存储策略、自动刷新机制、请求拦截器的实现,以及并发请求时的Token刷新队列
-
权限控制:理解RBAC权限模型,实现路由守卫和按钮级权限控制,确保前端权限与后端权限一致
-
安全防护:掌握XSS防护、CSRF防护、Token安全存储等安全最佳实践,构建安全的认证体系
-
生产实践:了解Token过期处理、多标签页同步、并发刷新等实际问题的解决方案
常见问题
认证和授权有什么区别?
认证(Authentication)验证"你是谁",例如通过用户名密码登录确认用户身份。授权(Authorization)决定"你能做什么",例如管理员可以删除用户而普通用户不能。认证是授权的前提,先确认身份再分配权限。
JWT和Session哪个更适合现代Web应用?
JWT(Bearer Token)更适合现代Web应用,因为它是无状态的,天然支持分布式系统和跨域场景,移动端友好。Session需要在服务器端存储状态,在微服务架构中需要额外的Session共享方案。但对于传统Web应用,Cookie+Session实现更简单。
Token应该存储在哪里最安全?
最安全的方案是Access Token存储在内存中(JavaScript变量),Refresh Token存储在httpOnly Cookie中。localStorage和sessionStorage容易被XSS攻击窃取。如果必须使用localStorage,应加密存储并设置较短的Token有效期。
如何处理Token过期的情况?
推荐使用双Token方案:Access Token短期有效(15分钟),Refresh Token长期有效(7天)。收到401响应时,使用Refresh Token获取新的Access Token并重试原请求。应实现请求队列机制,避免并发请求导致多次刷新。还可以在Token即将过期前主动刷新。
多标签页之间如何同步Token?
有两种方案:一是监听storage事件,当一个标签页更新Token时通知其他标签页重新加载;二是使用BroadcastChannel API进行标签页间通信,刷新Token后广播通知所有标签页更新。
OAuth 2.0适用于什么场景?
OAuth 2.0适用于第三方应用授权场景,如"使用微信/Google登录"。它允许用户授权第三方应用访问其在其他服务上的信息,而无需将密码提供给第三方。实现复杂度较高,但对于需要跨平台授权的场景是标准解决方案。
本文涉及AI创作
内容由AI创作,请仔细甄别