Flutter + Node.js:安全认证体系设计与最佳实践
认证和安全是移动应用后端服务中最关键的环节之一。一个设计不当的认证体系可能导致用户数据泄露、接口被恶意调用、甚至整个系统被攻破。这篇文章记录了我在 Flutter + Node.js 项目中构建生产级安全认证体系的完整思路和实现细节。
安全不是一个功能模块,而是一种贯穿整个系统设计的思维方式。每一次 API 调用、每一次数据传输、每一条存储的记录,都应该经过安全评估。
一、JWT 双 Token 机制
单一 Token 的方案有一个明显的问题:Token 过期后用户必须重新登录,体验很差。但如果延长 Token 有效期,一旦被截获风险又太大。解决方案是采用双 Token 机制:Access Token(短期有效)+ Refresh Token(长期有效)。
// 生成 Access Token(有效期 30 分钟)
const generateAccessToken = (userId) => {
return jwt.sign(
{ id: userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '30m' }
);
};
// 生成 Refresh Token(有效期 7 天)
const generateRefreshToken = (userId) => {
return jwt.sign(
{ id: userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET, // 使用独立的密钥
{ expiresIn: '7d' }
);
};
登录成功后同时返回两个 Token:
const login = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.matchPassword(password))) {
return errorResponse(res, '邮箱或密码错误', 401);
}
const accessToken = generateAccessToken(user._id);
const refreshToken = generateRefreshToken(user._id);
// Refresh Token 存储到数据库(可随时撤销)
await Token.create({
userId: user._id,
token: refreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
successResponse(res, {
accessToken,
refreshToken,
user: { id: user._id, username: user.username, email: user.email }
});
};
二、Refresh Token 轮换
每次使用 Refresh Token 换取新的 Access Token 时,旧的 Refresh Token 立即失效,同时生成一个新的 Refresh Token。这种「一次性使用」策略可以有效防止 Token 重放攻击:
const refreshToken = async (req, res) => {
const { refreshToken: token } = req.body;
if (!token) {
return errorResponse(res, '缺少 Refresh Token', 400);
}
// 验证 Token 签名
const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
// 确认是 refresh 类型
if (decoded.type !== 'refresh') {
return errorResponse(res, 'Token 类型错误', 401);
}
// 在数据库中查找并删除旧 Token
const storedToken = await Token.findOneAndDelete({
userId: decoded.id,
token,
expiresAt: { $gt: new Date() }
});
if (!storedToken) {
// Token 可能已被使用过或已过期 → 可能存在盗用
await Token.deleteMany({ userId: decoded.id });
return errorResponse(res, 'Refresh Token 已失效,请重新登录', 401);
}
// 生成新的 Token 对
const newAccessToken = generateAccessToken(decoded.id);
const newRefreshToken = generateRefreshToken(decoded.id);
await Token.create({
userId: decoded.id,
token: newRefreshToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
successResponse(res, {
accessToken: newAccessToken,
refreshToken: newRefreshToken
});
};
三、接口防刷:Rate Limiting
暴力破解、短信轰炸、恶意注册……这些攻击都可以通过接口限流来有效防护。使用 express-rate-limit 实现基于 IP 的请求频率限制:
const rateLimit = require('express-rate-limit');
// 通用限流:每 IP 每分钟最多 100 次请求
const generalLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: {
success: false,
message: '请求过于频繁,请稍后再试'
},
standardHeaders: true,
legacyHeaders: false
});
// 登录接口限流:每 IP 每小时最多 10 次
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
message: {
success: false,
message: '登录尝试次数过多,请1小时后再试'
},
skipSuccessfulRequests: true // 登录成功不计入
});
// 应用限流
app.use('/api/', generalLimiter);
app.use('/api/v1/auth/login', loginLimiter);
四、安全中间件配置
使用 helmet 一键配置常用的 HTTP 安全头,防止 XSS、点击劫持、MIME 类型嗅探等常见攻击:
const helmet = require('helmet');
const hpp = require('hpp');
app.use(helmet());
app.use(hpp()); // 防止 HTTP 参数污染
// 禁用不必要的响应头
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));
五、输入校验:防止注入攻击
永远不要信任客户端传来的数据。使用 express-validator 对每个接口的输入参数进行严格校验:
const { body, validationResult } = require('express-validator');
const validateRegister = [
body('username')
.trim()
.isLength({ min: 2, max: 20 })
.withMessage('用户名需为 2-20 个字符')
.matches(/^[\u4e00-\u9fa5a-zA-Z0-9_]+$/)
.withMessage('用户名只能包含中英文、数字和下划线'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('邮箱格式不正确'),
body('password')
.isLength({ min: 6, max: 32 })
.withMessage('密码需为 6-32 个字符')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('密码需包含大小写字母和数字'),
// 校验处理中间件
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map(e => ({
field: e.path,
message: e.msg
}))
});
}
next();
}
];
// 使用
router.post('/register', validateRegister, register);
六、Flutter 端安全存储与自动刷新
在 Flutter 端,Token 必须使用 flutter_secure_storage 安全存储,绝不能使用明文的 SharedPreferences:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:dio/dio.dart';
class AuthManager {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
// 保存 Token
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await _storage.write(key: _accessTokenKey, value: accessToken);
await _storage.write(key: _refreshTokenKey, value: refreshToken);
}
// 自动刷新 Token 的拦截器
static Interceptor get authInterceptor => InterceptorsWrapper(
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// 尝试用 Refresh Token 换取新的 Access Token
final newTokens = await _refreshAccessToken();
if (newTokens != null) {
// 更新请求头并重试原请求
error.requestOptions.headers['Authorization'] =
'Bearer ${newTokens['accessToken']}';
return handler.resolve(await _dio.fetch(error.requestOptions));
}
// Refresh Token 也过期了 → 跳转登录页
await logout();
}
handler.next(error);
},
);
}
七、HTTPS 与数据加密传输
生产环境必须使用 HTTPS。如果在 Node.js 中自托管,可以使用 letsencrypt 免费证书:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/etc/letsencrypt/live/yourdomain.com/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/yourdomain.com/fullchain.pem'),
};
https.createServer(options, app).listen(443, () => {
});
同时配置 HTTP 自动重定向到 HTTPS:
const http = require('http');
http.createServer((req, res) => {
res.writeHead(301, {
Location: `https://${req.headers.host}${req.url}`
});
res.end();
}).listen(80);
总结
一个生产级的安全认证体系需要从多个维度进行防护:
身份认证层面:JWT 双 Token 机制 + Refresh Token 轮换,兼顾安全性与用户体验。Access Token 短期有效降低泄露风险,Refresh Token 一次性使用防止重放攻击。
接口防护层面:Rate Limiting 限制请求频率,输入校验防止注入攻击,Helmet 加固 HTTP 安全头。
数据传输层面:强制 HTTPS 加密传输,敏感数据加密存储。
客户端层面:使用 flutter_secure_storage 安全存储凭证,拦截器自动处理 Token 过期和刷新。
安全没有银弹,最好的策略是「纵深防御」——每一层都有自己的安全措施,即使某一层被突破,后面的层仍然能提供保护。这套方案虽然不是最复杂的,但在大多数中小型项目中已经足够安全可靠。