框架模块说明 #07 二次验证(MFA)_02

发布于:2024-12-07 ⋅ 阅读:(166) ⋅ 点赞:(0)

前言

在前一篇文章中,我们介绍了用户 MFA(多因素认证)的统一校验过程,即在操作过程中的身份校验。然而,在某些业务场景中可能需要强制校验,以确保操作确实由用户本人执行。为此,我们可以采用切面 + 注解的方式来实现业务操作的强制校验,进一步提升安全性。

与此同时,在某些特定场景下(例如 APP 中的一些校验场景),注解方式可能不够灵活或不太适用。为了解决这些问题,我们还可以提供一个工具类(Util),便于开发者在不同的业务场景中灵活调用。

接下来,我们将围绕以下三个方面进行详细展开:

  1. 操作过程中的强制校验实现(基于切面 + 注解)。
  2. 工具类的设计与实现,满足不同场景的灵活调用需求。
  3. 实际业务场景中的应用与最佳实践

通过上述方法,我们能够在不同场景下实现高效、灵活的用户校验机制,进一步提升业务操作的安全性和用户体验。

注解类

切面类的属性里默认的mfa验证是google,目前实现的也只有google,未来可能会实现如邮件、手机等一些需求,我们也预留了一个适配器的接口进行处理。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MfaAuth {
    CoreConstant.MfaAuthType mfaAuthType() default CoreConstant.MfaAuthType.GOOGLE;
}

切面类

对header头里的验证码进行校验,这段代码只支持了Google验证器一种验证方式,其它情况会抛出异常,还有就是验证不通过也会抛现一个错误码,供前台进行翻译用。

@Aspect
@Slf4j
@Order(10)
public class MfaAuthAspect {
    @Pointcut("@annotation(com.unknow.first.annotation.MfaAuth)")
    public void mfaAuth() {
    }

    @Around("mfaAuth()")
    public Object mfaAuthCheck(ProceedingJoinPoint joinPoint) throws Throwable {
        //用的最多通知的签名
        Signature signature = joinPoint.getSignature();
        MethodSignature msg = (MethodSignature) signature;
        Object target = joinPoint.getTarget();
        Method method = target.getClass().getMethod(msg.getName(), msg.getParameterTypes());
        final MfaAuth mfaAuthAnnotation = method.getAnnotation(MfaAuth.class);
        // 如果不校验双因子验证那么直接不拦截,此处为url强校验开关,每次都要校验。
//        Boolean isMfaVerify = SystemDicUtil.single().getValue("systemConfig", "isMfaVerify", "false", Boolean.class);
//        if (!isMfaVerify) {
//            return joinPoint.proceed();
//        }
        if (CoreConstant.MfaAuthType.GOOGLE.equals(mfaAuthAnnotation.mfaAuthType())) {
            checkGoogleValidCode();
        } else {
            throw new BusinessException(String.format("MFA validate [%s],is not support!", mfaAuthAnnotation.mfaAuthType()));
        }
        return joinPoint.proceed();
    }


    /**
     * 校验当前用户的谷歌验证码是否正确
     */
    private void checkGoogleValidCode() throws Exception {
        String googleSecret = GoogleAuthenticatorUtil.single().getCurrentUserVerifyKey();
        if (!GoogleAuthenticatorUtil.single().checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }

UTIL类

主要是在构造函数中加载好google验证的service类,这里是有问题的,对未来兼容其它验证方式显得过于死板,未来这里是要重构的,只能说满足了现在的需求而已。未来一定要通过接口适配的试重构。

package com.unknow.first.util;


import static com.unknow.first.mfa.config.MfaFilterConfig.__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY;
import static org.cloud.constant.MfaConstant.CORRELATION_YOUR_GOOGLE_KEY;
import static org.cloud.constant.MfaConstant.MFA_HEADER_NAME;
import static org.cloud.constant.MfaConstant._GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME;
import static org.cloud.constant.MfaConstant._GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME;

import cn.hutool.core.util.ObjectUtil;
import com.unknow.first.mfa.service.GoogleAuthenticatorService;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
import org.cloud.context.RequestContext;
import org.cloud.context.RequestContextManager;
import org.cloud.core.redis.RedisUtil;
import org.cloud.encdec.service.AESService;
import org.cloud.entity.LoginUserDetails;
import org.cloud.exception.BusinessException;
import org.cloud.feign.service.ICommonServiceFeignClient;
import org.cloud.utils.CollectionUtil;
import org.cloud.utils.HttpServletUtil;
import org.cloud.utils.SpringContextUtil;
import org.cloud.vo.FrameUserRefVO;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;

/**
 * google身份验证器,java服务端实现
 *
 * @author yangbo
 * @version 创建时间:2017年8月14日 上午10:10:02
 */
@Slf4j
public final class GoogleAuthenticatorUtil {

    // 生成的key长度( Generate secret key length)
    public final int SECRET_SIZE = 15;
    public final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
    // Java实现随机数算法
    public final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    /**
     * Generate a random secret key. This must be saved by the server and associated with the users account to verify the code displayed by
     * Google Authenticator. The user must register this secret on their device. 生成一个随机秘钥
     *
     * @return secret key
     */
    public String generateSecretKey() {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(SEED));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String encodedKey = new String(bEncodedKey);
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            // should never occur... configuration error
        }
        return null;
    }

    /**
     * Return a URL that generates and displays a QR barcode. The user scans this bar code with the Google Authenticator application on
     * their smartphone to register the auth code. They can also manually enter the secret if desired
     *
     * @param user   user id (e.g. fflinstone)
     * @param host   host or system that the code is for (e.g. myapp.com)
     * @param secret the secret that was previously generated for this user
     * @return the URL for the QR code to scan
     */
    @SneakyThrows
    public String getQRBarcodeURL(String user, String host, String secret) {
        final String otpauth = "otpauth://totp/%s@%s?secret=%s";
        final String barCodeApiUrl = "https://api.pwmqr.com/qrcode/create/?url=";
        return barCodeApiUrl + URLEncoder.encode(String.format(otpauth, user, host, secret), "utf8");
    }

    /**
     * 生成一个google身份验证器,识别的字符串,只需要把该方法返回值生成二维码扫描就可以了。
     *
     * @param user   账号
     * @param secret 密钥
     * @return
     */
    public String getQRBarcode(String user, String secret) {
        String format = "otpauth://totp/%s?secret=%s";
        return String.format(format, user, secret);
    }

    /**
     * Check the code entered by the user to see if it is valid 验证code是否合法
     *
     * @param secret   The users secret.
     * @param code     The code displayed on the users device
     * @param timeMsec The time in msec (System.currentTimeMillis() for example)
     * @return
     */
    public boolean checkCode(String secret, long code, long timeMsec) {
        return authenticatorService.checkCode(secret, code, timeMsec);
    }

    /**
     * @return
     * @throws Exception
     */
    public String getCurrentUserVerifyKey() throws Exception {

        String result = this.getCurrentUserVerifyKey(false);
        if (result != null) {
            return result;
        }
        RequestContext currentRequestContext = RequestContextManager.single().getRequestContext();
        LoginUserDetails user = currentRequestContext.getUser();
        FrameUserRefVO googleSecretRefVO = this.createNewUserRefVO(user);
        commonServiceFeignClient.addUserRef(googleSecretRefVO);
        final Map<String, String> exceptionObject = new LinkedHashMap<>();
        exceptionObject.put("description", CORRELATION_YOUR_GOOGLE_KEY.description());
        exceptionObject.put("secret", googleSecretRefVO.getAttributeValue());
        exceptionObject.put("secretQRBarcode", this.getQRBarcode(user.getUsername(), googleSecretRefVO.getAttributeValue()));
        exceptionObject.put("secretQRBarcodeURL", this.getQRBarcodeURL(user.getUsername(), "", googleSecretRefVO.getAttributeValue()));
        RedisUtil.single().set(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId(), googleSecretRefVO.getAttributeValue(), -1L);
        throw new BusinessException(CORRELATION_YOUR_GOOGLE_KEY.value(), exceptionObject, HttpStatus.BAD_REQUEST.value()); //
    }

    /**
     * @return
     * @throws Exception
     */
    public String getCurrentUserVerifyKey(Boolean isRefresh) throws Exception {
        RequestContext currentRequestContext = RequestContextManager.single().getRequestContext();
        LoginUserDetails user = currentRequestContext.getUser();
        if (!isRefresh) {
            String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId());
            if (CollectionUtil.single().isNotEmpty(googleSecret)) {
                return googleSecret;
            }
        }
        FrameUserRefVO googleSecretRefVO = commonServiceFeignClient.getCurrentUserRefByAttributeName(
            _GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.value());
        //  如果未绑定谷歌验证那么插入谷歌验证属性
        if (ObjectUtil.isNotEmpty(googleSecretRefVO)) {
            if (isRefresh) {
                googleSecretRefVO.setAttributeValue(aesService.encrypt(this.generateSecretKey()));
                commonServiceFeignClient.updateUserRef(googleSecretRefVO);
            }
            RedisUtil.single().set(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + user.getId(), googleSecretRefVO.getAttributeValue(), -1L);
            return googleSecretRefVO.getAttributeValue();
        }
        return null;

    }

    /**
     * 校验当前用户是否已经绑定谷歌验证码
     */
    public void verifyCurrentUserBindGoogleKey() throws BusinessException {
        FrameUserRefVO frameUserRefVO = commonServiceFeignClient.getCurrentUserRefByAttributeName(
            _GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value());

        if (frameUserRefVO == null || "false".equals(frameUserRefVO.getAttributeValue())) {
            throw new BusinessException(CORRELATION_YOUR_GOOGLE_KEY.value(), CORRELATION_YOUR_GOOGLE_KEY.description(),
                HttpStatus.BAD_REQUEST.value());
        }
    }

    public Boolean checkGoogleVerifyCode(String googleSecret) throws BusinessException {
        final String mfaValue = HttpServletUtil.single().getHttpServlet().getHeader(MFA_HEADER_NAME.value());
        return checkGoogleVerifyCode(googleSecret, mfaValue);
    }

    public Boolean checkGoogleVerifyCode(final String googleSecretEnc, final String mfaValue) throws BusinessException {
        return authenticatorService.checkGoogleVerifyCode(googleSecretEnc, mfaValue);
    }

    @SneakyThrows
    public FrameUserRefVO createNewUserRefVO(LoginUserDetails loginUserDetails) {
        final String googleSecret = this.generateSecretKey();
        FrameUserRefVO frameUserRefVO = new FrameUserRefVO();
        frameUserRefVO.setAttributeName(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.value());
        frameUserRefVO.setUserId(loginUserDetails.getId());
        frameUserRefVO.setAttributeValue(aesService.encrypt(googleSecret));
        frameUserRefVO.setRemark(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.description());
        frameUserRefVO.setCreateBy(loginUserDetails.getUsername());
        frameUserRefVO.setUpdateBy(loginUserDetails.getUsername());
        frameUserRefVO.setRemark(_GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME.description());
        return frameUserRefVO;
    }

    public void checkGoogleValidCode() throws Exception {
        verifyCurrentUserBindGoogleKey();
        String googleSecret = getCurrentUserVerifyKey();
        if (!GoogleAuthenticatorUtil.single().checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }

    public void checkGoogleValidCode(Long userId) throws Exception {
        String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + userId);
        if (!checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }

    public void checkGoogleValidCode(Long userId, String mfaValue) throws Exception {
        String googleSecret = RedisUtil.single().get(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + userId);
        if (!checkGoogleVerifyCode(googleSecret, mfaValue)) {
            throw new BusinessException("system.error.google.valid", 401);
        }
    }


    private final ICommonServiceFeignClient commonServiceFeignClient;
    private final GoogleAuthenticatorService authenticatorService;
    private final AESService aesService;

    @Lazy
    private GoogleAuthenticatorUtil() {
        commonServiceFeignClient = SpringContextUtil.getBean(ICommonServiceFeignClient.class);
        authenticatorService = SpringContextUtil.getBean(GoogleAuthenticatorService.class);
        aesService = SpringContextUtil.getBean(AESService.class);
    }

    private final static GoogleAuthenticatorUtil INSTANCE = new GoogleAuthenticatorUtil();

    public static GoogleAuthenticatorUtil single() {
        return INSTANCE;
    }

}

总结

在这两篇文章中,我们详细介绍了开发框架中关于 MFA(多因素认证)验证的相关内容。我们实现了对用户操作过程的行为校验以及业务操作的强制校验,设计上注重灵活性,能够适应多种场景的需求。目前框架支持的验证方式仅限于 Google Authenticator,这也是未来需要优化和扩展的方向。

如需了解详细的代码实现,请访问以下地址:
GitCode - 全球开发者的开源社区,开源代码托管平台GitCode是面向全球开发者的开源社区,包括原创博客,开源代码托管,代码协作,项目管理等。与开发者社区互动,提升您的研发效率和质量。icon-default.png?t=O83Ahttps://gitcode.com/YouYouLongLong/springcloud-framework/tree/master/core-common-parent/mfa-common

欢迎大家提出宝贵的建议和意!


网站公告

今日签到

点亮在社区的每一天
去签到