登录注册、手机号、邮箱
This commit is contained in:
454
schoolNewsServ/auth/登录功能说明.md
Normal file
454
schoolNewsServ/auth/登录功能说明.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# 登录功能说明文档
|
||||
|
||||
## 功能概述
|
||||
|
||||
本系统实现了多种登录方式,包括:
|
||||
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
|
||||
**作者**:开发团队
|
||||
|
||||
Reference in New Issue
Block a user