1077 lines
30 KiB
Markdown
1077 lines
30 KiB
Markdown
# 用户故事1.3实现计划:认证系统现代化
|
||
|
||
## 📋 实现目标
|
||
**作为** 系统用户,**我想要** 在新系统中安全便捷地登录和管理我的账户,**以便** 我能够正常访问配置管理功能。
|
||
|
||
## 🎯 验收标准对照与实现计划
|
||
|
||
### ✅ 功能需求实现计划
|
||
|
||
#### 1. 用户名/密码登录功能
|
||
**需求**: 实现用户名/密码登录功能,支持管理员自动登录功能
|
||
|
||
**当前状态分析**:
|
||
- ✅ 登录页面基础结构存在
|
||
- ✅ 表单组件已创建
|
||
- ⚠️ 缺少JWT令牌管理
|
||
- ⚠️ 缺少会话管理
|
||
- ⚠️ 缺少管理员自动登录功能
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// 1. 创建认证Context
|
||
interface AuthContextType {
|
||
user: User | null;
|
||
token: string | null;
|
||
login: (username: string, password: string) => Promise<void>;
|
||
logout: () => void;
|
||
isAuthenticated: boolean;
|
||
isLoading: boolean;
|
||
}
|
||
|
||
// 2. 创建登录API接口
|
||
interface LoginRequest {
|
||
username: string;
|
||
password: string;
|
||
captcha?: string;
|
||
}
|
||
|
||
interface LoginResponse {
|
||
token: string;
|
||
refreshToken: string;
|
||
user: User;
|
||
expiresIn: number;
|
||
}
|
||
|
||
// 3. 管理员自动登录检测
|
||
const autoLoginForAdmin = () => {
|
||
if (window.location.hostname === 'localhost' ||
|
||
window.location.hostname === '127.0.0.1') {
|
||
return {
|
||
username: 'admin',
|
||
password: 'admin123'
|
||
};
|
||
}
|
||
return null;
|
||
};
|
||
```
|
||
|
||
**需要创建/修改的文件**:
|
||
- `src/store/authStore.ts` - Zustand认证状态管理
|
||
- `src/hooks/useAuth.ts` - 认证Hook
|
||
- `src/services/authService.ts` - 认证API服务
|
||
- `src/types/auth.ts` - 认证相关类型定义
|
||
- `src/components/auth/LoginForm.tsx` - 登录表单组件
|
||
- `src/utils/tokenStorage.ts` - 令牌存储工具
|
||
|
||
#### 2. JWT令牌自动刷新和管理机制
|
||
**需求**: 实现 JWT 令牌自动刷新和管理机制,用于安全的会话处理
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// tokenStorage.ts
|
||
interface TokenStorage {
|
||
getToken(): string | null;
|
||
setTokens(accessToken: string, refreshToken: string): void;
|
||
removeTokens(): void;
|
||
getRefreshToken(): string | null;
|
||
isTokenExpired(token: string): boolean;
|
||
}
|
||
|
||
// authService.ts
|
||
class AuthService {
|
||
private refreshPromise: Promise<string> | null = null;
|
||
|
||
async refreshToken(): Promise<string> {
|
||
// 防止并发刷新
|
||
if (this.refreshPromise) {
|
||
return this.refreshPromise;
|
||
}
|
||
|
||
this.refreshPromise = this.doRefreshToken();
|
||
return this.refreshPromise.finally(() => {
|
||
this.refreshPromise = null;
|
||
});
|
||
}
|
||
|
||
private async doRefreshToken(): Promise<string> {
|
||
const refreshToken = tokenStorage.getRefreshToken();
|
||
if (!refreshToken) {
|
||
throw new Error('No refresh token available');
|
||
}
|
||
|
||
const response = await fetch('/api/auth/refresh', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ refreshToken }),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Token refresh failed');
|
||
}
|
||
|
||
const { token, refreshToken: newRefreshToken } = await response.json();
|
||
tokenStorage.setTokens(token, newRefreshToken);
|
||
return token;
|
||
}
|
||
}
|
||
|
||
// HTTP拦截器
|
||
const createAuthenticatedHttpClient = () => {
|
||
const client = axios.create({
|
||
baseURL: '/api',
|
||
});
|
||
|
||
client.interceptors.request.use(async (config) => {
|
||
let token = tokenStorage.getToken();
|
||
|
||
if (token && tokenStorage.isTokenExpired(token)) {
|
||
try {
|
||
token = await authService.refreshToken();
|
||
} catch (error) {
|
||
// 刷新失败,跳转到登录页
|
||
authStore.getState().logout();
|
||
return Promise.reject(error);
|
||
}
|
||
}
|
||
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`;
|
||
}
|
||
|
||
return config;
|
||
});
|
||
|
||
return client;
|
||
};
|
||
```
|
||
|
||
**需要创建的文件**:
|
||
- `src/services/httpClient.ts` - HTTP客户端
|
||
- `src/utils/tokenStorage.ts` - 令牌存储
|
||
- `src/services/authService.ts` - 认证服务
|
||
- `src/middleware/authInterceptor.ts` - 认证拦截器
|
||
|
||
#### 3. 会话超时和异常登录检测
|
||
**需求**: 实现会话超时和异常登录检测,增强安全性
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// sessionManager.ts
|
||
interface SessionManager {
|
||
startSessionMonitoring(): void;
|
||
stopSessionMonitoring(): void;
|
||
extendSession(): void;
|
||
checkSessionValidity(): boolean;
|
||
}
|
||
|
||
class SessionManagerImpl implements SessionManager {
|
||
private sessionTimeout: number = 30 * 60 * 1000; // 30分钟
|
||
private warningTimeout: number = 25 * 60 * 1000; // 25分钟警告
|
||
private lastActivity: number = Date.now();
|
||
private warningTimer: NodeJS.Timeout | null = null;
|
||
private timeoutTimer: NodeJS.Timeout | null = null;
|
||
|
||
startSessionMonitoring(): void {
|
||
this.resetTimers();
|
||
this.setupActivityListeners();
|
||
}
|
||
|
||
private resetTimers(): void {
|
||
this.clearTimers();
|
||
|
||
// 会话超时定时器
|
||
this.timeoutTimer = setTimeout(() => {
|
||
this.handleSessionTimeout();
|
||
}, this.sessionTimeout);
|
||
|
||
// 会话警告定时器
|
||
this.warningTimer = setTimeout(() => {
|
||
this.showSessionWarning();
|
||
}, this.warningTimeout);
|
||
}
|
||
|
||
private setupActivityListeners(): void {
|
||
const activities = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
|
||
|
||
activities.forEach(event => {
|
||
document.addEventListener(event, () => {
|
||
this.updateLastActivity();
|
||
}, true);
|
||
});
|
||
}
|
||
|
||
private updateLastActivity(): void {
|
||
this.lastActivity = Date.now();
|
||
this.extendSession();
|
||
}
|
||
|
||
extendSession(): void {
|
||
// 调用API延长会话
|
||
this.resetTimers();
|
||
}
|
||
|
||
private handleSessionTimeout(): void {
|
||
// 显示会话超时提示
|
||
authStore.getState().logout();
|
||
window.location.href = '/login?reason=session_timeout';
|
||
}
|
||
|
||
private showSessionWarning(): void {
|
||
// 显示会话即将过期警告
|
||
toast.warning('会话即将在5分钟后过期,请保存您的工作');
|
||
}
|
||
}
|
||
|
||
// 异常登录检测
|
||
interface SecurityMonitor {
|
||
detectAnomalousLogin(ipAddress: string, userAgent: string): Promise<boolean>;
|
||
recordLoginAttempt(attempt: LoginAttempt): void;
|
||
checkLoginFrequency(userId: string): Promise<boolean>;
|
||
}
|
||
```
|
||
|
||
**需要创建的文件**:
|
||
- `src/services/sessionManager.ts` - 会话管理器
|
||
- `src/services/securityMonitor.ts` - 安全监控
|
||
- `src/components/auth/SessionWarning.tsx` - 会话警告组件
|
||
- `src/hooks/useSessionTimeout.ts` - 会话超时Hook
|
||
|
||
#### 4. 密码修改功能
|
||
**需求**: 支持密码修改功能,具有适当的验证和安全检查
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// passwordService.ts
|
||
interface PasswordChangeRequest {
|
||
currentPassword: string;
|
||
newPassword: string;
|
||
confirmPassword: string;
|
||
}
|
||
|
||
interface PasswordValidation {
|
||
isValid: boolean;
|
||
errors: string[];
|
||
strength: 'weak' | 'medium' | 'strong';
|
||
}
|
||
|
||
class PasswordService {
|
||
validatePassword(password: string): PasswordValidation {
|
||
const errors: string[] = [];
|
||
|
||
if (password.length < 8) {
|
||
errors.push('密码长度至少8位');
|
||
}
|
||
|
||
if (!/[A-Z]/.test(password)) {
|
||
errors.push('密码必须包含大写字母');
|
||
}
|
||
|
||
if (!/[a-z]/.test(password)) {
|
||
errors.push('密码必须包含小写字母');
|
||
}
|
||
|
||
if (!/\d/.test(password)) {
|
||
errors.push('密码必须包含数字');
|
||
}
|
||
|
||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||
errors.push('密码必须包含特殊字符');
|
||
}
|
||
|
||
const strength = this.calculatePasswordStrength(password);
|
||
|
||
return {
|
||
isValid: errors.length === 0,
|
||
errors,
|
||
strength
|
||
};
|
||
}
|
||
|
||
private calculatePasswordStrength(password: string): 'weak' | 'medium' | 'strong' {
|
||
let score = 0;
|
||
|
||
if (password.length >= 12) score++;
|
||
if (password.length >= 8) score++;
|
||
if (/[a-z]/.test(password)) score++;
|
||
if (/[A-Z]/.test(password)) score++;
|
||
if (/\d/.test(password)) score++;
|
||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score++;
|
||
|
||
if (score >= 5) return 'strong';
|
||
if (score >= 3) return 'medium';
|
||
return 'weak';
|
||
}
|
||
|
||
async changePassword(request: PasswordChangeRequest): Promise<void> {
|
||
// 验证当前密码
|
||
const currentPasswordValid = await this.verifyCurrentPassword(request.currentPassword);
|
||
if (!currentPasswordValid) {
|
||
throw new Error('当前密码不正确');
|
||
}
|
||
|
||
// 验证新密码
|
||
const validation = this.validatePassword(request.newPassword);
|
||
if (!validation.isValid) {
|
||
throw new Error(validation.errors.join(', '));
|
||
}
|
||
|
||
// 确认新密码
|
||
if (request.newPassword !== request.confirmPassword) {
|
||
throw new Error('两次输入的新密码不一致');
|
||
}
|
||
|
||
// 调用API修改密码
|
||
const response = await fetch('/api/auth/change-password', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${tokenStorage.getToken()}`,
|
||
},
|
||
body: JSON.stringify({
|
||
currentPassword: request.currentPassword,
|
||
newPassword: request.newPassword,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('密码修改失败');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 密码修改表单组件
|
||
const PasswordChangeForm: React.FC = () => {
|
||
const [formData, setFormData] = useState<PasswordChangeRequest>({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: '',
|
||
});
|
||
|
||
const [validation, setValidation] = useState<PasswordValidation | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const handleNewPasswordChange = (value: string) => {
|
||
setFormData(prev => ({ ...prev, newPassword: value }));
|
||
const passwordValidation = passwordService.validatePassword(value);
|
||
setValidation(passwordValidation);
|
||
};
|
||
|
||
return (
|
||
<Card className="w-full max-w-md">
|
||
<CardHeader>
|
||
<CardTitle>修改密码</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="currentPassword">当前密码</Label>
|
||
<Input
|
||
id="currentPassword"
|
||
type="password"
|
||
value={formData.currentPassword}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
currentPassword: e.target.value
|
||
}))}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="newPassword">新密码</Label>
|
||
<Input
|
||
id="newPassword"
|
||
type="password"
|
||
value={formData.newPassword}
|
||
onChange={(e) => handleNewPasswordChange(e.target.value)}
|
||
required
|
||
/>
|
||
{validation && (
|
||
<PasswordStrengthIndicator validation={validation} />
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="confirmPassword">确认新密码</Label>
|
||
<Input
|
||
id="confirmPassword"
|
||
type="password"
|
||
value={formData.confirmPassword}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
confirmPassword: e.target.value
|
||
}))}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<Button type="submit" disabled={loading} className="w-full">
|
||
{loading ? '修改中...' : '修改密码'}
|
||
</Button>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
};
|
||
```
|
||
|
||
**需要创建的文件**:
|
||
- `src/services/passwordService.ts` - 密码服务
|
||
- `src/components/auth/PasswordChangeForm.tsx` - 密码修改表单
|
||
- `src/components/auth/PasswordStrengthIndicator.tsx` - 密码强度指示器
|
||
- `src/hooks/usePasswordValidation.ts` - 密码验证Hook
|
||
|
||
#### 5. 与原系统设计一致的登录界面
|
||
**需求**: 创建与原系统设计一致的登录界面
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// LoginForm.tsx
|
||
interface LoginFormProps {
|
||
onSuccess?: () => void;
|
||
onError?: (error: string) => void;
|
||
}
|
||
|
||
const LoginForm: React.FC<LoginFormProps> = ({ onSuccess, onError }) => {
|
||
const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
|
||
const [formData, setFormData] = useState({
|
||
username: '',
|
||
password: '',
|
||
phone: '',
|
||
phoneCode: '',
|
||
captcha: '',
|
||
});
|
||
|
||
const [loading, setLoading] = useState(false);
|
||
const [captchaUrl, setCaptchaUrl] = useState('');
|
||
|
||
const { login } = useAuth();
|
||
|
||
// 检测管理员环境自动填充
|
||
useEffect(() => {
|
||
const adminCreds = autoLoginForAdmin();
|
||
if (adminCreds) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
username: adminCreds.username,
|
||
password: adminCreds.password,
|
||
}));
|
||
}
|
||
}, []);
|
||
|
||
// 刷新验证码
|
||
const refreshCaptcha = () => {
|
||
setCaptchaUrl(`/api/auth/captcha?t=${Date.now()}`);
|
||
};
|
||
|
||
useEffect(() => {
|
||
refreshCaptcha();
|
||
}, []);
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
|
||
try {
|
||
if (loginType === 'password') {
|
||
await login(formData.username, formData.password, formData.captcha);
|
||
} else {
|
||
await loginWithPhone(formData.phone, formData.phoneCode, formData.captcha);
|
||
}
|
||
onSuccess?.();
|
||
} catch (error) {
|
||
onError?.(error instanceof Error ? error.message : '登录失败');
|
||
refreshCaptcha();
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4">
|
||
<Card className="w-full max-w-md">
|
||
<CardHeader className="text-center">
|
||
<div className="w-16 h-16 bg-green-600 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||
<Tractor className="w-8 h-8 text-white" />
|
||
</div>
|
||
<CardTitle className="text-2xl text-green-900">
|
||
智慧农业生产管理系统
|
||
</CardTitle>
|
||
<p className="text-muted-foreground">
|
||
请登录您的账户
|
||
</p>
|
||
</CardHeader>
|
||
|
||
<CardContent>
|
||
<Tabs value={loginType} onValueChange={(value) => setLoginType(value as 'password' | 'phone')}>
|
||
<TabsList className="grid w-full grid-cols-2">
|
||
<TabsTrigger value="password">密码登录</TabsTrigger>
|
||
<TabsTrigger value="phone">手机登录</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="password">
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="username">用户名</Label>
|
||
<Input
|
||
id="username"
|
||
type="text"
|
||
value={formData.username}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
username: e.target.value
|
||
}))}
|
||
placeholder="请输入用户名"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="password">密码</Label>
|
||
<Input
|
||
id="password"
|
||
type="password"
|
||
value={formData.password}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
password: e.target.value
|
||
}))}
|
||
placeholder="请输入密码"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="captcha">验证码</Label>
|
||
<div className="flex space-x-2">
|
||
<Input
|
||
id="captcha"
|
||
type="text"
|
||
value={formData.captcha}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
captcha: e.target.value
|
||
}))}
|
||
placeholder="请输入验证码"
|
||
required
|
||
className="flex-1"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={refreshCaptcha}
|
||
className="px-3"
|
||
>
|
||
<img
|
||
src={captchaUrl}
|
||
alt="验证码"
|
||
className="h-10 w-20"
|
||
onError={(e) => {
|
||
(e.target as HTMLImageElement).style.display = 'none';
|
||
}}
|
||
/>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox id="remember" />
|
||
<Label htmlFor="remember">记住登录状态</Label>
|
||
</div>
|
||
<Button variant="link" className="p-0 h-auto">
|
||
忘记密码?
|
||
</Button>
|
||
</div>
|
||
|
||
<Button type="submit" disabled={loading} className="w-full">
|
||
{loading ? '登录中...' : '登录'}
|
||
</Button>
|
||
</form>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="phone">
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div>
|
||
<Label htmlFor="phone">手机号</Label>
|
||
<Input
|
||
id="phone"
|
||
type="tel"
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
phone: e.target.value
|
||
}))}
|
||
placeholder="请输入手机号"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="phoneCode">短信验证码</Label>
|
||
<div className="flex space-x-2">
|
||
<Input
|
||
id="phoneCode"
|
||
type="text"
|
||
value={formData.phoneCode}
|
||
onChange={(e) => setFormData(prev => ({
|
||
...prev,
|
||
phoneCode: e.target.value
|
||
}))}
|
||
placeholder="请输入短信验证码"
|
||
required
|
||
className="flex-1"
|
||
/>
|
||
<Button type="button" variant="outline">
|
||
获取验证码
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Button type="submit" disabled={loading} className="w-full">
|
||
{loading ? '登录中...' : '登录'}
|
||
</Button>
|
||
</form>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{adminCreds && (
|
||
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
|
||
<p className="text-sm text-green-700">
|
||
<strong>测试账号:</strong> {adminCreds.username} / {adminCreds.password}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
**需要创建的文件**:
|
||
- `src/components/auth/LoginForm.tsx` - 登录表单组件
|
||
- `src/components/auth/CaptchaInput.tsx` - 验证码输入组件
|
||
- `src/components/auth/PhoneLogin.tsx` - 手机登录组件
|
||
- `src/styles/auth.css` - 认证相关样式
|
||
|
||
#### 6. "记住我"功能
|
||
**需求**: 实现"记住我"功能,用于跨浏览器会话的会话持久化
|
||
|
||
**实现计划**:
|
||
```typescript
|
||
// 记住我功能实现
|
||
interface RememberMeOptions {
|
||
enabled: boolean;
|
||
duration: number; // 记住时长(毫秒)
|
||
}
|
||
|
||
class RememberMeService {
|
||
private readonly storageKey = 'remember_me';
|
||
private readonly defaultDuration = 7 * 24 * 60 * 60 * 1000; // 7天
|
||
|
||
saveRememberedUser(username: string, duration: number = this.defaultDuration): void {
|
||
const data = {
|
||
username,
|
||
timestamp: Date.now(),
|
||
expiresAt: Date.now() + duration,
|
||
};
|
||
|
||
localStorage.setItem(this.storageKey, JSON.stringify(data));
|
||
}
|
||
|
||
getRememberedUser(): string | null {
|
||
const data = localStorage.getItem(this.storageKey);
|
||
if (!data) return null;
|
||
|
||
try {
|
||
const parsed = JSON.parse(data);
|
||
|
||
if (Date.now() > parsed.expiresAt) {
|
||
this.clearRememberedUser();
|
||
return null;
|
||
}
|
||
|
||
return parsed.username;
|
||
} catch {
|
||
this.clearRememberedUser();
|
||
return null;
|
||
}
|
||
}
|
||
|
||
clearRememberedUser(): void {
|
||
localStorage.removeItem(this.storageKey);
|
||
}
|
||
|
||
isRememberMeEnabled(): boolean {
|
||
return this.getRememberedUser() !== null;
|
||
}
|
||
}
|
||
|
||
// 在登录表单中使用
|
||
const LoginForm: React.FC = () => {
|
||
const [rememberMe, setRememberMe] = useState(false);
|
||
const rememberedUsername = rememberMeService.getRememberedUser();
|
||
|
||
useEffect(() => {
|
||
if (rememberedUsername) {
|
||
setFormData(prev => ({ ...prev, username: rememberedUsername }));
|
||
setRememberMe(true);
|
||
}
|
||
}, [rememberedUsername]);
|
||
|
||
const handleLoginSuccess = () => {
|
||
if (rememberMe) {
|
||
rememberMeService.saveRememberedUser(formData.username);
|
||
} else {
|
||
rememberMeService.clearRememberedUser();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit}>
|
||
{/* 表单字段 */}
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="remember"
|
||
checked={rememberMe}
|
||
onCheckedChange={setRememberMe}
|
||
/>
|
||
<Label htmlFor="remember">记住登录状态</Label>
|
||
</div>
|
||
|
||
<Button type="submit">登录</Button>
|
||
</form>
|
||
);
|
||
};
|
||
```
|
||
|
||
**需要创建的文件**:
|
||
- `src/services/rememberMeService.ts` - 记住我服务
|
||
- `src/components/auth/RememberMeCheckbox.tsx` - 记住我复选框
|
||
|
||
### ✅ 集成需求实现计划
|
||
|
||
#### 4. 现有用户认证凭据继续正常工作
|
||
**实现计划**:
|
||
```typescript
|
||
// 向后兼容的认证验证
|
||
class BackwardCompatibilityService {
|
||
async validateLegacyCredentials(username: string, password: string): Promise<boolean> {
|
||
try {
|
||
// 尝试使用新的认证API
|
||
const response = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password }),
|
||
});
|
||
|
||
if (response.ok) return true;
|
||
|
||
// 如果新API失败,尝试旧的认证方式
|
||
return await this.tryLegacyAuth(username, password);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private async tryLegacyAuth(username: string, password: string): Promise<boolean> {
|
||
// 实现旧版本认证逻辑
|
||
const legacyHash = this.generateLegacyHash(password);
|
||
const response = await fetch('/api/legacy/auth', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, hash: legacyHash }),
|
||
});
|
||
|
||
return response.ok;
|
||
}
|
||
|
||
private generateLegacyHash(password: string): string {
|
||
// 实现旧的密码哈希算法
|
||
return btoa(password + 'legacy_salt');
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 5. 新认证系统遵循既定的安全模式和标准
|
||
**实现计划**:
|
||
- 遵循OWASP认证安全最佳实践
|
||
- 实现密码强度验证
|
||
- 添加登录尝试限制
|
||
- 实现CSRF保护
|
||
- 使用HTTPS传输
|
||
- 安全存储令牌
|
||
|
||
#### 6. 与现有用户数据库的集成保持当前用户账户结构
|
||
**实现计划**:
|
||
```typescript
|
||
// 用户数据映射
|
||
interface LegacyUser {
|
||
id: number;
|
||
username: string;
|
||
password_hash: string;
|
||
real_name: string;
|
||
phone?: string;
|
||
role: string;
|
||
created_at: string;
|
||
}
|
||
|
||
interface ModernUser {
|
||
id: string;
|
||
username: string;
|
||
realName: string;
|
||
phone?: string;
|
||
role: UserRole;
|
||
createdAt: string;
|
||
lastLoginAt?: string;
|
||
isActive: boolean;
|
||
}
|
||
|
||
class UserDataAdapter {
|
||
transformLegacyUser(legacyUser: LegacyUser): ModernUser {
|
||
return {
|
||
id: legacyUser.id.toString(),
|
||
username: legacyUser.username,
|
||
realName: legacyUser.real_name,
|
||
phone: legacyUser.phone,
|
||
role: this.mapLegacyRole(legacyUser.role),
|
||
createdAt: legacyUser.created_at,
|
||
isActive: true,
|
||
};
|
||
}
|
||
|
||
private mapLegacyRole(legacyRole: string): UserRole {
|
||
const roleMap = {
|
||
'admin': 'admin',
|
||
'operator': 'operator',
|
||
'technician': 'technician',
|
||
};
|
||
|
||
return (roleMap[legacyRole] || 'operator') as UserRole;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7. 会话管理保持现有用户体验和安全级别
|
||
**实现计划**:
|
||
- 保持现有的会话超时时间
|
||
- 实现平滑的令牌刷新
|
||
- 维持用户导航状态
|
||
- 保存用户偏好设置
|
||
|
||
### ✅ 质量需求实现计划
|
||
|
||
#### 7. 认证系统被全面的安全测试覆盖
|
||
**实现计划**:
|
||
```typescript
|
||
// 安全测试工具
|
||
class SecurityTestSuite {
|
||
async runAuthenticationTests(): Promise<void> {
|
||
// 测试密码强度验证
|
||
await this.testPasswordStrength();
|
||
|
||
// 测试会话管理
|
||
await this.testSessionManagement();
|
||
|
||
// 测试令牌刷新
|
||
await this.testTokenRefresh();
|
||
|
||
// 测试异常登录检测
|
||
await this.testAnomalousLoginDetection();
|
||
|
||
// 测试CSRF保护
|
||
await this.testCSRFProtection();
|
||
}
|
||
|
||
private async testPasswordStrength(): Promise<void> {
|
||
const weakPasswords = ['123', 'password', 'admin'];
|
||
|
||
for (const password of weakPasswords) {
|
||
const validation = passwordService.validatePassword(password);
|
||
if (validation.isValid) {
|
||
throw new Error(`弱密码验证失败: ${password}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
private async testSessionManagement(): Promise<void> {
|
||
// 测试会话超时
|
||
// 测试会话刷新
|
||
// 测试并发登录
|
||
}
|
||
}
|
||
|
||
// 运行安全测试
|
||
const securityTests = new SecurityTestSuite();
|
||
await securityTests.runAuthenticationTests();
|
||
```
|
||
|
||
#### 8. 认证文档更新了新的安全功能
|
||
**需要创建的文档**:
|
||
- `docs/security/authentication.md` - 认证安全文档
|
||
- `docs/api/auth-endpoints.md` - 认证API文档
|
||
- `docs/user-guide/login.md` - 用户登录指南
|
||
|
||
#### 9. 验证现有用户访问功能无回归
|
||
**实现计划**:
|
||
```typescript
|
||
// 回归测试套件
|
||
class AuthenticationRegressionTests {
|
||
async runRegressionTests(): Promise<void> {
|
||
// 测试现有用户账户登录
|
||
await this.testExistingUserLogin();
|
||
|
||
// 测试权限系统
|
||
await this.testPermissionSystem();
|
||
|
||
// 测试导航功能
|
||
await this.testNavigationAccess();
|
||
|
||
// 测试数据访问
|
||
await this.testDataAccess();
|
||
}
|
||
|
||
private async testExistingUserLogin(): Promise<void> {
|
||
const testUsers = [
|
||
{ username: 'admin', password: 'admin123' },
|
||
{ username: 'operator', password: 'operator123' },
|
||
];
|
||
|
||
for (const user of testUsers) {
|
||
const result = await authService.login(user.username, user.password);
|
||
if (!result.success) {
|
||
throw new Error(`用户 ${user.username} 登录失败`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📁 详细实现文件清单
|
||
|
||
### 需要创建的核心文件
|
||
|
||
#### 1. 认证状态管理
|
||
- `src/store/authStore.ts` - Zustand认证状态管理
|
||
- `src/hooks/useAuth.ts` - 认证Hook
|
||
- `src/hooks/useSessionTimeout.ts` - 会话超时Hook
|
||
|
||
#### 2. 认证服务
|
||
- `src/services/authService.ts` - 认证API服务
|
||
- `src/services/sessionManager.ts` - 会话管理器
|
||
- `src/services/securityMonitor.ts` - 安全监控服务
|
||
- `src/services/passwordService.ts` - 密码服务
|
||
- `src/services/rememberMeService.ts` - 记住我服务
|
||
- `src/services/httpClient.ts` - HTTP客户端
|
||
|
||
#### 3. 认证组件
|
||
- `src/components/auth/LoginForm.tsx` - 登录表单
|
||
- `src/components/auth/PasswordChangeForm.tsx` - 密码修改表单
|
||
- `src/components/auth/PasswordStrengthIndicator.tsx` - 密码强度指示器
|
||
- `src/components/auth/SessionWarning.tsx` - 会话警告组件
|
||
- `src/components/auth/CaptchaInput.tsx` - 验证码输入组件
|
||
- `src/components/auth/PhoneLogin.tsx` - 手机登录组件
|
||
|
||
#### 4. 工具和配置
|
||
- `src/utils/tokenStorage.ts` - 令牌存储工具
|
||
- `src/middleware/authInterceptor.ts` - 认证拦截器
|
||
- `src/types/auth.ts` - 认证相关类型定义
|
||
- `src/config/auth.ts` - 认证配置
|
||
|
||
#### 5. 测试文件
|
||
- `src/tests/auth.test.ts` - 认证功能测试
|
||
- `src/tests/security.test.ts` - 安全功能测试
|
||
- `src/tests/regression.test.ts` - 回归测试
|
||
|
||
#### 6. 文档
|
||
- `docs/security/authentication.md` - 认证安全文档
|
||
- `docs/api/auth-endpoints.md` - 认证API文档
|
||
- `docs/user-guide/login.md` - 用户登录指南
|
||
|
||
## 🚀 实施步骤
|
||
|
||
### 阶段1: 基础认证架构 (60分钟)
|
||
1. 创建认证状态管理 (authStore.ts)
|
||
2. 实现基础认证服务 (authService.ts)
|
||
3. 创建令牌存储工具 (tokenStorage.ts)
|
||
4. 设置HTTP客户端和拦截器
|
||
|
||
### 阶段2: 登录界面开发 (45分钟)
|
||
1. 创建登录表单组件 (LoginForm.tsx)
|
||
2. 实现验证码功能 (CaptchaInput.tsx)
|
||
3. 添加手机登录支持 (PhoneLogin.tsx)
|
||
4. 实现记住我功能
|
||
|
||
### 阶段3: 会话管理和安全 (60分钟)
|
||
1. 实现会话管理器 (sessionManager.ts)
|
||
2. 添加安全监控 (securityMonitor.ts)
|
||
3. 实现令牌自动刷新
|
||
4. 创建会话超时警告
|
||
|
||
### 阶段4: 密码管理功能 (45分钟)
|
||
1. 创建密码修改表单 (PasswordChangeForm.tsx)
|
||
2. 实现密码强度验证 (PasswordStrengthIndicator.tsx)
|
||
3. 添加密码安全策略
|
||
4. 创建密码服务 (passwordService.ts)
|
||
|
||
### 阶段5: 集成和测试 (60分钟)
|
||
1. 集成所有认证组件
|
||
2. 实现向后兼容性
|
||
3. 运行安全测试
|
||
4. 执行回归测试
|
||
5. 更新文档
|
||
|
||
## ✅ 预期成果
|
||
|
||
### 完成后系统将具备
|
||
1. ✅ 现代化的JWT认证系统
|
||
2. ✅ 安全的会话管理和令牌刷新
|
||
3. ✅ 多种登录方式支持
|
||
4. ✅ 密码强度验证和修改功能
|
||
5. ✅ 异常登录检测和防护
|
||
6. ✅ 完善的用户体验和界面
|
||
7. ✅ 全面的安全测试覆盖
|
||
|
||
### 验收标准完成情况
|
||
- [ ] 功能需求1: 用户名/密码登录功能
|
||
- [ ] 功能需求2: JWT令牌自动刷新和管理
|
||
- [ ] 功能需求3: 会话超时和异常登录检测
|
||
- [ ] 功能需求4: 密码修改功能
|
||
- [ ] 功能需求5: 与原系统设计一致的登录界面
|
||
- [ ] 功能需求6: "记住我"功能
|
||
- [ ] 集成需求4: 现有用户认证凭据兼容
|
||
- [ ] 集成需求5: 遵循安全模式和标准
|
||
- [ ] 集成需求6: 用户数据库集成
|
||
- [ ] 集成需求7: 会话管理保持用户体验
|
||
- [ ] 质量需求7: 安全测试覆盖
|
||
- [ ] 质量需求8: 认证文档更新
|
||
- [ ] 质量需求9: 无回归验证
|
||
|
||
---
|
||
|
||
## 🛡️ 安全考虑
|
||
|
||
### 实现的安全措施
|
||
1. **密码安全**: 强密码策略、加盐哈希存储
|
||
2. **令牌安全**: JWT令牌、自动刷新机制
|
||
3. **会话安全**: 超时管理、异常检测
|
||
4. **传输安全**: HTTPS、CSRF保护
|
||
5. **输入验证**: 前后端双重验证
|
||
6. **访问控制**: 基于角色的权限管理
|
||
|
||
### 安全测试
|
||
- 密码强度测试
|
||
- 会话管理测试
|
||
- 令牌刷新测试
|
||
- 异常登录检测测试
|
||
- CSRF保护测试
|
||
- 权限控制测试
|
||
|
||
---
|
||
|
||
*此实现计划遵循用户故事1.3的所有验收标准,确保系统具备现代化、安全的认证功能。* |