Migrate project from typing_practiceweb
This commit is contained in:
205
server/middleware/auth.ts
Normal file
205
server/middleware/auth.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
// server/middleware/auth.ts
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { config } from '../config';
|
||||
|
||||
// 用户接口
|
||||
interface UserPayload {
|
||||
_id: string;
|
||||
username: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
// 扩展 Request 类型
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
token?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 认证错误类
|
||||
class AuthError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode: number = 401,
|
||||
public code: string = 'AUTH_ERROR'
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AuthError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并解析 JWT token
|
||||
* @param token JWT token
|
||||
* @returns 解析后的用户信息
|
||||
*/
|
||||
const verifyToken = (token: string): UserPayload => {
|
||||
try {
|
||||
const decoded = jwt.verify(token, config.JWT_SECRET) as UserPayload;
|
||||
|
||||
console.debug('Token verification:', {
|
||||
userId: decoded._id,
|
||||
username: decoded.username,
|
||||
expiresIn: decoded.exp ? new Date(decoded.exp * 1000).toISOString() : 'unknown'
|
||||
});
|
||||
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
console.debug('Token expired:', { error: error.message, expiredAt: error.expiredAt });
|
||||
throw new AuthError('认证令牌已过期', 401, 'TOKEN_EXPIRED');
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
console.debug('Invalid token:', { error: error.message });
|
||||
throw new AuthError('无效的认证令牌', 401, 'INVALID_TOKEN');
|
||||
}
|
||||
console.error('Unexpected token verification error:', error);
|
||||
throw new AuthError('认证失败', 401, 'AUTH_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
*/
|
||||
export const auth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.header('Authorization');
|
||||
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
error: '未提供认证令牌',
|
||||
code: 'NO_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
if (!authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
error: '认证令牌格式无效',
|
||||
code: 'INVALID_TOKEN_FORMAT'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '').trim();
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
error: '认证令牌为空',
|
||||
code: 'EMPTY_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = verifyToken(token);
|
||||
|
||||
if (!decoded._id || !decoded.username) {
|
||||
return res.status(401).json({
|
||||
error: '认证令牌信息不完整',
|
||||
code: 'INCOMPLETE_TOKEN'
|
||||
});
|
||||
}
|
||||
|
||||
if (decoded.exp) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const timeLeft = decoded.exp - now;
|
||||
if (timeLeft < 300) {
|
||||
console.debug('Token expiring soon:', {
|
||||
timeLeft: `${timeLeft}s`,
|
||||
expireAt: new Date(decoded.exp * 1000).toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
req.user = decoded;
|
||||
req.token = token;
|
||||
|
||||
console.debug('Auth successful:', {
|
||||
userId: decoded._id,
|
||||
username: decoded.username,
|
||||
path: req.path
|
||||
});
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return res.status(error.statusCode).json({
|
||||
error: error.message,
|
||||
code: error.code
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth error:', error);
|
||||
return res.status(401).json({
|
||||
error: '认证失败',
|
||||
code: 'AUTH_FAILED'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选认证中间件
|
||||
* 不强制要求认证,但如果提供了有效的 token 则解析用户信息
|
||||
*/
|
||||
export const optionalAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
if (token) {
|
||||
console.debug('Optional auth: token present');
|
||||
const decoded = verifyToken(token);
|
||||
req.user = decoded;
|
||||
req.token = token;
|
||||
} else {
|
||||
console.debug('Optional auth: no token');
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
console.debug('Optional auth failed:', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
code: error instanceof AuthError ? error.code : 'OPTIONAL_AUTH_FAILED'
|
||||
});
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 管理员认证中间件
|
||||
*/
|
||||
export const adminAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await auth(req, res, () => {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
console.debug('Admin auth failed:', {
|
||||
userId: req.user?._id,
|
||||
username: req.user?.username,
|
||||
isAdmin: req.user?.isAdmin
|
||||
});
|
||||
return res.status(403).json({
|
||||
error: '需要管理员权限',
|
||||
code: 'ADMIN_REQUIRED'
|
||||
});
|
||||
}
|
||||
console.debug('Admin auth successful:', {
|
||||
userId: req.user._id,
|
||||
username: req.user.username
|
||||
});
|
||||
next();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Admin auth unexpected error:', error);
|
||||
res.status(500).json({
|
||||
error: '服务器内部错误',
|
||||
code: 'SERVER_ERROR'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 导出类型和错误类
|
||||
export type { UserPayload };
|
||||
export { AuthError };
|
||||
58
server/middleware/practiceValidation.ts
Normal file
58
server/middleware/practiceValidation.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// server/middleware/practiceValidation.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import crypto from 'crypto';
|
||||
export const validatePracticeSubmission = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const { signature, timestamp, ...practiceData } = req.body;
|
||||
const currentServerTime = Date.now();
|
||||
|
||||
// 1. 验证时间戳
|
||||
if (Math.abs(currentServerTime - timestamp) > 2000) {
|
||||
return res.status(400).json({
|
||||
error: '提交时间异111常',
|
||||
code: 'INVALID_TIMESTAMP'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 使用相同的方式序列化数据
|
||||
const orderedData = JSON.stringify(practiceData, Object.keys(practiceData).sort());
|
||||
|
||||
// 3. 验证签名
|
||||
const expectedSignature = crypto
|
||||
.createHash('sha256')
|
||||
.update(orderedData + timestamp)
|
||||
.digest('hex');
|
||||
|
||||
console.log('Debug info:', {
|
||||
receivedSignature: signature,
|
||||
expectedSignature: expectedSignature,
|
||||
data: orderedData,
|
||||
timestamp: timestamp
|
||||
});
|
||||
|
||||
if (signature !== expectedSignature) {
|
||||
return res.status(400).json({
|
||||
error: '数据签名无效',
|
||||
code: 'INVALID_SIGNATURE',
|
||||
debug: {
|
||||
received: signature,
|
||||
expected: expectedSignature
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ... 其他验证代码保持不变
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
res.status(400).json({
|
||||
error: '数据验证失败'+error,
|
||||
code: 'VALIDATION_ERROR'
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user