# 登录功能说明文档

## 功能概述

本系统实现了多种登录方式，包括：
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  
**作者**：开发团队

