【SpringBoot登录】设置图片验证及2FA双重身份验证两种方式

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("验证码错误或已失效");
		}
	}

}
相关推荐
华为云开发者联盟3 分钟前
一文了解Spring Boot启动类SpringApplication
java·spring boot·华为云开发者联盟·springapplication
一直学习永不止步5 分钟前
LeetCode题练习与总结:二叉树的后序遍历--145
java·算法·leetcode·二叉树···深度优先搜索
java66666888833 分钟前
Java数据结构:选择合适的数据结构解决问题
java·开发语言·数据结构
威哥爱编程36 分钟前
GuavaCache、EVCache、Tair、Aerospike 缓存框架比较
java·缓存·guava
MobTech袤博科技40 分钟前
ShareSDK iOS端如何实现小红书分享
java·大数据·ios
柚几哥哥40 分钟前
JFreeChart 生成Word图表
java·spring boot·word
喜欢猪猪1 小时前
注解的原理?如何自定义注解?
java·数据库·python
u0104058361 小时前
Java中的自然语言处理应用
java·开发语言·自然语言处理
明戈戈1 小时前
设计模式-观察者模式
java·观察者模式·设计模式
苟且.2 小时前
ThreadLocal、InheritableThreadLocal 和 TransmittableThreadLocal
java·多线程