1. 登录接口
- 用户登录模块的思路,先校验验证码是否匹配,然后校验用户名,密码是否匹配,匹配则用StpUtil生成token并返回。
java
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import dev.samstevens.totp.exceptions.QrGenerationException;
import im.gy.zfile.core.util.AjaxJson;
import im.gy.zfile.module.config.model.dto.SystemConfigDTO;
import im.gy.zfile.module.config.service.SystemConfigService;
import im.gy.zfile.module.login.model.enums.LoginVerifyModeEnum;
import im.gy.zfile.module.login.model.request.VerifyLoginTwoFactorAuthenticatorRequest;
import im.gy.zfile.module.login.model.result.LoginTwoFactorAuthenticatorResult;
import im.gy.zfile.module.login.model.result.LoginVerifyImgResult;
import im.gy.zfile.module.login.request.UserLoginRequest;
import im.gy.zfile.module.login.service.ImgVerifyCodeService;
import im.gy.zfile.module.login.service.TwoFactorAuthenticatorVerifyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 登录注销相关接口
*/
@Api(tags = "登录模块")
@ApiSort(1)
@RestController
@RequestMapping("/admin")
public class LoginController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ImgVerifyCodeService imgVerifyCodeService;
@Resource
private TwoFactorAuthenticatorVerifyService twoFactorAuthenticatorVerifyService;
@ApiOperationSupport(order = 1, ignoreParameters = {"zfile-token"})
@ApiOperation("登录")
@PostMapping("/login")
public AjaxJson<?> doLogin(@Valid @RequestBody UserLoginRequest userLoginRequest) {
String verifyCode = userLoginRequest.getVerifyCode();
String verifyCodeUUID = userLoginRequest.getVerifyCodeUUID();
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
LoginVerifyModeEnum loginVerifyMode = systemConfig.getLoginVerifyMode();
String loginVerifySecret = systemConfig.getLoginVerifySecret();
if (ObjectUtil.equals(loginVerifyMode, LoginVerifyModeEnum.TWO_FACTOR_AUTHENTICATION_MODE)) {
twoFactorAuthenticatorVerifyService.checkCode(loginVerifySecret, verifyCode);
} else if (ObjectUtil.equals(loginVerifyMode, LoginVerifyModeEnum.IMG_VERIFY_MODE)) {
imgVerifyCodeService.checkCaptcha(verifyCodeUUID, verifyCode);
}
if (ObjectUtil.equals(systemConfig.getUsername(), userLoginRequest.getUsername()) &&
ObjectUtil.equals(systemConfig.getPassword(), SecureUtil.md5(userLoginRequest.getPassword()))) {
StpUtil.login("admin");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return AjaxJson.getSuccess("登录成功", tokenInfo);
}
return AjaxJson.getError("登录失败,账号或密码错误");
}
@ApiOperationSupport(order = 2)
@ApiOperation(value = "注销")
@PostMapping("/logout")
public AjaxJson<?> logout() {
StpUtil.logout();
return AjaxJson.getSuccess("注销成功");
}
@ApiOperationSupport(order = 3)
@ApiOperation(value = "生成 2FA")
@GetMapping("/2fa/setup")
public AjaxJson<LoginTwoFactorAuthenticatorResult> setupDevice() throws QrGenerationException {
LoginTwoFactorAuthenticatorResult loginTwoFactorAuthenticatorResult = twoFactorAuthenticatorVerifyService.setupDevice();
return AjaxJson.getSuccessData(loginTwoFactorAuthenticatorResult);
}
@ApiOperationSupport(order = 4)
@ApiOperation(value = "2FA 验证并绑定")
@PostMapping("/2fa/verify")
public AjaxJson<?> deviceVerify(@Valid @RequestBody VerifyLoginTwoFactorAuthenticatorRequest verifyLoginTwoFactorAuthenticatorRequest) {
twoFactorAuthenticatorVerifyService.deviceVerify(verifyLoginTwoFactorAuthenticatorRequest);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 5)
@ApiOperation(value = "获取登录验证方式")
@GetMapping("/login/verify-mode")
public AjaxJson<LoginVerifyModeEnum> loginVerifyMode() {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
return AjaxJson.getSuccessData(systemConfig.getLoginVerifyMode());
}
@ApiOperationSupport(order = 6)
@ApiOperation(value = "获取图形验证码")
@GetMapping("/login/captcha")
public AjaxJson<LoginVerifyImgResult> captcha() {
LoginVerifyImgResult loginVerifyImgResult = imgVerifyCodeService.generatorCaptcha();
return AjaxJson.getSuccessData(loginVerifyImgResult);
}
@ApiOperationSupport(order = 7)
@ApiOperation(value = "检测是否已登录")
@GetMapping("/login/check")
public AjaxJson<Boolean> checkLogin() {
return AjaxJson.getSuccessData(StpUtil.isLogin());
}
}
2. 图片验证码服务
- 图片验证码使用CaptchaUtil生成,并保存在缓存中,采用FIFO更新策略
- 验证方式是根据用户提供的uuid匹配对应的code,code与用户提供的相同则匹配成功。
java
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.FIFOCache;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.core.lang.UUID;
import im.zhaojun.zfile.core.exception.LoginVerifyException;
import im.zhaojun.zfile.module.login.model.result.LoginVerifyImgResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 图片验证码 Service
*
* @author zhaojun
*/
@Service
@Slf4j
public class ImgVerifyCodeService {
/**
* 最大容量为 100 的验证码缓存,防止恶意请求占满内存. 验证码有效期为 60 秒.
*/
private final FIFOCache<String, String> verifyCodeCache = CacheUtil.newFIFOCache(100,60 * 1000L);
/**
* 生成验证码,并写入缓存中.
*
* @return 验证码生成结果
*/
public LoginVerifyImgResult generatorCaptcha() {
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 45, 4, 7);
String code = captcha.getCode();
String imageBase64 = captcha.getImageBase64Data();
String uuid = UUID.fastUUID().toString();
verifyCodeCache.put(uuid, code);
LoginVerifyImgResult loginVerifyImgResult = new LoginVerifyImgResult();
loginVerifyImgResult.setImgBase64(imageBase64);
loginVerifyImgResult.setUuid(uuid);
return loginVerifyImgResult;
}
/**
* 对验证码进行验证.
*
* @param uuid
* 验证码 uuid
*
* @param code
* 验证码
*
* @return 是否验证成功
*/
public boolean verifyCaptcha(String uuid, String code) {
String expectedCode = verifyCodeCache.get(uuid);
return Objects.equals(expectedCode, code);
}
/**
* 对验证码进行验证, 如验证失败则抛出异常
*
* @param uuid
* 验证码 uuid
*
* @param code
* 验证码
*/
public void checkCaptcha(String uuid, String code) {
boolean flag = verifyCaptcha(uuid, code);
if (!flag) {
throw new LoginVerifyException("验证码错误或已失效.");
}
}
}
3. 双因素认证服务
- 验证使用verifier校验用户提供的secret和code
java
package im.zhaojun.zfile.module.login.service;
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import im.zhaojun.zfile.core.exception.LoginVerifyException;
import im.zhaojun.zfile.module.login.model.request.VerifyLoginTwoFactorAuthenticatorRequest;
import im.zhaojun.zfile.module.login.model.result.LoginTwoFactorAuthenticatorResult;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.login.model.enums.LoginVerifyModeEnum;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import static dev.samstevens.totp.util.Utils.getDataUriForImage;
/**
* 2FA 双因素认证 Service
*
* @author zhaojun
*/
@Service
public class TwoFactorAuthenticatorVerifyService {
@Resource
private SecretGenerator secretGenerator;
@Resource
private QrDataFactory qrDataFactory;
@Resource
private QrGenerator qrGenerator;
@Resource
private CodeVerifier verifier;
@Resource
private SystemConfigService systemConfigService;
/**
* 生成 2FA 双因素认证二维码和密钥
*
* @return 2FA 双因素认证二维码和密钥
* @throws QrGenerationException 二维码生成异常
*/
public LoginTwoFactorAuthenticatorResult setupDevice() throws QrGenerationException {
// 生成 2FA 密钥
String secret = secretGenerator.generate();
QrData data = qrDataFactory.newBuilder().secret(secret).issuer("ZFile").build();
// 将生成的 2FA 密钥转换为 Base64 图像字符串
String qrCodeImage = getDataUriForImage(
qrGenerator.generate(data),
qrGenerator.getImageMimeType());
return new LoginTwoFactorAuthenticatorResult(qrCodeImage, secret);
}
/**
* 验证 2FA 双因素认证是否正确,正确则进行绑定.
*
* @param verifyLoginTwoFactorAuthenticatorRequest
* 2FA 双因素认证请求参数
*/
public void deviceVerify(VerifyLoginTwoFactorAuthenticatorRequest verifyLoginTwoFactorAuthenticatorRequest) {
String secret = verifyLoginTwoFactorAuthenticatorRequest.getSecret();
String code = verifyLoginTwoFactorAuthenticatorRequest.getCode();
if (verifier.isValidCode(secret, code)) {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
systemConfig.setLoginVerifyMode(LoginVerifyModeEnum.TWO_FACTOR_AUTHENTICATION_MODE);
systemConfig.setLoginVerifySecret(secret);
systemConfigService.updateSystemConfig(systemConfig);
} else {
throw new LoginVerifyException("验证码不正确");
}
}
/**
* 验证 2FA 双因素认证.
*
* @param loginVerifySecret
* 2FA 双因素认证密钥
*
* @param verifyCode
* 2FA 双因素认证验证码
*/
public void checkCode(String loginVerifySecret, String verifyCode) {
if (!verifier.isValidCode(loginVerifySecret, verifyCode)) {
throw new LoginVerifyException("验证码错误或已失效");
}
}
}