跳到主要内容

网站登录

前端专业视角

单点登录(SSO)实现

单点登录(Single Sign-On, SSO)是一种身份验证机制,允许用户在多个相关应用中只登录一次,即可访问所有系统资源,而无需在每个应用中单独登录。这种方案能够显著提升用户体验,同时简化应用间的认证流程和身份管理。

SSO的核心优势在于:

  • 提升用户体验:用户无需记忆多组账号密码
  • 简化管理:集中管理用户身份和访问权限
  • 增强安全性:集中控制认证流程,便于实施额外的安全措施
  • 提高效率:减少用户登录时间和支持请求

1. 基于Cookie的SSO

基于Cookie的SSO是最简单的实现方式,适用于同一主域名下的多个子域名应用(如app1.example.com和app2.example.com)。

实现原理: 在用户登录主应用后,服务器在主域名下设置一个包含认证信息的加密Cookie,所有子域名应用都可以读取这个Cookie来获取用户认证状态。Cookie的作用域设置为主域名,确保所有子域名都能访问。

适用场景:

  • 同一组织内部的多个子域名应用
  • 对跨域认证需求不高的场景
  • 希望实现简单且低成本的SSO方案

实现步骤:

  1. 用户访问应用A(example.com/app1)并提供凭据
  2. 认证服务器验证凭据成功后,在主域名example.com下设置包含用户身份信息的加密Cookie
  3. 用户访问应用B(example.com/app2)时,浏览器会自动携带主域名下的Cookie
  4. 应用B服务器从Cookie中提取认证信息,并与认证服务器验证其有效性
  5. 验证通过后,用户无需再次登录即可访问应用B

安全考虑:

  • 使用HttpOnly标志防止XSS攻击窃取Cookie
  • 使用Secure标志确保Cookie仅通过HTTPS传输
  • 对Cookie内容进行加密,避免明文泄露用户信息
  • 设置合理的过期时间,平衡安全性和用户体验

代码示例:

// 登录成功后设置Cookie
function setAuthCookie(token) {
// 设置Cookie在主域名下,确保所有子域名可访问
// secure: 仅通过HTTPS传输
// HttpOnly: 防止JavaScript访问Cookie,抵御XSS攻击
// SameSite: 控制Cookie何时发送,防止CSRF攻击
document.cookie = `auth_token=${token}; domain=example.com; path=/; secure; HttpOnly; SameSite=Strict`;
}

// 验证Cookie
function verifyAuthCookie() {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [name, value] = cookie.split('=');
if (name === 'auth_token') {
// 验证token有效性
return validateToken(value);
}
}
return false;
}

// 验证token有效性
async function validateToken(token) {
try {
const response = await fetch('https://auth.example.com/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
});
const data = await response.json();
return data.valid;
} catch (error) {
console.error('Token validation failed:', error);
return false;
}
}

后端设置Cookie代码 (Node.js + Express):

const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');

// 登录路由
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 验证用户凭据 (伪代码)
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ message: '认证失败' });
}

// 生成JWT令牌
const token = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

// 设置Cookie
res.cookie('auth_token', token, {
domain: 'example.com', // 主域名
path: '/', // 所有路径
secure: true, // 仅HTTPS
httpOnly: true, // 防止XSS
sameSite: 'strict' // 防止CSRF
});

res.json({ success: true, message: '登录成功' });
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 验证令牌路由
router.post('/validate', (req, res) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({ valid: false, message: '令牌缺失' });
}

// 验证令牌
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({ valid: false, message: '令牌无效' });
}

res.json({ valid: true, user: { id: decoded.userId, username: decoded.username } });
});
} catch (error) {
console.error('Validation error:', error);
res.status(500).json({ message: '服务器错误' });
}
});

module.exports = router;

2. 基于Token的SSO

基于Token的SSO是一种更灵活的实现方式,适用于不同域名下的应用集成,也是目前前后端分离架构中最常用的认证方式。

实现原理: 用户登录后,认证服务器生成一个加密令牌(token)返回给前端,前端将token存储在本地(通常是localStorage、sessionStorage或IndexedDB),后续请求时携带这个token进行身份验证。认证服务器负责令牌的生成、验证和过期管理。

适用场景:

  • 跨不同域名的应用集成
  • 前后端分离架构
  • 移动应用与Web应用的统一认证
  • 第三方应用授权(如使用GitHub、Google账号登录)

实现步骤:

  1. 用户访问应用A并提供登录凭据
  2. 应用A将凭据发送给认证服务器验证
  3. 认证服务器验证成功后,生成token(通常是JWT)并返回给应用A
  4. 应用A将token存储在本地
  5. 用户访问应用B时,应用B从本地存储中获取token
  6. 应用B携带token向认证服务器验证其有效性
  7. 认证服务器验证token有效后,返回用户身份信息
  8. 应用B根据返回的用户信息,允许用户访问资源

安全考虑:

  • 选择合适的token存储方式(避免使用localStorage以防XSS攻击)
  • 使用HTTPS确保token传输安全
  • 设置合理的token过期时间
  • 实现token刷新机制,避免频繁重新登录
  • 对敏感操作实施二次验证

代码示例:

// 登录成功后存储token
function storeToken(token, refreshToken) {
// 对于敏感应用,考虑使用HttpOnly Cookie存储token
// 此处使用localStorage仅为示例
localStorage.setItem('auth_token', token);
localStorage.setItem('refresh_token', refreshToken);
}

// 获取token
function getToken() {
return localStorage.getItem('auth_token');
}

// 获取刷新令牌
function getRefreshToken() {
return localStorage.getItem('refresh_token');
}

// 清除token
function clearTokens() {
localStorage.removeItem('auth_token');
localStorage.removeItem('refresh_token');
}

// Axios请求拦截器 - 添加token到请求头
axios.interceptors.request.use(config => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// Axios响应拦截器 - 处理token过期和刷新
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// 处理401错误且不是刷新token的请求
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
// 获取新token
const refreshToken = getRefreshToken();
const response = await axios.post('https://auth.example.com/refresh', {
refreshToken
});

const { token: newToken, refreshToken: newRefreshToken } = response.data;

// 存储新token
storeToken(newToken, newRefreshToken);

// 更新原始请求的Authorization头
originalRequest.headers.Authorization = `Bearer ${newToken}`;

// 重试原始请求
return axios(originalRequest);
} catch (refreshError) {
// 刷新token失败,清除存储并跳转到登录页
clearTokens();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

// 登录函数
async function login(username, password) {
try {
const response = await axios.post('https://auth.example.com/login', {
username,
password
});
const { token, refreshToken } = response.data;
storeToken(token, refreshToken);
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}

后端认证服务器代码 (Node.js + Express):

const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 模拟数据库存储用户信息和刷新令牌
const users = [
{ id: 1, username: 'admin', password: 'hashed_password', role: 'admin' }
];
const refreshTokens = [];

// 登录路由
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
// 验证用户凭据 (伪代码)
const user = users.find(u => u.username === username && verifyPassword(password, u.password));
if (!user) {
return res.status(401).json({ message: '认证失败' });
}

// 生成访问令牌 (短过期时间)
const accessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' } // 15分钟过期
);

// 生成刷新令牌 (长过期时间)
const refreshToken = crypto.randomBytes(32).toString('hex');
// 存储刷新令牌
refreshTokens.push({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7天过期
});

res.json({
accessToken,
refreshToken,
user: { id: user.id, username: user.username, role: user.role }
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 刷新令牌路由
router.post('/refresh', (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ message: '刷新令牌缺失' });
}

// 查找并验证刷新令牌
const storedToken = refreshTokens.find(t => t.token === refreshToken);
if (!storedToken || storedToken.expiresAt < new Date()) {
// 移除过期的刷新令牌
if (storedToken && storedToken.expiresAt < new Date()) {
refreshTokens.splice(refreshTokens.indexOf(storedToken), 1);
}
return res.status(401).json({ message: '刷新令牌无效或已过期' });
}

// 查找用户
const user = users.find(u => u.id === storedToken.userId);
if (!user) {
return res.status(404).json({ message: '用户不存在' });
}

// 生成新的访问令牌
const newAccessToken = jwt.sign(
{ userId: user.id, username: user.username, role: user.role },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);

// 可选:生成新的刷新令牌
const newRefreshToken = crypto.randomBytes(32).toString('hex');
// 移除旧的刷新令牌并添加新的
refreshTokens.splice(refreshTokens.indexOf(storedToken), 1);
refreshTokens.push({
token: newRefreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});

res.json({
token: newAccessToken,
refreshToken: newRefreshToken
});
} catch (error) {
console.error('Refresh error:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 验证令牌中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (token == null) return res.status(401).json({ message: '令牌缺失' });

jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: '令牌无效' });
req.user = user;
next();
});
}

// 验证密码 (示例函数)
function verifyPassword(password, hashedPassword) {
// 实际应用中应使用bcrypt等库进行密码验证
return password === hashedPassword; // 仅为示例
}

module.exports = { router, authenticateToken };

3. OAuth2.0 + OpenID Connect

OAuth2.0是一个授权框架,允许第三方应用在不获取用户凭据的情况下访问用户资源;而OpenID Connect(OIDC)是基于OAuth2.0的身份认证协议,提供了标准化的身份验证机制。两者结合可以实现安全的第三方登录功能,是现代Web应用中最常用的身份认证解决方案之一。

核心概念:

  • 身份提供者(IdP):负责验证用户身份并颁发身份令牌的服务(如Google、GitHub、Microsoft等)
  • 依赖方(RP):请求身份验证的应用程序
  • 授权服务器:颁发访问令牌的服务器
  • 资源服务器:存储用户资源的服务器
  • 访问令牌(Access Token):授权第三方应用访问用户资源的令牌
  • 身份令牌(ID Token):包含用户身份信息的JWT令牌
  • 授权码(Authorization Code):用于换取访问令牌的临时代码

实现原理:

  • OAuth2.0负责授权过程,允许第三方应用代表用户访问受保护的资源
  • OpenID Connect在OAuth2.0基础上增加了身份认证层,通过ID Token提供用户身份信息
  • 最常用的流程是授权码流程(Authorization Code Flow),适用于服务端应用
  • 对于单页应用(SPA),通常使用授权码流程+PKCE(Proof Key for Code Exchange)增强安全性

实现步骤(授权码流程):

  1. 用户点击应用中的"使用第三方账号登录"按钮
  2. 应用生成一个随机的state参数(用于防止CSRF攻击)并存储
  3. 应用重定向用户到身份提供者的授权端点,携带client_idredirect_uriscopestate等参数
  4. 用户在身份提供者处登录并确认授权请求
  5. 身份提供者重定向用户回应用的redirect_uri,携带codestate参数
  6. 应用验证state参数与之前存储的一致
  7. 应用使用codeclient_idclient_secret向身份提供者的令牌端点换取访问令牌、身份令牌和刷新令牌
  8. 应用验证身份令牌的有效性
  9. 应用使用访问令牌访问用户资源,或根据身份令牌中的信息创建用户会话

安全考虑:

  • 始终使用HTTPS保护所有通信
  • 使用state参数防止CSRF攻击
  • 对于单页应用,使用PKCE流程增强安全性
  • 不要在前端存储client_secret
  • 验证身份令牌的签名和声明
  • 设置适当的令牌过期时间
  • 实现令牌撤销机制

代码示例(使用React和OAuth2.0 + PKCE):

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import crypto from 'crypto-js';

function LoginWithGoogle() {
const navigate = useNavigate();

const generateCodeVerifier = () => {
// 生成随机字符串作为code verifier
const array = new Uint32Array(32);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).slice(-2)).join('');
};

const generateCodeChallenge = (verifier) => {
// 对code verifier进行SHA-256哈希并base64编码
const hash = crypto.SHA256(verifier).toString(crypto.enc.Base64);
return hash
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};

const handleLogin = () => {
const clientId = 'your_google_client_id';
const redirectUri = 'http://localhost:3000/callback';
const scope = 'openid email profile';
const responseType = 'code';
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const state = Math.random().toString(36).substring(2, 15);

// 存储code verifier和state
localStorage.setItem('code_verifier', codeVerifier);
localStorage.setItem('oauth_state', state);

// 重定向到Google授权页面
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.append('client_id', clientId);
authUrl.searchParams.append('redirect_uri', redirectUri);
authUrl.searchParams.append('response_type', responseType);
authUrl.searchParams.append('scope', scope);
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('code_challenge', codeChallenge);
authUrl.searchParams.append('code_challenge_method', 'S256');

window.location.href = authUrl.toString();
};

return (
<button onClick={handleLogin} className="google-login-btn">
使用Google账号登录
</button>
);
}

// 回调页面
function GoogleCallback() {
const navigate = useNavigate();
const [error, setError] = useState(null);

useEffect(() => {
// 获取URL参数
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const errorParam = params.get('error');
const storedState = localStorage.getItem('oauth_state');
const codeVerifier = localStorage.getItem('code_verifier');

// 清除存储的state和code verifier
localStorage.removeItem('oauth_state');
localStorage.removeItem('code_verifier');

// 检查是否有错误
if (errorParam) {
setError(`登录错误: ${errorParam}`);
return;
}

// 验证state以防止CSRF攻击
if (state !== storedState) {
setError('无效的请求');
return;
}

// 换取令牌
if (code && codeVerifier) {
fetch('http://localhost:4000/api/google/callback', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ code, codeVerifier })
})
.then(res => res.json())
.then(data => {
if (data.token && data.user) {
localStorage.setItem('auth_token', data.token);
localStorage.setItem('user_info', JSON.stringify(data.user));
navigate('/');
} else {
setError('登录失败');
}
})
.catch(err => {
console.error('Login error:', err);
setError('服务器错误');
});
} else {
setError('缺少必要参数');
}
}, [navigate]);

if (error) {
return <div className="error-message">{error}</div>;
}

return <div>登录中...</div>;
}

export { LoginWithGoogle, GoogleCallback };

后端Node.js代码:

const express = require('express');
const router = express.Router();
const axios = require('axios');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// Google OAuth配置
const GOOGLE_CLIENT_ID = 'your_google_client_id';
const GOOGLE_CLIENT_SECRET = 'your_google_client_secret';
const GOOGLE_REDIRECT_URI = 'http://localhost:3000/callback';
const JWT_SECRET = 'your_jwt_secret';

// 创建JWKS客户端用于验证ID Token
const client = jwksClient({
jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'
});

function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}

// 处理Google回调
router.post('/google/callback', async (req, res) => {
try {
const { code, codeVerifier } = req.body;

// 换取访问令牌和ID Token
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
code,
redirect_uri: GOOGLE_REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier
}),
{
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
);

const { access_token, id_token } = tokenResponse.data;

// 验证ID Token
jwt.verify(id_token, getKey, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) {
console.error('ID Token verification failed:', err);
return res.status(401).json({ message: '身份验证失败' });
}

// 检查client_id是否匹配
if (decoded.aud !== GOOGLE_CLIENT_ID) {
return res.status(401).json({ message: '无效的客户端ID' });
}

// 检查令牌是否过期
if (decoded.exp < Date.now() / 1000) {
return res.status(401).json({ message: '令牌已过期' });
}

// 提取用户信息
const userInfo = {
id: decoded.sub,
email: decoded.email,
name: decoded.name,
picture: decoded.picture,
emailVerified: decoded.email_verified
};

// 生成应用自己的JWT令牌
const token = jwt.sign(
{ id: userInfo.id, email: userInfo.email, name: userInfo.name },
JWT_SECRET,
{ expiresIn: '24h' }
);

res.json({ token, user: userInfo });
});
} catch (error) {
console.error('Google callback error:', error.response ? error.response.data : error.message);
res.status(500).json({ message: '服务器错误' });
}
});

module.exports = router;

部署注意事项:

  • 在生产环境中,确保所有客户端密钥和敏感配置存储在环境变量中
  • 为身份提供者配置正确的重定向URI
  • 考虑使用成熟的OAuth/OIDC库,如Passport.js、oauth2-client-js等
  • 实现适当的错误处理和用户反馈机制
  • 定期轮换密钥和证书
  • 监控授权请求和令牌使用情况以检测异常活动
import { useAuth0 } from '@auth0/auth0-react';

function LoginButton() {
const { loginWithRedirect, isAuthenticated } = useAuth0();

if (!isAuthenticated) {
return <button onClick={() => loginWithRedirect()}>Login with Auth0</button>;
}

return <p>Already logged in!</p>;
}

function UserProfile() {
const { user, isAuthenticated } = useAuth0();

if (isAuthenticated) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<img src={user.picture} alt={user.name} />
</div>
);
}

return <p>Please log in to see your profile.</p>;
}

### 身份验证方式

#### 1. 密码认证
密码认证是最传统但仍广泛使用的身份验证方式,实现简单但存在安全风险。尽管有许多现代认证方式,但密码认证仍然是大多数应用的基础认证机制。

**安全存储建议**:
- 永远不要明文存储密码,即使在数据库备份中也不应该出现明文密码
- 使用bcrypt、Argon2或Scrypt等自适应哈希算法,这些算法故意设计得很慢,可以抵御暴力破解攻击
- 为每个用户使用唯一的盐值(salt),不要在所有用户中使用相同的盐值
- 实现密码复杂度要求,包括大小写字母、数字和特殊字符
- 定期检查密码是否出现在已知的数据泄露中(如使用HaveIBeenPwned API)
- 避免强制用户频繁更换密码,这可能导致用户选择更简单的密码或重复使用密码
- 禁止使用常见密码(如"password"、"123456"等)

**密码策略实现**:
```javascript
// 密码强度检查函数
function validatePassword(password) {
// 至少8个字符
if (password.length < 8) {
return { valid: false, message: '密码长度至少为8个字符' };
}

// 包含至少一个大写字母
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个大写字母' };
}

// 包含至少一个小写字母
if (!/[a-z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个小写字母' };
}

// 包含至少一个数字
if (!/[0-9]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个数字' };
}

// 包含至少一个特殊字符
if (!/[^A-Za-z0-9]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个特殊字符' };
}

// 检查常见密码
const commonPasswords = ['password', '123456', 'qwerty', 'admin', 'welcome'];
if (commonPasswords.includes(password.toLowerCase())) {
return { valid: false, message: '密码不能是常见的弱密码' };
}

return { valid: true };
}

前端实现代码(带密码强度检查):

<form id="loginForm">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" required>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" required>
<div id="passwordStrength" class="password-strength"></div>
</div>
<button type="submit">登录</button>
</form>

<script>
// 密码强度检查
function checkPasswordStrength(password) {
const strengthIndicator = document.getElementById('passwordStrength');
let strength = '弱';
let color = 'red';

if (password.length >= 12 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password) &&
/[^A-Za-z0-9]/.test(password)) {
strength = '强';
color = 'green';
} else if (password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password)) {
strength = '中';
color = 'orange';
}

strengthIndicator.textContent = `密码强度: ${strength}`;
strengthIndicator.style.color = color;
}

// 监听密码输入事件
document.getElementById('password').addEventListener('input', (e) => {
checkPasswordStrength(e.target.value);
});

// 表单提交处理
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;

// 前端验证
const validationResult = validatePassword(password);
if (!validationResult.valid) {
alert(validationResult.message);
return;
}

try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});

const data = await response.json();
if (response.ok) {
// 存储令牌
localStorage.setItem('auth_token', data.token);
alert('登录成功');
window.location.href = '/dashboard';
} else {
alert(data.message || '用户名或密码错误');
}
} catch (error) {
console.error('登录失败:', error);
alert('登录失败,请稍后重试');
}
});

// 密码强度验证函数
function validatePassword(password) {
// 至少8个字符
if (password.length < 8) {
return { valid: false, message: '密码长度至少为8个字符' };
}

// 包含至少一个大写字母
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个大写字母' };
}

// 包含至少一个小写字母
if (!/[a-z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个小写字母' };
}

// 包含至少一个数字
if (!/[0-9]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个数字' };
}

return { valid: true };
}
</script>

后端实现代码(Node.js + Express + MongoDB):

const bcrypt = require('bcrypt');
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { body, validationResult } = require('express-validator');
const axios = require('axios');

// 密码哈希轮数 - 数值越高越安全,但性能越低
const SALT_ROUNDS = 12;

// 用户注册
router.post('/register', [
// 输入验证
body('username').notEmpty().withMessage('用户名不能为空'),
body('password').isLength({ min: 8 }).withMessage('密码长度至少为8个字符')
.matches(/[A-Z]/).withMessage('密码必须包含至少一个大写字母')
.matches(/[a-z]/).withMessage('密码必须包含至少一个小写字母')
.matches(/[0-9]/).withMessage('密码必须包含至少一个数字')
.matches(/[^A-Za-z0-9]/).withMessage('密码必须包含至少一个特殊字符'),
body('email').isEmail().withMessage('请输入有效的电子邮件地址')
], async (req, res) => {
try {
// 检查验证错误
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { username, password, email } = req.body;

// 检查用户是否已存在
const existingUser = await User.findOne({ $or: [{ username }, { email }] });
if (existingUser) {
return res.status(400).json({ message: '用户名或电子邮件已被注册' });
}

// 检查密码是否在数据泄露中
try {
const pwnedResponse = await axios.get(`https://api.pwnedpasswords.com/range/${password.substring(0, 5)}`);
const pwnedHashes = pwnedResponse.data.split('\r\n');
const passwordHash = bcrypt.hashSync(password, 0).substring(5).toUpperCase();
const isPwned = pwnedHashes.some(hash => hash.startsWith(passwordHash));

if (isPwned) {
return res.status(400).json({ message: '此密码已在数据泄露中发现,请选择其他密码' });
}
} catch (error) {
// 如果HaveIBeenPwned API不可用,继续注册流程
console.warn('无法检查密码是否在数据泄露中:', error);
}

// 哈希密码
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

// 创建新用户
const newUser = new User({
username,
password: hashedPassword,
email,
createdAt: new Date()
});

await newUser.save();

// 生成JWT令牌
const token = jwt.sign(
{ userId: newUser._id, username: newUser.username },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

res.status(201).json({ token, user: { id: newUser._id, username: newUser.username, email: newUser.email } });
} catch (error) {
console.error('注册错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 用户登录
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;

// 查找用户
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ message: '认证失败' });
}

// 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: '认证失败' });
}

// 检查账户是否被锁定
if (user.accountLocked) {
return res.status(401).json({ message: '账户已锁定,请联系管理员' });
}

// 重置登录尝试次数
user.loginAttempts = 0;
await user.save();

// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

res.json({ token, user: { id: user._id, username: user.username, role: user.role } });
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 请求密码重置
router.post('/forgot-password', async (req, res) => {
try {
const { email } = req.body;

const user = await User.findOne({ email });
if (!user) {
// 不透露用户是否存在
return res.status(200).json({ message: '如果该电子邮件存在,我们将发送密码重置链接' });
}

// 生成密码重置令牌
const resetToken = crypto.randomBytes(32).toString('hex');
const resetTokenExpiry = Date.now() + 3600000; // 1小时后过期

user.resetToken = resetToken;
user.resetTokenExpiry = resetTokenExpiry;
await user.save();

// 发送密码重置电子邮件 (伪代码)
// sendPasswordResetEmail(user.email, resetToken);

res.status(200).json({ message: '如果该电子邮件存在,我们将发送密码重置链接' });
} catch (error) {
console.error('密码重置请求错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 重置密码
router.post('/reset-password', async (req, res) => {
try {
const { token, newPassword } = req.body;

// 验证密码强度
const validationResult = validatePassword(newPassword);
if (!validationResult.valid) {
return res.status(400).json({ message: validationResult.message });
}

const user = await User.findOne({
resetToken: token,
resetTokenExpiry: { $gt: Date.now() }
});

if (!user) {
return res.status(400).json({ message: '无效或过期的重置令牌' });
}

// 哈希新密码
const hashedPassword = await bcrypt.hash(newPassword, SALT_ROUNDS);

// 更新密码并清除重置令牌
user.password = hashedPassword;
user.resetToken = undefined;
user.resetTokenExpiry = undefined;
await user.save();

res.status(200).json({ message: '密码已成功重置' });
} catch (error) {
console.error('密码重置错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 验证密码函数
function validatePassword(password) {
// 至少8个字符
if (password.length < 8) {
return { valid: false, message: '密码长度至少为8个字符' };
}

// 包含至少一个大写字母
if (!/[A-Z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个大写字母' };
}

// 包含至少一个小写字母
if (!/[a-z]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个小写字母' };
}

// 包含至少一个数字
if (!/[0-9]/.test(password)) {
return { valid: false, message: '密码必须包含至少一个数字' };
}

return { valid: true };
}

module.exports = router;

用户模型示例(MongoDB Schema):

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
loginAttempts: {
type: Number,
default: 0
},
accountLocked: {
type: Boolean,
default: false
},
resetToken: String,
resetTokenExpiry: Date,
createdAt: {
type: Date,
default: Date.now
}
});

// 密码更新前钩子 - 确保密码被哈希
userSchema.pre('save', async function(next) {
// 只有在密码被修改时才哈希
if (!this.isModified('password')) {
return next();
}

try {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});

const User = mongoose.model('User', userSchema);

module.exports = User;

安全最佳实践:

  • 实现登录尝试限制,防止暴力破解
  • 使用HTTPS保护所有密码传输
  • 定期更新密码哈希算法和参数
  • 考虑实现密码轮换策略,但不要强制过于频繁的更换
  • 提供双因素认证作为额外的安全层
  • 对所有密码操作进行审计日志记录
  • 教育用户关于密码安全的最佳实践

2. 双因素认证(2FA)

双因素认证(2FA)是一种安全验证方法,要求用户提供两种不同类型的认证因素来证明身份。这两种因素通常属于以下类别之一:

  • 知识因素:用户知道的信息(如密码、PIN码)
  • 拥有因素:用户拥有的物品(如手机、硬件令牌)
  • 生物因素:用户的生理特征(如指纹、面部识别)

通过结合两种不同类型的因素,即使其中一种被泄露,攻击者仍然无法获得完整的认证信息,从而显著提高账户安全性。

常见实现方式比较:

实现方式优点缺点适用场景
短信验证码实现简单,用户无需额外应用易被拦截,依赖手机信号,存在SIM卡劫持风险低风险应用,临时验证
电子邮件验证码实现简单,用户无需额外应用邮箱可能被黑客访问,验证速度较慢低风险应用,辅助验证
认证应用(TOTP/HOTP)安全可靠,无需网络连接用户需要安装额外应用,初始设置略复杂中高风险应用,大多数网站和服务
硬件令牌极高安全性,物理隔离成本较高,丢失后恢复复杂高风险系统,企业级安全需求
推送通知认证用户体验好,操作简便依赖网络连接,需要额外开发移动应用,注重用户体验的服务

基于时间的一次性密码(TOTP)实现: TOTP是最常用的双因素认证标准,基于共享密钥和当前时间生成一次性密码。

实现步骤:

  1. 服务器为每个用户生成唯一的密钥
  2. 服务器将密钥以二维码形式展示给用户
  3. 用户使用认证应用(如Google Authenticator)扫描二维码,存储密钥
  4. 应用根据密钥和当前时间生成每30秒变化的一次性密码
  5. 用户在登录时输入此密码,服务器验证其有效性

完整代码示例(Node.js + Express + React):

后端实现:

const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const User = require('../models/User');
const { authenticate } = require('../middleware/auth');

// 生成密钥
function generateSecretKey() {
return speakeasy.generateSecret({
length: 20,
name: 'MyApp'
});
}

// 生成二维码数据URL
async function generateQrCodeDataUrl(secret, username) {
const otpauthUrl = speakeasy.otpauthURL({
secret: secret.base32,
label: `MyApp:${username}`,
issuer: 'MyApp',
algorithm: 'sha1',
digits: 6,
period: 30
});
return await QRCode.toDataURL(otpauthUrl);
}

// 验证TOTP代码
function verifyTotpCode(secret, code) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: code,
window: 1 // 允许前后各1个时间窗口(30秒)的偏差
});
}

// 设置2FA路由
router.get('/2fa/setup', authenticate, async (req, res) => {
try {
// 生成新密钥
const secret = generateSecretKey();

// 保存密钥到用户数据库
const user = await User.findById(req.user.id);
user.twoFactorSecret = secret.base32;
user.twoFactorEnabled = false;
await user.save();

// 生成二维码
const qrCodeDataUrl = await generateQrCodeDataUrl(secret, user.username);

res.json({
qrCodeDataUrl,
secret: secret.base32, // 提供备用密钥,以防用户无法扫描二维码
success: true
});
} catch (error) {
console.error('2FA设置错误:', error);
res.status(500).json({
success: false,
message: '服务器错误,请稍后重试'
});
}
});

// 验证2FA代码并启用路由
router.post('/2fa/verify', authenticate, async (req, res) => {
try {
const { code } = req.body;
const user = await User.findById(req.user.id);

if (!user.twoFactorSecret) {
return res.status(400).json({
success: false,
message: '请先设置双因素认证'
});
}

// 验证代码
const isValid = verifyTotpCode(user.twoFactorSecret, code);

if (isValid) {
user.twoFactorEnabled = true;
// 生成恢复码(用于丢失设备时)
user.twoFactorRecoveryCodes = generateRecoveryCodes();
await user.save();

res.json({
success: true,
message: '双因素认证已成功启用',
recoveryCodes: user.twoFactorRecoveryCodes
});
} else {
res.status(401).json({
success: false,
message: '验证码无效,请重试'
});
}
} catch (error) {
console.error('2FA验证错误:', error);
res.status(500).json({
success: false,
message: '服务器错误,请稍后重试'
});
}
});

// 登录时验证2FA代码
router.post('/login/verify-2fa', async (req, res) => {
try {
const { username, code } = req.body;
const user = await User.findOne({ username });

if (!user || !user.twoFactorEnabled) {
return res.status(401).json({
success: false,
message: '认证失败'
});
}

// 验证代码
const isValid = verifyTotpCode(user.twoFactorSecret, code);

if (isValid) {
// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

res.json({
success: true,
token,
user: { id: user._id, username: user.username, role: user.role }
});
} else {
res.status(401).json({
success: false,
message: '验证码无效,请重试'
});
}
} catch (error) {
console.error('2FA登录验证错误:', error);
res.status(500).json({
success: false,
message: '服务器错误,请稍后重试'
});
}
});

// 生成恢复码
function generateRecoveryCodes() {
const codes = [];
for (let i = 0; i < 10; i++) {
// 生成8位随机字母数字组合
const code = Math.random().toString(36).substring(2, 10).toUpperCase();
codes.push(code);
}
return codes;
}

// 使用恢复码禁用2FA
router.post('/2fa/recover', async (req, res) => {
try {
const { username, recoveryCode } = req.body;
const user = await User.findOne({ username });

if (!user || !user.twoFactorRecoveryCodes) {
return res.status(401).json({
success: false,
message: '认证失败'
});
}

// 查找并移除使用过的恢复码
const codeIndex = user.twoFactorRecoveryCodes.indexOf(recoveryCode);
if (codeIndex === -1) {
return res.status(401).json({
success: false,
message: '无效的恢复码'
});
}

// 移除使用过的恢复码
user.twoFactorRecoveryCodes.splice(codeIndex, 1);
user.twoFactorEnabled = false;
await user.save();

res.json({
success: true,
message: '双因素认证已禁用,建议您重新设置'
});
} catch (error) {
console.error('2FA恢复错误:', error);
res.status(500).json({
success: false,
message: '服务器错误,请稍后重试'
});
}
});

前端实现(React):

import React, { useState, useEffect } from 'react';
import axios from 'axios';

// 设置2FA组件
function SetupTwoFactorAuth() {
const [qrCode, setQrCode] = useState('');
const [secret, setSecret] = useState('');
const [code, setCode] = useState('');
const [status, setStatus] = useState('');
const [recoveryCodes, setRecoveryCodes] = useState([]);

useEffect(() => {
// 获取二维码
axios.get('/api/2fa/setup', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }
})
.then(response => {
setQrCode(response.data.qrCodeDataUrl);
setSecret(response.data.secret);
})
.catch(error => {
setStatus('获取二维码失败: ' + error.response.data.message);
});
}, []);

const handleVerify = () => {
axios.post('/api/2fa/verify', {
code
}, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }
})
.then(response => {
setStatus('双因素认证已成功启用!');
setRecoveryCodes(response.data.recoveryCodes);
})
.catch(error => {
setStatus('验证失败: ' + error.response.data.message);
});
};

return (
<div className="two-factor-setup">
<h2>设置双因素认证</h2>
{status && <div className="status">{status}</div>}

<div className="qr-code-container">
{qrCode && <img src={qrCode} alt="扫描二维码" />}
</div>

<div className="manual-secret">
<p>无法扫描二维码?手动输入密钥:</p>
<div className="secret-key">{secret}</div>
</div>

<div className="verification-code">
<label htmlFor="code">输入认证应用中的验证码:</label>
<input
type="text"
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="6位数字"
maxLength={6}
/>
<button onClick={handleVerify}>验证并启用</button>
</div>

{recoveryCodes.length > 0 && (
<div className="recovery-codes">
<h3>重要:保存恢复码</h3>
<p>这些代码可用于在丢失设备时禁用双因素认证。请妥善保管!</p>
<ul>
{recoveryCodes.map((code, index) => (
<li key={index}>{code}</li>
))}
</ul>
<button onClick={() => navigator.clipboard.writeText(recoveryCodes.join('\n'))}>
复制所有代码
</button>
</div>
)}
</div>
);
}

// 2FA登录验证组件
function VerifyTwoFactorAuth({ username, onSuccess }) {
const [code, setCode] = useState('');
const [status, setStatus] = useState('');

const handleVerify = () => {
axios.post('/api/login/verify-2fa', {
username,
code
})
.then(response => {
localStorage.setItem('auth_token', response.data.token);
onSuccess(response.data.user);
})
.catch(error => {
setStatus('验证码无效,请重试');
});
};

return (
<div className="two-factor-verify">
<h2>双因素认证</h2>
{status && <div className="status error">{status}</div>}

<div className="verification-code">
<label htmlFor="code">输入认证应用中的验证码:</label>
<input
type="text"
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="6位数字"
maxLength={6}
/>
<button onClick={handleVerify}>验证</button>
</div>

<div className="recovery-option">
<a href="/recover-2fa">无法访问认证应用?</a>
</div>
</div>
);
}

export { SetupTwoFactorAuth, VerifyTwoFactorAuth };

用户模型扩展:

// 添加到现有的userSchema
const userSchema = new mongoose.Schema({
// ... 现有字段 ...
twoFactorSecret: String,
twoFactorEnabled: {
type: Boolean,
default: false
},
twoFactorRecoveryCodes: [String]
// ... 现有字段 ...
});

推送通知认证实现: 推送通知认证是一种更现代化的2FA实现方式,用户只需点击手机上的推送通知即可完成认证。

// 后端实现(使用Firebase Cloud Messaging)
const admin = require('firebase-admin');

aadmin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});

// 发送认证推送通知
router.post('/2fa/push', authenticate, async (req, res) => {
try {
const user = await User.findById(req.user.id);
if (!user.fcmToken) {
return res.status(400).json({
success: false,
message: '未设置推送通知设备'
});
}

// 生成随机挑战码
const challenge = Math.random().toString(36).substring(2, 10);
user.authChallenge = challenge;
await user.save();

// 发送推送通知
const message = {
token: user.fcmToken,
notification: {
title: '登录验证请求',
body: '有人尝试登录您的账户,点击确认或拒绝'
},
data: {
type: 'auth_request',
challenge: challenge
}
};

await admin.messaging().send(message);

res.json({
success: true,
message: '推送通知已发送,请在手机上确认'
});
} catch (error) {
console.error('推送通知错误:', error);
res.status(500).json({
success: false,
message: '发送推送通知失败'
});
}
});

// 验证推送通知响应
router.post('/2fa/push/verify', async (req, res) => {
try {
const { username, challenge, approved } = req.body;

if (!approved) {
return res.status(401).json({
success: false,
message: '认证被拒绝'
});
}

const user = await User.findOne({ username });
if (!user || user.authChallenge !== challenge) {
return res.status(401).json({
success: false,
message: '无效的认证请求'
});
}

// 清除挑战码
user.authChallenge = null;
await user.save();

// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

res.json({
success: true,
token,
user: { id: user._id, username: user.username, role: user.role }
});
} catch (error) {
console.error('推送验证错误:', error);
res.status(500).json({
success: false,
message: '服务器错误,请稍后重试'
});
}
});

双因素认证最佳实践:

  • 始终提供恢复码,以防用户丢失认证设备
  • 实现代码验证时允许一定的时间窗口偏差(通常为±1个窗口)
  • 对于高风险操作,强制要求双因素认证
  • 教育用户如何正确设置和使用双因素认证
  • 考虑实现多设备支持,允许用户在多个设备上使用2FA
  • 定期轮换密钥,提高安全性
  • 记录所有2FA验证尝试,包括成功和失败的尝试
  • 提供多种2FA方式,让用户选择最适合自己的方式

安全考虑:

  • 存储密钥时使用加密,而不是明文存储
  • 实现暴力破解防护,限制验证尝试次数
  • 对于短信验证码,考虑使用HSM(硬件安全模块)发送
  • 提醒用户警惕钓鱼攻击,不要向任何人透露验证码
  • 定期更新2FA相关库和依赖,修复已知漏洞

3. 生物识别认证

生物识别认证利用用户的生理特征进行身份验证,提供更高的安全性和便利性。

常见类型:

  • 指纹识别
  • 面部识别
  • 虹膜识别
  • 声纹识别

WebAPI实现代码:

// 检查浏览器是否支持生物识别API
function checkBiometricSupport() {
return !!(navigator.credentials && navigator.credentials.create);
}

// 注册生物识别凭证
async function registerBiometric(username) {
if (!checkBiometricSupport()) {
alert('您的浏览器不支持生物识别认证');
return;
}

try {
// 创建一个挑战
const challenge = crypto.randomUUID();
// 存储挑战到服务器
await fetch('/api/biometric/register/start', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, challenge })
});

// 创建生物识别凭证
const credential = await navigator.credentials.create({
publicKey: {
challenge: new TextEncoder().encode(challenge),
rp: { name: 'MyApp' },
user: {
id: new TextEncoder().encode(username),
name: username,
displayName: username
},
pubKeyCredParams: [{
type: 'public-key',
alg: -7 // ES256
}],
timeout: 60000
}
});

// 发送凭证到服务器完成注册
await fetch('/api/biometric/register/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, credential })
});

alert('生物识别注册成功');
} catch (error) {
console.error('生物识别注册失败:', error);
alert('生物识别注册失败');
}
}

// 使用生物识别登录
async function loginWithBiometric() {
if (!checkBiometricSupport()) {
alert('您的浏览器不支持生物识别认证');
return;
}

try {
// 获取挑战
const response = await fetch('/api/biometric/login/start');
const { challenge } = await response.json();

// 请求生物识别验证
const credential = await navigator.credentials.get({
publicKey: {
challenge: new TextEncoder().encode(challenge),
allowCredentials: [{
type: 'public-key',
id: new TextEncoder().encode('registered-user-id'),
transports: ['usb', 'nfc', 'ble', 'internal']
}],
timeout: 60000
}
});

// 验证凭证
const verifyResponse = await fetch('/api/biometric/login/verify', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ credential })
});

if (verifyResponse.ok) {
alert('登录成功');
window.location.href = '/dashboard';
} else {
alert('生物识别验证失败');
}
} catch (error) {
console.error('生物识别登录失败:', error);
alert('生物识别登录失败');
}
}

4. 无密码认证

无密码认证完全替代传统密码,使用一次性验证码、链接或生物识别等方式进行登录。

实现方式:

  • 电子邮件一次性登录链接
  • 短信验证码
  • 推送通知认证
  • 生物识别认证

电子邮件一次性链接实现代码:

// 后端生成一次性登录链接
router.post('/login/email', async (req, res) => {
try {
const { email } = req.body;
const user = await User.findOne({ email });

if (!user) {
// 即使用户不存在,也返回成功消息以防止邮箱枚举
return res.json({ message: '登录链接已发送' });
}

// 生成一次性token
const token = crypto.randomBytes(32).toString('hex');
user.loginToken = token;
user.loginTokenExpires = Date.now() + 3600000; // 1小时后过期
await user.save();

// 发送邮件
const loginLink = `https://yourapp.com/login/verify?token=${token}`;
await sendEmail(email, '登录链接', `请点击以下链接登录: ${loginLink}`);

res.json({ message: '登录链接已发送' });
} catch (error) {
console.error('发送登录链接失败:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 验证一次性登录链接
router.get('/login/verify', async (req, res) => {
try {
const { token } = req.query;
const user = await User.findOne({
loginToken: token,
loginTokenExpires: { $gt: Date.now() }
});

if (!user) {
return res.status(401).json({ message: '无效或过期的登录链接' });
}

// 生成会话或token
const sessionToken = generateToken(user._id);

// 清除一次性登录token
user.loginToken = undefined;
user.loginTokenExpires = undefined;
await user.save();

// 设置cookie并重定向
res.cookie('sessionToken', sessionToken, { httpOnly: true, secure: true });
res.redirect('/dashboard');
} catch (error) {
console.error('验证登录链接失败:', error);
res.status(500).json({ message: '服务器错误' });
}
});

人机验证

人机验证用于区分真实用户和自动化程序,是防止恶意攻击的重要手段。

1. 图形验证码

图形验证码通过展示扭曲的文字、数字或图片,要求用户识别并输入,虽然传统但仍广泛使用。

实现代码(使用svg-captcha库):

const svgCaptcha = require('svg-captcha');
const express = require('express');
const router = express.Router();

// 生成图形验证码
router.get('/captcha', (req, res) => {
const captcha = svgCaptcha.create({
size: 4, // 验证码长度
ignoreChars: '0o1il', // 忽略易混淆字符
noise: 3, // 干扰线数量
color: true, // 彩色验证码
background: '#f1f5f9' // 背景色
});

// 存储验证码文本到session
req.session.captcha = captcha.text;

// 设置响应类型为SVG
res.type('svg');
res.send(captcha.data);
});

// 验证验证码
router.post('/verify-captcha', (req, res) => {
const { captcha } = req.body;
if (captcha.toLowerCase() === req.session.captcha.toLowerCase()) {
// 验证成功
res.json({ valid: true });
// 清除session中的验证码
req.session.captcha = null;
} else {
// 验证失败
res.json({ valid: false });
}
});

前端代码:

<div class="captcha-container">
<img id="captchaImage" src="/api/captcha" alt="验证码">
<button onclick="refreshCaptcha()">刷新</button>
</div>
<input type="text" id="captchaInput" placeholder="请输入验证码">
<button onclick="verifyCaptcha()">验证</button>

<script>
function refreshCaptcha() {
// 添加随机参数防止缓存
document.getElementById('captchaImage').src = '/api/captcha?' + Math.random();
}

async function verifyCaptcha() {
const captcha = document.getElementById('captchaInput').value;
if (!captcha) {
alert('请输入验证码');
return;
}

try {
const response = await fetch('/api/verify-captcha', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ captcha })
});
const data = await response.json();

if (data.valid) {
alert('验证码验证成功');
} else {
alert('验证码错误');
refreshCaptcha();
}
} catch (error) {
console.error('验证码验证失败:', error);
alert('验证失败,请稍后重试');
}
}
</script>

2. 行为验证码

行为验证码分析用户的鼠标移动、点击等行为特征,判断是否为真人操作。

实现思路:

  1. 展示一个简单任务(如拖拽滑块、按顺序点击图标)
  2. 收集用户完成任务过程中的行为数据
  3. 分析数据特征,判断是否为自动化程序

代码示例(滑块验证码):

<div class="slider-captcha">
<div class="slider-container">
<div class="puzzle"></div>
<div class="slider-track">
<div class="slider-button"></div>
</div>
</div>
<p id="captchaStatus">请拖动滑块完成验证</p>
</div>

<script>
const puzzle = document.querySelector('.puzzle');
const sliderButton = document.querySelector('.slider-button');
const sliderTrack = document.querySelector('.slider-track');
const captchaStatus = document.getElementById('captchaStatus');
let isDragging = false;
let startX, moveX, currentX = 0;
const targetPosition = 200; // 目标位置

// 初始化
function initCaptcha() {
// 随机生成拼图位置
const puzzlePosition = Math.floor(Math.random() * 150) + 50;
puzzle.style.left = `${puzzlePosition}px`;
currentX = 0;
sliderButton.style.transform = `translateX(0px)`;
captchaStatus.textContent = '请拖动滑块完成验证';
captchaStatus.style.color = '#333';
}

// 开始拖动
sliderButton.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
sliderButton.classList.add('active');
});

// 拖动中
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;

moveX = e.clientX - startX;
currentX = Math.max(0, Math.min(moveX, sliderTrack.offsetWidth - sliderButton.offsetWidth));
sliderButton.style.transform = `translateX(${currentX}px)`;
puzzle.style.transform = `translateX(${currentX}px)`;
});

// 结束拖动
document.addEventListener('mouseup', () => {
if (!isDragging) return;

isDragging = false;
sliderButton.classList.remove('active');

// 验证是否成功
if (Math.abs(currentX - targetPosition) < 10) {
captchaStatus.textContent = '验证成功';
captchaStatus.style.color = 'green';
sliderButton.disabled = true;
// 发送行为数据到服务器进行进一步验证
sendBehaviorData();
} else {
captchaStatus.textContent = '验证失败,请重试';
captchaStatus.style.color = 'red';
// 重置
setTimeout(() => {
sliderButton.style.transform = `translateX(0px)`;
puzzle.style.transform = `translateX(0px)`;
initCaptcha();
}, 1000);
}
});

// 发送行为数据
function sendBehaviorData() {
// 收集行为数据,如拖动时间、路径等
const behaviorData = {
// 这里仅为示例,实际应收集更多数据
dragTime: Date.now() - startTime,
path: dragPath,
errorMargin: Math.abs(currentX - targetPosition)
};

fetch('/api/verify-behavior', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(behaviorData)
});
}

// 初始化验证码
initCaptcha();
</script>

3. reCAPTCHA

reCAPTCHA是Google提供的高级验证码服务,能够有效区分人类和机器人。

实现步骤:

  1. 注册Google reCAPTCHA获取站点密钥
  2. 在页面中加载reCAPTCHA脚本
  3. 添加reCAPTCHA容器
  4. 验证用户响应

代码示例:

<!-- 加载reCAPTCHA脚本 -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>

<!-- 添加reCAPTCHA容器 -->
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>

<!-- 表单提交时验证 -->
<form id="myForm" onsubmit="return validateForm()">
<!-- 其他表单字段 -->
<div class="g-recaptcha" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">提交</button>
</form>

<script>
function validateForm() {
const response = grecaptcha.getResponse();
if (response.length === 0) {
alert('请完成验证码验证');
return false;
}
return true;
}
</script>

后端验证代码(Node.js):

const axios = require('axios');

async function verifyRecaptcha(token) {
try {
const response = await axios.post(
'https://www.google.com/recaptcha/api/siteverify',
`secret=YOUR_SECRET_KEY&response=${token}`,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return response.data.success;
} catch (error) {
console.error('reCAPTCHA验证失败:', error);
return false;
}
}

// 路由中使用
router.post('/submit-form', async (req, res) => {
const { recaptchaToken } = req.body;
const isHuman = await verifyRecaptcha(recaptchaToken);

if (!isHuman) {
return res.status(400).json({ message: '验证码验证失败' });
}

// 处理表单提交
// ...
});

4. 隐藏字段验证

隐藏字段验证是一种对用户透明的验证方式,主要用于检测自动化爬虫。

实现原理:

  1. 在表单中添加一个对用户隐藏的字段
  2. 真实用户不会看到并填写该字段
  3. 自动化爬虫通常会填写所有字段
  4. 服务器检查该字段是否被填写,若填写则判定为爬虫

代码示例:

<form id="contactForm" action="/submit-form" method="post">
<!-- 可见字段 -->
<div>
<label for="name">姓名:</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="message">留言:</label>
<textarea id="message" name="message" required></textarea>
</div>

<!-- 隐藏字段 -->
<div style="display: none;">
<label for="bot-check">请不要填写此字段:</label>
<input type="text" id="bot-check" name="bot-check">
</div>

<button type="submit">提交</button>
</form>

后端验证代码:

router.post('/submit-form', (req, res) => {
// 检查隐藏字段
if (req.body['bot-check']) {
// 字段被填写,可能是爬虫
console.log('检测到可能的爬虫提交');
return res.status(403).json({ message: '提交失败' });
}

// 处理正常表单提交
// ...
});

通俗易懂的后端视角

认证流程简化

1. 登录请求处理

登录请求处理是身份认证的第一步,负责验证用户提供的凭据并创建会话。

详细流程:

  1. 接收客户端发送的用户名和密码
  2. 从数据库中查询用户信息
  3. 验证密码是否匹配
  4. 生成认证凭证(Session或Token)
  5. 将凭证返回给客户端

代码示例(Node.js + Express + JWT):

const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();

router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;

// 1. 查询用户
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ message: '认证失败' });
}

// 2. 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ message: '认证失败' });
}

// 3. 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

// 4. 生成刷新令牌
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);

// 5. 存储刷新令牌
user.refreshToken = refreshToken;
await user.save();

// 6. 返回令牌
res.json({
token,
refreshToken,
user: {
id: user._id,
username: user.username,
role: user.role
}
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

module.exports = router;

2. 会话管理

会话管理负责维护用户的登录状态,确保用户在访问多个页面时无需重复登录。

Session vs Token:

  • Session: 服务器端存储,需要Cookie支持,适合同一域名下的应用
  • Token: 客户端存储,无状态,适合前后端分离和跨域场景

Token刷新机制实现:

// 刷新令牌路由
router.post('/refresh-token', async (req, res) => {
try {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ message: '刷新令牌缺失' });
}

// 验证刷新令牌
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);

// 查找用户
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({ message: '无效的刷新令牌' });
}

// 生成新的访问令牌
const token = jwt.sign(
{ userId: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);

// 生成新的刷新令牌
const newRefreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);

// 更新存储的刷新令牌
user.refreshToken = newRefreshToken;
await user.save();

res.json({ token, refreshToken: newRefreshToken });
} catch (error) {
console.error('刷新令牌错误:', error);
res.status(401).json({ message: '无效的刷新令牌' });
}
});

中间件验证Token:

// 认证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
return res.status(401).json({ message: '访问令牌缺失' });
}

jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: '无效的访问令牌' });
}

req.user = user;
next();
});
}

// 保护路由
router.get('/protected', authenticateToken, (req, res) => {
res.json({ message: '这是受保护的资源', user: req.user });
});

3. 权限控制

权限控制确保用户只能访问其有权限的资源,常见的实现方式是基于角色的访问控制(RBAC)。

RBAC实现步骤:

  1. 定义不同角色(如管理员、普通用户、访客)
  2. 为角色分配权限
  3. 将角色分配给用户
  4. 在访问资源时检查用户角色和权限

代码示例:

// 角色定义
const roles = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
};

// 权限定义
const permissions = {
[roles.ADMIN]: ['read', 'write', 'delete', 'admin'],
[roles.USER]: ['read', 'write'],
[roles.GUEST]: ['read']
};

// 权限检查中间件
function checkPermission(requiredPermission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ message: '未认证' });
}

const userRole = req.user.role;
const userPermissions = permissions[userRole] || [];

if (!userPermissions.includes(requiredPermission)) {
return res.status(403).json({ message: '权限不足' });
}

next();
};
}

// 使用权限中间件
router.get('/admin/users', authenticateToken, checkPermission('admin'), (req, res) => {
// 处理管理员才能访问的资源
});

router.post('/posts', authenticateToken, checkPermission('write'), (req, res) => {
// 处理需要写权限的操作
});

4. 安全防护

安全防护是登录系统的重要组成部分,用于防止各种恶意攻击。

防止暴力破解:

// 登录失败尝试限制
const loginAttempts = new Map();

function preventBruteForce(req, res, next) {
const ip = req.ip;
const attempts = loginAttempts.get(ip) || 0;

if (attempts >= 5) {
return res.status(429).json({ message: '登录尝试次数过多,请稍后再试' });
}

next();
}

// 登录路由使用中间件
router.post('/login', preventBruteForce, async (req, res) => {
try {
// 登录逻辑
// ...

// 登录成功,重置尝试次数
loginAttempts.delete(req.ip);
// ...
} catch (error) {
// 登录失败,增加尝试次数
const ip = req.ip;
const attempts = (loginAttempts.get(ip) || 0) + 1;
loginAttempts.set(ip, attempts);

// 设置过期时间
if (attempts === 1) {
setTimeout(() => {
loginAttempts.delete(ip);
}, 300000); // 5分钟后重置
}

res.status(401).json({ message: '认证失败' });
}
});

防止CSRF攻击:

const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// 应用CSRF中间件
app.use(csrfProtection);

// 提供CSRF令牌给前端
router.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});

// 表单提交路由需要验证CSRF令牌
router.post('/submit', csrfProtection, (req, res) => {
// 处理表单提交
});

实现建议

1. 使用成熟框架

使用成熟的认证框架可以显著减少开发时间并提高安全性。

推荐框架:

  • Node.js: Passport.js、NextAuth.js
  • Python: Django Auth、Flask-Login
  • Java: Spring Security
  • PHP: Laravel Auth
  • .NET: ASP.NET Identity

Passport.js示例:

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');
const bcrypt = require('bcrypt');

// 配置本地策略
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (!user) {
return done(null, false, { message: '用户名不存在' });
}

const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return done(null, false, { message: '密码错误' });
}

return done(null, user);
} catch (error) {
return done(error);
}
}
));

// 序列化和反序列化用户
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error);
}
});

// 应用Passport中间件
app.use(passport.initialize());
app.use(passport.session());

// 登录路由
router.post('/login',
passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }),
(req, res) => {
res.redirect('/dashboard');
}
);

2. 密码加密存储

永远不要明文存储密码,使用强哈希算法加密后再存储。

bcrypt示例:

const bcrypt = require('bcrypt');
const saltRounds = 12; // 工作因子,值越高越安全但越慢

// 注册用户时加密密码
async function registerUser(username, password) {
const hashedPassword = await bcrypt.hash(password, saltRounds);
const user = new User({
username,
password: hashedPassword
});
await user.save();
return user;
}

3. 会话有效期管理

设置合理的会话过期时间,并提供令牌刷新机制。

最佳实践:

  • 访问令牌有效期设置为较短时间(如15分钟到1小时)
  • 刷新令牌有效期设置为较长时间(如7天)
  • 实现令牌撤销机制
  • 支持多设备登录管理

4. 日志记录与监控

记录登录行为并监控异常活动,及时发现潜在的安全风险。

日志记录示例:

const winston = require('winston');

// 配置日志记录器
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});

// 记录登录事件
function logLoginEvent(username, ip, success) {
logger.info('Login attempt', {
username,
ip,
success,
timestamp: new Date()
});
}

// 在登录路由中使用
router.post('/login', async (req, res) => {
try {
// 登录逻辑
// ...

if (success) {
logLoginEvent(username, req.ip, true);
// 返回成功响应
} else {
logLoginEvent(username, req.ip, false);
// 返回失败响应
}
} catch (error) {
logLoginEvent(username, req.ip, false);
// 处理错误
}
});

最佳实践

1. 前后端分离架构

在前后端分离架构中,使用JWT等无状态认证机制可以简化会话管理。

实现要点:

  • 前端存储JWT令牌(通常在localStorage或sessionStorage)
  • 每次请求将令牌添加到请求头
  • 服务器验证令牌有效性
  • 实现令牌刷新机制
  • 登出时清除客户端令牌

代码示例(前端Axios拦截器):

import axios from 'axios';

const api = axios.create({
baseURL: 'https://api.yourapp.com'
});

// 请求拦截器添加令牌
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// 响应拦截器处理令牌过期
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// 如果是401错误且不是重复尝试
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
// 尝试刷新令牌
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('https://api.yourapp.com/auth/refresh-token', {
refreshToken
});

// 保存新令牌
localStorage.setItem('token', response.data.token);
localStorage.setItem('refreshToken', response.data.refreshToken);

// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${response.data.token}`;
return axios(originalRequest);
} catch (refreshError) {
// 刷新令牌失败,跳转到登录页
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

export default api;

2. 多设备登录管理

多设备登录管理允许用户查看和管理当前登录的所有设备,并支持远程注销。

实现步骤:

  1. 登录时生成设备标识
  2. 记录设备信息和登录时间
  3. 提供API查询当前登录设备
  4. 支持选择性注销设备

代码示例:

// 登录时记录设备信息
router.post('/login', async (req, res) => {
// ... 前面的登录逻辑

// 获取设备信息
const deviceInfo = {
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date(),
deviceId: crypto.randomUUID()
};

// 存储设备信息
user.devices.push(deviceInfo);
await user.save();

// 返回令牌和设备信息
res.json({
token,
refreshToken,
deviceId: deviceInfo.deviceId,
user: {
id: user._id,
username: user.username,
role: user.role
}
});
});

// 获取当前登录设备
router.get('/devices', authenticateToken, async (req, res) => {
try {
const user = await User.findById(req.user.userId);
res.json({
devices: user.devices.map(device => ({
id: device.deviceId,
ip: device.ip,
userAgent: device.userAgent,
loginTime: device.timestamp
}))
});
} catch (error) {
console.error('获取设备信息错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

// 注销特定设备
router.post('/devices/logout', authenticateToken, async (req, res) => {
try {
const { deviceId } = req.body;
const user = await User.findById(req.user.userId);

// 移除指定设备
user.devices = user.devices.filter(device => device.deviceId !== deviceId);
await user.save();

res.json({ message: '设备已注销' });
} catch (error) {
console.error('注销设备错误:', error);
res.status(500).json({ message: '服务器错误' });
}
});

3. 渐进式认证

渐进式认证根据操作的敏感程度动态调整认证强度,在安全性和用户体验之间取得平衡。

实现场景:

  • 普通浏览: 无需认证
  • 查看个人资料: 轻量级认证
  • 进行支付: 强认证(如双因素认证)

代码示例:

// 定义不同级别的认证要求
const authLevels = {
NONE: 'none',
BASIC: 'basic',
STRONG: 'strong'
};

// 渐进式认证中间件
function progressiveAuth(requiredLevel) {
return (req, res, next) => {
// 公开资源不需要认证
if (requiredLevel === authLevels.NONE) {
return next();
}

// 检查用户是否已登录
if (!req.user) {
return res.status(401).json({ message: '请先登录' });
}

// 基础认证要求已满足
if (requiredLevel === authLevels.BASIC) {
return next();
}

// 强认证要求
if (requiredLevel === authLevels.STRONG) {
// 检查是否已完成强认证
if (req.session.strongAuthVerified) {
return next();
}

// 未完成强认证,重定向到验证页面
return res.status(403).json({
message: '需要额外验证',
redirectTo: '/verify-2fa'
});
}

next();
};
}

// 应用不同级别的认证
router.get('/public', progressiveAuth(authLevels.NONE), (req, res) => {
// 公开资源
});

router.get('/profile', progressiveAuth(authLevels.BASIC), (req, res) => {
// 个人资料
});

router.post('/payment', progressiveAuth(authLevels.STRONG), (req, res) => {
// 支付操作
});

// 2FA验证路由
router.post('/verify-2fa', authenticateToken, async (req, res) => {
const { code } = req.body;
const user = await User.findById(req.user.userId);

// 验证2FA代码
const isVerified = verifyTotpCode(user.twoFactorSecret, code);

if (isVerified) {
// 标记强认证已通过,有效期1小时
req.session.strongAuthVerified = true;
req.session.strongAuthExpires = Date.now() + 3600000;
res.json({ success: true });
} else {
res.status(401).json({ success: false, message: '验证码无效' });
}
});

4. 用户体验优化

良好的登录体验可以提高用户满意度和转化率。

优化建议:

  • 记住登录状态: 提供"记住我"选项
  • 自动填充: 利用浏览器自动填充功能
  • 密码强度提示: 实时反馈密码强度
  • 错误提示: 清晰的错误信息
  • 表单验证: 客户端实时验证
  • 社交登录: 提供第三方登录选项

"记住我"功能实现:

// 登录路由处理"记住我"选项
router.post('/login', async (req, res) => {
// ... 登录逻辑

if (req.body.rememberMe) {
// 设置长效Cookie
res.cookie('rememberMe', token, {
httpOnly: true,
secure: true,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30天
});
}

res.json({ token, user: { id: user._id, username: user.username } });
});

// 中间件检查"记住我"Cookie
function checkRememberMe(req, res, next) {
if (!req.user && req.cookies.rememberMe) {
try {
// 验证Cookie中的令牌
const decoded = jwt.verify(req.cookies.rememberMe, process.env.JWT_SECRET);
// 设置用户信息
req.user = decoded;
} catch (error) {
// 无效令牌,清除Cookie
res.clearCookie('rememberMe');
}
}
next();
}

// 应用中间件
app.use(checkRememberMe);

密码强度检查:

<input type="password" id="password" name="password" oninput="checkPasswordStrength(this.value)">
<div id="passwordStrength"></div>

<script>
function checkPasswordStrength(password) {
let strength = '弱';
let color = 'red';

// 密码强度规则
if (password.length >= 8) {
strength = '中';
color = 'orange';
}

if (password.length >= 12 && /[A-Z]/.test(password) && /[0-9]/.test(password) && /[^A-Za-z0-9]/.test(password)) {
strength = '强';
color = 'green';
}

const strengthElement = document.getElementById('passwordStrength');
strengthElement.textContent = `密码强度: ${strength}`;
strengthElement.style.color = color;
}
</script>

总结

网站登录系统是用户体验和安全的重要组成部分。本文从前端和后端视角详细介绍了单点登录、身份验证方式、人机验证等核心技术,并提供了丰富的代码示例和最佳实践。在实际开发中,应根据项目需求和用户场景选择合适的认证方案,同时平衡安全性和用户体验。