前言
在前一篇文章中,我们介绍了用户 MFA(多因素认证)的统一校验过程,即在操作过程中的身份校验。然而,在某些业务场景中可能需要强制校验,以确保操作确实由用户本人执行。为此,我们可以采用切面 + 注解的方式来实现业务操作的强制校验,进一步提升安全性。
与此同时,在某些特定场景下(例如 APP 中的一些校验场景),注解方式可能不够灵活或不太适用。为了解决这些问题,我们还可以提供一个工具类(Util),便于开发者在不同的业务场景中灵活调用。
接下来,我们将围绕以下三个方面进行详细展开:
- 操作过程中的强制校验实现(基于切面 + 注解)。
- 工具类的设计与实现,满足不同场景的灵活调用需求。
- 实际业务场景中的应用与最佳实践。
通过上述方法,我们能够在不同场景下实现高效、灵活的用户校验机制,进一步提升业务操作的安全性和用户体验。
注解类
切面类的属性里默认的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,这也是未来需要优化和扩展的方向。
欢迎大家提出宝贵的建议和意!