外观
如何实现双因子认证功能
这是什么?
双因子认证(2FA)是一种安全机制,要求用户提供两种不同的验证方式来证明其身份。就像进入一个高安全级别区域,不仅需要会员卡(密码),还需要指纹或密码锁(第二因子)。这个指南将教你如何给系统添加双因子认证功能,提升系统安全性。
📝 实现步骤
第一步:首次登录与双因子认证数据模型
首先,我们需要定义用户登录时需要提供的信息和双因子认证相关的数据模型:
java
@Data
public class LoginDto {
/**
* 用户名(账号)
*/
@NotBlank(message = "请输入用户名!")
private String accountNo;
/**
* 密码
*/
@NotBlank(message = "请输入密码!")
private String password;
}
@Data
public class SendCodeDto {
/**
* 手机号(用于短信验证码登录)
*/
private String phone;
}
@Data
public class TwoFactorAuthDto {
/**
* 验证码
*/
@NotBlank(message = "验证码不能为空!")
private String code;
}第二步:创建登录接口和双因子认证接口
接下来,创建处理登录请求和双因子认证的接口:
java
/**
* 用户登录接口(第一阶段)
* @param dto 登录信息
* @return 登录结果(包含用户ID,用于双因子认证)
*/
@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResultVo<SessionVo>> login(@RequestBody @Valid LoginDto dto) {
return Mono.just(ResultVo.ok(loginService.firstFactorLogin(dto)));
}
/**
* 发送验证码接口
* @param dto 发送验证码请求
* @return 发送结果
*/
@PostMapping(value = "/sendCode", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResultVo<Boolean>> sendCode(@RequestBody @Valid SendCodeDto dto) {
return Mono.just(ResultVo.ok(loginService.sendVerificationCode(dto)));
}
/**
* 双因子认证接口(第二阶段)
* @param dto 双因子认证信息
* @return 登录结果(包含完整会话信息)
*/
@PostMapping(value = "/verify2fa", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResultVo<SessionVo>> verifyTwoFactor(@RequestBody @Valid TwoFactorAuthDto dto) {
return Mono.just(ResultVo.ok(loginService.verifyTwoFactorAuth(dto)));
}⚠️ 特别注意: 双因子认证接口必须返回ResultVo<SessionVo>类型的结果,否则登录会失败。
第三步:编写双因子认证登录逻辑
这一步是实现两阶段登录验证逻辑,包括用户名密码验证和双因子验证:
java
@Override
public SessionVo firstFactorLogin(LoginDto dto) {
// 验证用户名和密码(第一因子)
if (!"admin".equals(dto.getAccountNo()) || !"123456".equals(dto.getPassword())) {
throw new InvokeException(-1, "用户名或密码错误,请重试!");
}
// 验证通过后,生成临时会话,返回用户ID,等待双因子认证
SessionVo session = new SessionVo();
session.setUserId("1");//用户id
session.setNickname("wueasy");//用户昵称
// 用于返回数据给前端
Map<String, String> successfulMap = new HashMap<>();
successfulMap.put("phone", "13800000000");
session.setSuccessfulMap(successfulMap);
return session;
}
@Override
public boolean sendVerificationCode(SendCodeDto dto) {
// 从会话中获取用户ID
Long userId = UserHelper.getUserIdLong();
// 生成验证码
String code = "123456";
// 存储验证码到Redis,设置过期时间(默认5分钟)
String redisKey = "two_factor:code:" + userId;
redisTemplate.opsForValue().set(redisKey, code, 5, TimeUnit.MINUTES);
//TODO 发送验证码到用户手机号(短信验证码登录)
return true;
}
@Override
public SessionVo verifyTwoFactorAuth(TwoFactorAuthDto dto) {
// 从会话中获取用户ID
Long userId = UserHelper.getUserIdLong();
// 检查验证码是否正确(第二因子)
String redisKey = "two_factor:code:" + userId;
String storedCode = redisTemplate.opsForValue().get(redisKey);
if (storedCode == null) {
throw new InvokeException(-3, "验证码已过期,请重新获取!");
}
if (!storedCode.equals(dto.getCode())) {
throw new InvokeException(-5, "验证码错误,请重新输入!");
}
// 验证通过,清除验证码和错误计数
redisTemplate.delete(redisKey);
// 创建完整用户会话信息
SessionVo session = new SessionVo();
session.setUserId(userId.toString());//用户id
session.setNickname("wueasy");//用户昵称
Set<String> authorizeCodeList = new HashSet<String>();
authorizeCodeList.add("user");
session.setAuthorizeCodeList(authorizeCodeList);//权限代码集合,用于前端判断权限
// 可访问的url地址权限,用户有权限的url地址集合
Set<String> linkUrlSetAll = new HashSet<String>();
linkUrlSetAll.add("/demo/get");
session.setAuthorizeUrlList(linkUrlSetAll);
// 登录成功时返回前端的信息
Map<String, String> successfulMap = new HashMap<>();
successfulMap.put("type", "dd1");
session.setSuccessfulMap(successfulMap);
return session;
}第四步:了解会话信息
SessionVo(会话信息)包含以下重要信息:
🔑 基本信息
userId:用户的唯一标识nickname:用户的昵称avatarUrl:用户头像地址
👮 权限信息
isSystem:是否是超级管理员authorizeUrlList:允许访问的网址列表authorizeCodeList:用户的权限代码列表
🗃️ 扩展信息
extendedMap:额外的用户信息successfulMap:登录成功时返回的信息
第五步:配置双因子认证系统
最后,在配置文件(config.yaml)中设置登录和双因子认证相关的规则:
yaml
gateway:
filter:
session:
enabled: true # 启用登录验证
rules:
- type: redis # 使用redis存储用户信息
redis-auto-expire: true # 自动延长登录有效期
expire: 1h # 登录1小时后过期
urls: # 拦截的url地址
- /demo/**
user-login-urls: # 登录接口地址
- /demo/login
- /demo/sendCode # 二次验证接口需要配置成登录权限拦截
- /demo/verify2fa # 二次验证接口需要配置成登录权限拦截
security-visitor-urls: # 不需要登录就能访问的地址
- /demo/login # 首次登录需要设置成免登陆
two-factor-auth-enabled: true # 启用双因子认证
two-factor-login-urls: # 双因子认证登录接口地址
- /demo/verify2fa
two-factor-auth-urls: # 双因子认证验证接口地址
- /demo/sendCode
- /demo/verify2fa
two-factor-auth-expire: 5m # 双因子认证验证码有效期🚀 如何使用双因子登录
双因子登录流程包含三个步骤:首次登录验证(用户名密码)、发送验证码、验证双因子。
步骤1:首次登录(用户名密码验证)
shell
curl --location --request POST "http://127.0.0.1:8080/demo/login" \
--header "Content-Type: application/json" \
--data-raw "{
\"accountNo\": \"admin\",
\"password\": \"123456\"
}"首次登录成功返回示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"authorization": "bearer:1:1a2e5967c2574713ab744d00dc9aacb2",
"tempAuthorization": "",
"successfulMap": {
"phone": "13800000000"
},
"twoFactorAuth": true
},
"encrypt": false
}步骤2:发送验证码
shell
curl --location --request POST "http://127.0.0.1:8080/demo/sendCode" \
--header "Content-Type: application/json" \
--data-raw "{
\"phone\": \"13800000000\"
}"发送验证码成功返回示例
json
{
"code": 0,
"successful": true,
"msg": null,
"encrypt": false
}步骤3:验证双因子认证
shell
curl --location --request POST "http://127.0.0.1:8080/demo/verify2fa" \
--header "Content-Type: application/json" \
--data-raw "{
\"code\": \"123456\" // 用户收到的验证码
}"双因子认证成功返回示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"authorization": "bearer:1:6c5d35f9fe464d2db256b2524c83a47b",
"tempAuthorization": "bearer:temp:6cc39973aa7648c8a4dbb4a0f1b03708",
"successfulMap": {
"type": "dd1"
},
"twoFactorAuth": false
},
"encrypt": false
}❓ 常见问题
1. 登录失败?
可能原因:
- ✗ 用户名或密码错误
- ✗ 接口地址不正确
- ✗ 请求格式错误
- ✗ 服务器内部错误
解决方法:
- ✓ 检查用户名和密码是否正确
- ✓ 确认接口地址是否正确
- ✓ 验证请求Content-Type为application/json
- ✓ 查看系统日志了解详细错误信息
2. 双因子认证失败?
可能原因:
- ✗ 验证码错误
- ✗ 验证码已过期
- ✗ 验证码重试次数超过限制
- ✗ 用户账号被临时锁定
解决方法:
- ✓ 检查输入的验证码是否正确
- ✓ 确认验证码是否在有效期内(默认5分钟)
- ✓ 如超过重试次数,请等待一段时间后重新获取验证码
- ✓ 查看系统日志了解详细错误信息
3. 收不到验证码?
可能原因:
- ✗ 短信/邮件服务配置错误
- ✗ 用户联系方式不正确
- ✗ 短信/邮件服务商限制
- ✗ 发送频率过高被限制
解决方法:
- ✓ 检查短信/邮件服务配置是否正确
- ✓ 确认用户的手机号码/邮箱是否正确
- ✓ 检查是否超过发送频率限制(默认60秒)
- ✓ 查看短信/邮件服务提供商的日志
4. 登录后无法访问其他功能?
可能原因:
- ✗ 登录凭证未正确传递
- ✗ 用户权限不足
- ✗ 登录已过期
- ✗ 配置文件中URL路径不匹配
解决方法:
- ✓ 检查请求头中是否包含authorization字段
- ✓ 确认用户是否有相应的访问权限
- ✓ 验证登录是否已过期
- ✓ 检查配置文件中的URL匹配规则
5. Redis连接问题?
可能原因:
- ✗ Redis服务未启动
- ✗ 连接配置错误
- ✗ 网络连接问题
- ✗ Redis密码错误
解决方法:
- ✓ 确认Redis服务是否正常运行
- ✓ 检查Redis连接配置是否正确
- ✓ 验证网络连接是否正常
- ✓ 确认Redis密码是否正确
6. 会话经常过期?
可能原因:
- ✗ 过期时间设置过短
- ✗ Redis内存不足
- ✗ 服务重启导致会话丢失
解决方法:
- ✓ 适当延长expire时间
- ✓ 启用redis-auto-expire自动延期
- ✓ 增加Redis内存或清理无用数据
💡 使用建议
开发测试时:
- 可以适当延长验证码有效期和登录有效期
- 打开详细日志方便调试
- 考虑使用模拟的验证码验证方式
正式使用时:
- 设置合理的密码强度要求
- 定期清理过期的会话信息和验证码缓存
- 注意保护用户登录凭证和验证码信息
- 合理设置验证码有效期、重试次数和发送间隔
- 建议使用HTTPS协议保护传输过程
安全性增强建议:
- 对重要操作可以强制要求双因子认证
- 记录双因子认证的日志,便于审计
- 考虑支持多种双因子认证方式(短信、邮件、认证APP等)
- 实现验证码失败次数过多时的账号临时锁定机制
🆘 需要帮助?
如果遇到问题,可以按以下步骤排查:
- 📝 查看日志:检查
./logs/app.log文件中的双因子认证相关日志 - 🔍 检查Redis:使用Redis客户端查看会话数据和验证码缓存
- 🌐 网络检查:确认网络连接和短信/邮件服务是否正常
- 📋 配置验证:检查YAML配置文件中的双因子认证配置项
- 🔧 调试模式:启用debug日志级别获取详细信息