455 lines
9.7 KiB
Markdown
455 lines
9.7 KiB
Markdown
# 登录功能说明文档
|
||
|
||
## 功能概述
|
||
|
||
本系统实现了多种登录方式,包括:
|
||
1. **密码登录**:用户名/手机号/邮箱 + 密码
|
||
2. **验证码登录**:手机号 + 短信验证码 / 邮箱 + 邮箱验证码
|
||
|
||
## 架构设计
|
||
|
||
### 策略模式架构
|
||
|
||
登录功能采用**策略模式(Strategy Pattern)**设计,具有以下优势:
|
||
- **高扩展性**:新增登录方式只需实现策略接口
|
||
- **低耦合**:登录逻辑与业务逻辑分离
|
||
- **易维护**:每种登录方式独立实现,互不干扰
|
||
|
||
### 核心组件
|
||
|
||
#### 1. 策略接口:`LoginStrategy`
|
||
|
||
```java
|
||
public interface LoginStrategy {
|
||
String getLoginType(); // 获取登录类型
|
||
boolean validate(LoginParam loginParam); // 验证登录参数
|
||
TbSysUser findUser(LoginParam loginParam); // 查找用户
|
||
boolean verifyCredential(String input, String stored); // 验证密码
|
||
boolean verifyCaptchaWithSession(LoginParam loginParam); // 验证验证码(带SessionID)
|
||
}
|
||
```
|
||
|
||
#### 2. 策略实现
|
||
|
||
- **PhoneLoginStrategy**:手机号登录策略
|
||
- 支持手机号 + 密码登录
|
||
- 支持手机号 + 短信验证码登录
|
||
|
||
- **EmailLoginStrategy**:邮箱登录策略
|
||
- 支持邮箱 + 密码登录
|
||
- 支持邮箱 + 邮箱验证码登录
|
||
|
||
- **UsernameLoginStrategy**:用户名登录策略
|
||
- 支持用户名 + 密码登录
|
||
|
||
- **PasswordLoginStrategy**:通用密码登录策略
|
||
- 自动识别用户名/手机号/邮箱
|
||
|
||
#### 3. 策略工厂:`LoginStrategyFactory`
|
||
|
||
根据登录类型(loginType)返回对应的登录策略实例。
|
||
|
||
#### 4. 登录服务:`LoginServiceImpl`
|
||
|
||
统一的登录入口,调用策略模式处理不同的登录方式。
|
||
|
||
## 前后端交互
|
||
|
||
### 前端登录请求
|
||
|
||
#### 密码登录示例
|
||
|
||
```javascript
|
||
{
|
||
"loginType": "phone", // 登录类型:username/phone/email/password
|
||
"phone": "13800138000", // 手机号(phone登录时必填)
|
||
"password": "123456", // 密码
|
||
"rememberMe": true, // 记住我
|
||
"agree": true // 同意协议
|
||
}
|
||
```
|
||
|
||
#### 验证码登录示例
|
||
|
||
```javascript
|
||
{
|
||
"loginType": "phone", // 登录类型
|
||
"phone": "13800138000", // 手机号
|
||
"captcha": "123456", // 验证码
|
||
"captchaId": "uuid-xxx", // 验证码会话ID
|
||
"rememberMe": true,
|
||
"agree": true
|
||
}
|
||
```
|
||
|
||
### 后端登录流程
|
||
|
||
```
|
||
1. 接收登录请求 (POST /auth/login)
|
||
↓
|
||
2. 自动检测登录类型(detectLoginType)
|
||
↓
|
||
3. 获取对应的登录策略(LoginStrategyFactory)
|
||
↓
|
||
4. 验证登录参数(strategy.validate)
|
||
↓
|
||
5. 查找用户(strategy.findUser)
|
||
↓
|
||
6. 检查用户状态
|
||
↓
|
||
7. 验证凭据:
|
||
- 密码登录 → strategy.verifyCredential(password, storedPassword)
|
||
- 验证码登录 → strategy.verifyCaptchaWithSession(loginParam)
|
||
↓
|
||
8. 生成JWT令牌
|
||
↓
|
||
9. 返回登录结果
|
||
```
|
||
|
||
## 验证码登录实现
|
||
|
||
### 验证码发送
|
||
|
||
#### 发送短信验证码
|
||
|
||
**接口**:`POST /auth/send-sms-code`
|
||
|
||
**请求**:
|
||
```json
|
||
{
|
||
"phone": "13800138000"
|
||
}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "验证码已发送",
|
||
"data": {
|
||
"sessionId": "uuid-xxx",
|
||
"message": "验证码已发送"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 发送邮箱验证码
|
||
|
||
**接口**:`POST /auth/send-email-code`
|
||
|
||
**请求**:
|
||
```json
|
||
{
|
||
"email": "user@example.com"
|
||
}
|
||
```
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"message": "验证码已发送到邮箱",
|
||
"data": {
|
||
"sessionId": "uuid-xxx",
|
||
"message": "验证码已发送到邮箱"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 验证码验证流程(策略模式)
|
||
|
||
以手机号验证码登录为例:
|
||
|
||
```java
|
||
@Override
|
||
public boolean verifyCaptchaWithSession(LoginParam loginParam) {
|
||
// 1. 获取参数
|
||
String captchaId = loginParam.getCaptchaId(); // 会话ID
|
||
String inputCaptcha = loginParam.getCaptcha(); // 用户输入的验证码
|
||
String phone = loginParam.getPhone(); // 手机号
|
||
|
||
// 2. 参数验证
|
||
if (captchaId == null || inputCaptcha == null) {
|
||
return false;
|
||
}
|
||
|
||
// 3. 从Redis获取验证码(使用SessionID)
|
||
String codeKey = "sms:code:" + captchaId;
|
||
String storedValue = redisService.get(codeKey); // 格式:"手机号:验证码"
|
||
|
||
if (storedValue == null) {
|
||
return false; // 验证码已过期
|
||
}
|
||
|
||
// 4. 解析存储的值
|
||
String[] parts = storedValue.split(":");
|
||
String storedPhone = parts[0];
|
||
String storedCaptcha = parts[1];
|
||
|
||
// 5. 验证手机号和验证码是否匹配
|
||
if (!storedPhone.equals(phone) || !storedCaptcha.equals(inputCaptcha)) {
|
||
return false;
|
||
}
|
||
|
||
// 6. 验证码使用后删除
|
||
redisService.delete(codeKey);
|
||
return true;
|
||
}
|
||
```
|
||
|
||
## 安全机制
|
||
|
||
### 1. SessionID绑定
|
||
|
||
- 验证码发送时生成唯一的`sessionId`
|
||
- Redis存储格式:`key="sms:code:{sessionId}", value="phone:code"`
|
||
- 验证时必须提供正确的`sessionId`才能获取验证码
|
||
- 防止验证码被其他用户使用
|
||
|
||
### 2. 双重验证
|
||
|
||
验证码登录时需要同时验证:
|
||
- 手机号/邮箱是否匹配
|
||
- 验证码是否正确
|
||
|
||
### 3. 一次性验证
|
||
|
||
验证码验证成功后立即从Redis删除,防止重复使用。
|
||
|
||
### 4. 有效期限制
|
||
|
||
- 验证码有效期:5分钟
|
||
- 发送频率限制:60秒内只能发送一次
|
||
|
||
### 5. 登录会话时长
|
||
|
||
- **勾选RememberMe**:Redis缓存7天,前端LocalStorage长期保存
|
||
- **未勾选RememberMe**:Redis缓存1天,前端SessionStorage会话保存
|
||
|
||
### 6. 登录失败限制
|
||
|
||
- 最大尝试次数:3次
|
||
- 超过限制后10分钟内无法登录
|
||
|
||
## Redis数据结构
|
||
|
||
### 验证码存储
|
||
|
||
```
|
||
# 短信验证码
|
||
key: sms:code:{sessionId}
|
||
value: {phone}:{code}
|
||
ttl: 300秒(5分钟)
|
||
|
||
# 邮箱验证码
|
||
key: email:code:{sessionId}
|
||
value: {email}:{code}
|
||
ttl: 300秒(5分钟)
|
||
```
|
||
|
||
### 发送频率限制
|
||
|
||
```
|
||
key: send:sms:{phone}
|
||
value: 1
|
||
ttl: 60秒
|
||
|
||
key: send:email:{email}
|
||
value: 1
|
||
ttl: 60秒
|
||
```
|
||
|
||
### 登录失败次数
|
||
|
||
```
|
||
key: login:attempt:{username}
|
||
value: {attemptCount}
|
||
ttl: 600秒(10分钟)
|
||
```
|
||
|
||
### 登录会话缓存
|
||
|
||
```
|
||
key: login:token:{userId}
|
||
value: LoginDomain对象
|
||
ttl:
|
||
- rememberMe=true: 7天(604800秒)
|
||
- rememberMe=false: 1天(86400秒)
|
||
```
|
||
|
||
## 前端实现要点
|
||
|
||
### 1. 登录模式切换
|
||
|
||
```javascript
|
||
const loginMode = ref<'password' | 'captcha'>('password'); // 密码登录/验证码登录
|
||
|
||
// 切换登录模式
|
||
const switchLoginMode = (mode) => {
|
||
loginMode.value = mode;
|
||
// 清空表单
|
||
loginForm.username = '';
|
||
loginForm.password = '';
|
||
loginForm.captcha = '';
|
||
loginForm.captchaId = '';
|
||
};
|
||
```
|
||
|
||
### 2. 验证码类型切换
|
||
|
||
```javascript
|
||
const captchaType = ref<'phone' | 'email'>('phone'); // 手机号/邮箱
|
||
|
||
// 切换验证码类型
|
||
const switchCaptchaType = (type) => {
|
||
captchaType.value = type;
|
||
// 清空相关字段
|
||
};
|
||
```
|
||
|
||
### 3. 发送验证码
|
||
|
||
```javascript
|
||
const handleSendSmsCode = async () => {
|
||
// 1. 验证手机号
|
||
if (!loginForm.phone || !phonePattern.test(loginForm.phone)) {
|
||
ElMessage.warning('请输入正确的手机号');
|
||
return;
|
||
}
|
||
|
||
// 2. 调用API发送验证码
|
||
const result = await authApi.sendSmsCode(loginForm.phone);
|
||
|
||
if (result.code === 200 && result.data) {
|
||
// 3. 保存sessionId(重要!)
|
||
loginForm.captchaId = result.data.sessionId;
|
||
|
||
// 4. 开始倒计时
|
||
smsCountdown.value = 60;
|
||
const timer = setInterval(() => {
|
||
smsCountdown.value--;
|
||
if (smsCountdown.value <= 0) {
|
||
clearInterval(timer);
|
||
}
|
||
}, 1000);
|
||
|
||
ElMessage.success('验证码已发送');
|
||
}
|
||
};
|
||
```
|
||
|
||
### 4. 提交登录
|
||
|
||
```javascript
|
||
const handleLogin = async () => {
|
||
// 1. 表单验证
|
||
await loginFormRef.value.validate();
|
||
|
||
// 2. 调用登录API(前端会自动填充loginType)
|
||
const result = await store.dispatch('auth/login', loginForm);
|
||
|
||
// 3. 登录成功处理
|
||
if (result.code === 200) {
|
||
ElMessage.success('登录成功!');
|
||
router.push('/home');
|
||
}
|
||
};
|
||
```
|
||
|
||
## API端点
|
||
|
||
| 方法 | 路径 | 说明 | 是否需要认证 |
|
||
|------|------|------|-------------|
|
||
| POST | /auth/login | 用户登录 | 否 |
|
||
| POST | /auth/send-sms-code | 发送短信验证码 | 否 |
|
||
| POST | /auth/send-email-code | 发送邮箱验证码 | 否 |
|
||
| POST | /auth/logout | 用户登出 | 是 |
|
||
|
||
## 配置说明
|
||
|
||
### application.yml
|
||
|
||
```yaml
|
||
# 邮件服务配置
|
||
spring:
|
||
mail:
|
||
host: smtp.qq.com
|
||
port: 587
|
||
username: your-email@qq.com
|
||
password: your-authorization-code
|
||
properties:
|
||
mail:
|
||
smtp:
|
||
auth: true
|
||
starttls:
|
||
enable: true
|
||
|
||
# 短信服务配置
|
||
sms:
|
||
enabled: true
|
||
provider: aliyun
|
||
access-key-id: your-access-key-id
|
||
access-key-secret: your-access-key-secret
|
||
sign-name: 红色思政学习平台
|
||
template-code: SMS_xxxxx
|
||
region-id: cn-hangzhou
|
||
|
||
# 安全白名单
|
||
school-news:
|
||
auth:
|
||
white-list:
|
||
- /auth/login
|
||
- /auth/send-sms-code
|
||
- /auth/send-email-code
|
||
```
|
||
|
||
## 测试建议
|
||
|
||
### 1. 密码登录测试
|
||
|
||
- 用户名 + 密码
|
||
- 手机号 + 密码
|
||
- 邮箱 + 密码
|
||
|
||
### 2. 验证码登录测试
|
||
|
||
- 手机号 + 短信验证码
|
||
- 邮箱 + 邮箱验证码
|
||
- 验证码过期测试
|
||
- SessionID不匹配测试
|
||
- 验证码错误测试
|
||
|
||
### 3. 安全测试
|
||
|
||
- 登录失败次数限制
|
||
- 验证码发送频率限制
|
||
- 验证码一次性使用
|
||
- SessionID绑定验证
|
||
|
||
## 扩展说明
|
||
|
||
如需新增登录方式(如微信登录),只需:
|
||
|
||
1. 创建新的策略实现类
|
||
```java
|
||
@Component
|
||
public class WechatLoginStrategy implements LoginStrategy {
|
||
@Override
|
||
public String getLoginType() {
|
||
return "wechat";
|
||
}
|
||
// 实现其他方法...
|
||
}
|
||
```
|
||
|
||
2. 前端添加对应的UI和逻辑
|
||
|
||
策略工厂会自动识别并加载新的登录策略。
|
||
|
||
---
|
||
|
||
**文档版本**:v1.0
|
||
**更新日期**:2025-11-03
|
||
**作者**:开发团队
|
||
|