认证与授权
JWT (JSON Web Token)
结构
header.payload.signature
header: {"alg": "HS256", "typ": "JWT"}
payload: {"sub": "user123", "iat": 1516239022, "exp": 1516242622}
signature: HMACSHA256(base64(header) + "." + base64(payload), secret)header.payload.signature
header: {"alg": "HS256", "typ": "JWT"}
payload: {"sub": "user123", "iat": 1516239022, "exp": 1516242622}
signature: HMACSHA256(base64(header) + "." + base64(payload), secret)使用流程
客户端 服务端
│ │
├─ POST /login ──────────► │ 用户名密码
│ │
│ ◄── JWT Token ──────────┤ 服务端验证并签发
│ │
├─ GET /api/data ─────────► │ 请求头: Authorization: Bearer <token>
│ │
│ ◄── 200 OK ─────────────┤ 服务端验证 Token 后返回数据客户端 服务端
│ │
├─ POST /login ──────────► │ 用户名密码
│ │
│ ◄── JWT Token ──────────┤ 服务端验证并签发
│ │
├─ GET /api/data ─────────► │ 请求头: Authorization: Bearer <token>
│ │
│ ◄── 200 OK ─────────────┤ 服务端验证 Token 后返回数据Token 存储
js
// ❌ localStorage(XSS 可读取)
localStorage.setItem('token', jwt)
// ✅ HttpOnly Cookie(防 XSS 窃取)
document.cookie = `token=${jwt}; HttpOnly; Secure; SameSite=Lax; Path=/`
// ✅ 内存存储(最安全,但刷新丢失)
let token = null
export function setToken(t) { token = t }
export function getToken() { return token }// ❌ localStorage(XSS 可读取)
localStorage.setItem('token', jwt)
// ✅ HttpOnly Cookie(防 XSS 窃取)
document.cookie = `token=${jwt}; HttpOnly; Secure; SameSite=Lax; Path=/`
// ✅ 内存存储(最安全,但刷新丢失)
let token = null
export function setToken(t) { token = t }
export function getToken() { return token }刷新机制
js
// access token(短期,15分钟)+ refresh token(长期,7天)
async function refreshToken() {
const res = await fetch('/api/auth/refresh', {
credentials: 'include', // 自动携带 refresh token cookie
})
if (res.ok) {
const { accessToken } = await res.json()
setToken(accessToken)
return accessToken
}
// 刷新失败,跳转登录
redirectToLogin()
}
// axios 拦截器自动处理
api.interceptors.response.use(
res => res,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshToken()
error.config.headers.Authorization = `Bearer ${newToken}`
return api(error.config) // 重试原请求
}
return Promise.reject(error)
}
)// access token(短期,15分钟)+ refresh token(长期,7天)
async function refreshToken() {
const res = await fetch('/api/auth/refresh', {
credentials: 'include', // 自动携带 refresh token cookie
})
if (res.ok) {
const { accessToken } = await res.json()
setToken(accessToken)
return accessToken
}
// 刷新失败,跳转登录
redirectToLogin()
}
// axios 拦截器自动处理
api.interceptors.response.use(
res => res,
async (error) => {
if (error.response?.status === 401) {
const newToken = await refreshToken()
error.config.headers.Authorization = `Bearer ${newToken}`
return api(error.config) // 重试原请求
}
return Promise.reject(error)
}
)OAuth 2.0
授权码流程(Authorization Code)
用户 ───→ 第三方应用 ───→ 授权服务器 ───→ 资源服务器
│ │ │ │
│ 1. 请求登录 │ │
│◄─── 跳转授权页 ────────┤ │
│ 2. 用户授权 │ │
├─────────────────► │ │
│◄─── 授权码 ────────────┤ │
│ 3. 换 Token │ │
│ ├─────────────► │
│ │◄──── access_token ──────────┤
│ 4. 获取资源 │ │
│ ├─────────────────────────────►
│ │◄────────── 数据 ────────────┤用户 ───→ 第三方应用 ───→ 授权服务器 ───→ 资源服务器
│ │ │ │
│ 1. 请求登录 │ │
│◄─── 跳转授权页 ────────┤ │
│ 2. 用户授权 │ │
├─────────────────► │ │
│◄─── 授权码 ────────────┤ │
│ 3. 换 Token │ │
│ ├─────────────► │
│ │◄──── access_token ──────────┤
│ 4. 获取资源 │ │
│ ├─────────────────────────────►
│ │◄────────── 数据 ────────────┤GitHub OAuth 示例
js
// 1. 跳转授权
const clientId = 'your_client_id'
const redirectUri = 'https://example.com/auth/callback'
window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=user`
// 2. 回调中换 token
async function handleCallback(code) {
const res = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
})
const { access_token } = await res.json()
return access_token
}
// 3. 获取用户信息
const userRes = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}` },
})// 1. 跳转授权
const clientId = 'your_client_id'
const redirectUri = 'https://example.com/auth/callback'
window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=user`
// 2. 回调中换 token
async function handleCallback(code) {
const res = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ client_id, client_secret, code }),
})
const { access_token } = await res.json()
return access_token
}
// 3. 获取用户信息
const userRes = await fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${access_token}` },
})SSO (Single Sign-On)
原理
用户 ───→ 应用A ───→ CAS 服务器 ───→ 应用B
│ │ │ │
├────────► │ │
│ 未登录 │ │ │
│◄─────── 跳转登录页 ──┤ │
├──────────────────────► │
│◄─────── 票据(ticket)────────────┤
│ ├──────────────► │
│ │◄─── 验证通过 ──────────────┤
│ │ 已登录,无需重复登录 │
│ │ │ ├────────►
│ │ │ │ 携带票据
│ │ │◄─────────────┤
│ │ ├────── 验证 ──►
│ │ │◄─── 登录成功 ─┤
│ │ │ │用户 ───→ 应用A ───→ CAS 服务器 ───→ 应用B
│ │ │ │
├────────► │ │
│ 未登录 │ │ │
│◄─────── 跳转登录页 ──┤ │
├──────────────────────► │
│◄─────── 票据(ticket)────────────┤
│ ├──────────────► │
│ │◄─── 验证通过 ──────────────┤
│ │ 已登录,无需重复登录 │
│ │ │ ├────────►
│ │ │ │ 携带票据
│ │ │◄─────────────┤
│ │ ├────── 验证 ──►
│ │ │◄─── 登录成功 ─┤
│ │ │ │实现方式
| 方式 | 原理 | 适用 |
|---|---|---|
| 同域名 Cookie | 共享根域名 Cookie | 统一子域名 |
| CAS (Central Auth Service) | 统一认证中心 | 跨域多系统 |
| JWT + 共享密钥 | Token 在各系统间传递 | 前后端分离 |
| OAuth 2.0 | 授权码模式 | 第三方登录 |
常见安全问题
js
// 1. 密码明文传输
fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ password: '123456' }),
// ✅ 应该使用 HTTPS + 密码加密
})
// 2. Token 不过期
// ✅ access token 15分钟,refresh token 7天
// 3. 缺少速率限制
// ✅ 登录接口加验证码 + 速率限制
// 4. 用户ID 枚举
// GET /api/user/123 递增ID
// ✅ 使用 UUID,而不是自增ID// 1. 密码明文传输
fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ password: '123456' }),
// ✅ 应该使用 HTTPS + 密码加密
})
// 2. Token 不过期
// ✅ access token 15分钟,refresh token 7天
// 3. 缺少速率限制
// ✅ 登录接口加验证码 + 速率限制
// 4. 用户ID 枚举
// GET /api/user/123 递增ID
// ✅ 使用 UUID,而不是自增ID
Cao Note