框架模块说明 #06 二次验证(MFA)_01

背景

在用户登录或执行敏感操作时,我们引入了二次验证机制,以全面提升后台安全性。具体而言:

  1. 登录时的图形验证:通过图形验证码,有效防范恶意攻击和自动化脚本,确保初始登录的安全性。
  2. 针对海外用户的 TG 验证码二次验证:额外增加基于 Telegram 验证码的身份验证,进一步增强安全保障。
  3. 谷歌验证:谷歌验证器分为两种,一种是对敏感操作的认证,我们增加了注解进行强制验证,另外一种是行为检查式的验证,通常是半小时内必须输入一次验证的等式。

这种双重验证机制,不仅强化了登录行为的安全性,还为敏感操作提供了更加稳固的保护措施,实现了对系统安全的多层次防护,本章节只讲mfa的后台配置、行为校验拦截器和表结构。

常量

如下定义了两个常量,用来保存用户是否绑定google的标识,这两个值将保存在t_frame_user_ref表中,在下面的段落中将会介绍。

    _GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME("GOOGLE_MFA_USER_SECRET_BIND_FLAG", "谷歌验证绑定标识", "system.mfa.google.secret.flag"),
    _GOOGLE_MFA_USER_SECRET_REF_ATTR_NAME("GOOGLE_MFA_USER_SECRET_KEY", "谷歌key", "system.mfa.google.secret.key"),

表结构

其中google的密钥要用aes加密处理。

CREATE TABLE `t_frame_user_ref` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `attribute_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
  `attribute_value` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `create_by` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_by` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  `remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  PRIMARY KEY (`user_id`,`attribute_name`) USING BTREE,
  KEY `ak_kid` (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8mb3;

API类

里面的代码太多了,这里就只贴出一部分,详细代码请参考GitCode - 全球开发者的开源社区,开源代码托管平台GitCode是面向全球开发者的开源社区,包括原创博客,开源代码托管,代码协作,项目管理等。与开发者社区互动,提升您的研发效率和质量。icon-default.png?t=O83Ahttps://gitcode.com/YouYouLongLong/springcloud-framework/blob/master/springcloud-tester/com.longyou.commservice/src/main/java/com/longyou/comm/conntroller/UserMfaController.java

@RestController
@RequestMapping("/user/mfa")
@SystemResource(path = "/common/user/mfa")
@Api(value = "UserMfaController", tags = "双因子验证API")
@Slf4j
public class UserMfaController {

    @Autowired
    private FrameUserRefService frameUserRefService;
    @Value("${system.mfa.expired-time:1800}")
    private Long expiredTime;

    /**
     * 校验谷歌验证码是否已经绑定
     *
     * @return
     * @throws Exception
     */
    @ApiOperation("校验谷歌验证码是否已经绑定")
    @GetMapping("/checkUserGoogleSecretBindStatus")
    @SystemResource
//    @AuthLog(bizType = "framework", desc = "校验谷歌验证码是否已经绑定", operateLogType = OperateLogType.LOG_TYPE_FRONTEND)
    public CommonApiResult<Map<String, Object>> checkUserGoogleSecretBindStatus() throws Exception {
        LoginUserDetails loginUserDetails = RequestContextManager.single().getRequestContext().getUser();
        CommonApiResult<Map<String, Object>> responseResult = CommonApiResult.createSuccessResult();
        FrameUserRefVO frameUserRefVO = frameUserRefService.getUserRefByAttributeName(loginUserDetails.getId(),
            _GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value());
        final Map<String, Object> returnData = new LinkedHashMap<>();
        if (frameUserRefVO != null && "true".equals(frameUserRefVO.getAttributeValue())) {
            returnData.put(_GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value(), true);
        } else {
            String googleSecretEnc = GoogleAuthenticatorUtil.single().getCurrentUserVerifyKey(true);
            if (googleSecretEnc == null) {
                frameUserRefVO = GoogleAuthenticatorUtil.single().createNewUserRefVO(loginUserDetails);
                frameUserRefService.create(frameUserRefVO);
                googleSecretEnc = frameUserRefVO.getAttributeValue();
            }
            final String googleSecret = AES128Util.single().decrypt(googleSecretEnc);
            returnData.put(_GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value(), false);
            returnData.put("description", CORRELATION_YOUR_GOOGLE_KEY.description());
            returnData.put("secret", googleSecret);
            returnData.put("secretQRBarcode", GoogleAuthenticatorUtil.single().getQRBarcode(loginUserDetails.getUsername(), googleSecret));
//            returnData.put("secretQRBarcodeURL",
//                GoogleAuthenticatorUtil.single().getQRBarcodeURL(loginUserDetails.getUsername(), "", googleSecret));
            RedisUtil.single().set(__MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY + loginUserDetails.getId(), googleSecretEnc);
        }
        responseResult.setData(returnData);
        return responseResult;
    }

    /**
     * 绑定谷歌验证码
     *
     * @return
     * @throws Exception
     */
    @ApiOperation("绑定谷歌验证")
    @GetMapping("/bindUserGoogleSecret")
    @SystemResource(value = "bindUserGoogleSecret", description = "绑定谷歌验证")
    @AuthLog(bizType = "framework", desc = "绑定谷歌验证", operateLogType = OperateLogType.LOG_TYPE_FRONTEND)
    public CommonApiResult<FrameUserRefVO> bindUserGoogleSecret() throws Exception {
        // 绑定的时候每次都要重新的生成一个出来
        String googleSecret = GoogleAuthenticatorUtil.single().getCurrentUserVerifyKey();
        if (!GoogleAuthenticatorUtil.single().checkGoogleVerifyCode(googleSecret)) {
            throw new BusinessException("system.error.google.valid", 400);
        }
        LoginUserDetails loginUserDetails = RequestContextManager.single().getRequestContext().getUser();
        CommonApiResult<FrameUserRefVO> responseResult = CommonApiResult.createSuccessResult();
        FrameUserRefVO frameUserRefVO = frameUserRefService.getUserRefByAttributeName(loginUserDetails.getId(),
            _GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value());
        if (frameUserRefVO == null) {
            frameUserRefVO = new FrameUserRefVO();
            frameUserRefVO.setUserId(loginUserDetails.getId());
            frameUserRefVO.setAttributeName(_GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.value());
            frameUserRefVO.setAttributeValue("true");
            frameUserRefVO.setRemark(_GOOGLE_MFA_USER_SECRET_REF_FlAG_ATTR_NAME.description());
            frameUserRefService.create(frameUserRefVO);
        } else {
            frameUserRefVO.setAttributeValue("true");
            frameUserRefService.update(frameUserRefVO);
        }
        responseResult.setData(frameUserRefVO);
        ThreadUtil.execAsync(() -> RedisUtil.single().removeUserLoginToken(loginUserDetails.getId()));
        return responseResult;
    }

行为校验过滤器

主要是对操作过程中的二次验证,按配置的时间间隔进行校验,如果用户在redis的mfa校验已经过期了,那么要重新校验,其中过期的KEY是和你的IP进行绑定的。

GitCode - 全球开发者的开源社区,开源代码托管平台GitCode是面向全球开发者的开源社区,包括原创博客,开源代码托管,代码协作,项目管理等。与开发者社区互动,提升您的研发效率和质量。icon-default.png?t=O83Ahttps://gitcode.com/YouYouLongLong/springcloud-framework/blob/master/core-common-parent/mfa-common/src/main/java/com/unknow/first/mfa/config/MfaFilterConfig.java

package com.unknow.first.mfa.config;

import static org.cloud.constant.LoginTypeConstant.LoginTypeEnum.LOGIN_BY_ADMIN_USER;
import static org.cloud.constant.MfaConstant.CORRELATION_GOOGLE_NOT_VERIFY_OR_EXPIRE;

import com.unknow.first.util.GoogleAuthenticatorUtil;
import java.io.IOException;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.Setter;
import org.cloud.constant.CoreConstant;
import org.cloud.context.RequestContext;
import org.cloud.context.RequestContextManager;
import org.cloud.core.redis.RedisUtil;
import org.cloud.entity.LoginUserDetails;
import org.cloud.exception.BusinessException;
import org.cloud.utils.HttpServletUtil;
import org.cloud.utils.IPUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

/**
 * mfa验证器,目前只支持google
 */
@ConfigurationProperties(prefix = "system.mfa")
@Configuration
@ConditionalOnProperty(prefix = "system.mfa", name = "enabled", matchIfMissing = true)
public class MfaFilterConfig {

    public final static String __MFA_TOKEN_USER_CACHE_KEY = "system:mfa:user:verify:result:";  // 校验结果
    public final static String __MFA_TOKEN_USER_GOOGLE_SECRET_CACHE_KEY = "system:mfa:user:secret:result:"; // 谷歌key

    @Setter
    private List<String> excludeUri;      // 默认为内部调用的url也可以自己添加
    private final CoreConstant.MfaAuthType mfaAuthType = CoreConstant.MfaAuthType.GOOGLE;  //默认为google验证

    @Bean
    public FilterRegistrationBean<?> mfaWebFilter() {
        this.excludeUri.add("/v2/api-docs");
        this.excludeUri.add("/inner/**/*");
        this.excludeUri.add("/user/verify/generate/*");
        this.excludeUri.add("/user/mfa/**");
        this.excludeUri.add("/app/userRef/isBindLoginIp");
        this.excludeUri.add("/app/userRef/ipLoginLockFlagOpen");
        this.excludeUri.add("/app/userRef/changeLoginIp");
        this.excludeUri.add("/app/userRef/getCurrentIp");
        this.excludeUri.add("/user/menu/getMenus");
        FilterRegistrationBean<?> registration = new FilterRegistrationBean<>(new MfaWebFilter(excludeUri));
        registration.addUrlPatterns("/*");
        registration.setName("mfaWebFilter");
        registration.setOrder(100);
        return registration;
    }

    static class MfaWebFilter extends OncePerRequestFilter {

        private final List<String> noMfaCheckUrl;      // 默认为内部调用的url也可以自己添加

        public MfaWebFilter(List<String> noMfaCheckUrl) {
            this.noMfaCheckUrl = noMfaCheckUrl;
        }

        @Override
        protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse,
            @NotNull FilterChain filterChain) throws ServletException, IOException {
            if (HttpServletUtil.single().isExcludeUri(httpServletRequest, noMfaCheckUrl)) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
                return;
            }
            // 如果数据字典中关闭了双因子校验,那么不进行双因子校验,关闭数据字典的全局配置,防止密码泄露后可以更改此项目绕过google
//            TSystemDicItem dicItem = SystemDicUtil.single().getDicItem("systemConfig", "isMfaVerify");
//            if (dicItem != null && StringUtils.hasLength(dicItem.getDicItemValue()) && "false".equals(dicItem.getDicItemValue())) {
//                filterChain.doFilter(httpServletRequest, httpServletResponse);
//                return;
//            }
            RequestContext currentRequestContext = RequestContextManager.single().getRequestContext();
            LoginUserDetails user = currentRequestContext.getUser();
            // 只有需要登录鉴权的接口并且用户类型为管理员才需要校验双因子
            if (user == null || (!LOGIN_BY_ADMIN_USER.userType.equals(user.getUserType()))) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
                return;
            }
            try {
                GoogleAuthenticatorUtil.single().verifyCurrentUserBindGoogleKey();
            } catch (BusinessException businessException) {
                try {
                    HttpServletUtil.single().handlerBusinessException(businessException, httpServletResponse);
                } catch (Exception e) {
                    logger.warn("校验google验证绑定情况失败时将信息写入到response时失败," + e.getMessage());
                }
                logger.error("用户未绑定google");
                return;
            }
            final String ipHash = RedisUtil.single().getMd5Key(IPUtil.single().getIpAddress(httpServletRequest));
            Boolean isValidatePass = RedisUtil.single().get(__MFA_TOKEN_USER_CACHE_KEY + user.getId() + ":" + ipHash);
            // 如果规定时间内校验过并且未过期,那么不校验
            if (isValidatePass != null && isValidatePass) {
                filterChain.doFilter(httpServletRequest, httpServletResponse);
            } else {
                try {
                    HttpServletUtil.single()
                        .handlerBusinessException(new BusinessException(CORRELATION_GOOGLE_NOT_VERIFY_OR_EXPIRE.value(),
                            CORRELATION_GOOGLE_NOT_VERIFY_OR_EXPIRE.description(), HttpStatus.BAD_REQUEST.value()), httpServletResponse);
                } catch (Exception e) {
                    logger.warn("校验google验证是否有效时将信息写入到response时失败," + e.getMessage());
                }
                logger.error("用户google未校验或者校验失效了");
            }
        }
    }
}

总结

本篇内容介绍了如何绑定及在操作过程中的定时的Google验证器的行为校验及如何检测的处理,下篇将介绍一些工具类和业务强制校验的实现过程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值