什么是 2FA(双因素身份验证)?

双因素身份验证(2FA)是一种安全系统,要求用户提供两种不同的身份验证方式才能访问某个系统或服务。国内普遍做短信验证码这种的用的比较少,不过在国外的网站中使用双因素身份验证的还是很多的。用户通过使用验证器扫描二维码,就能在app上获取登录的动态口令,进一步加强了账户的安全性。

主要步骤

pom.xml中增加依赖

<!-- 用于SecureKey生成 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- 二维码依赖 -->
<dependency>
<groupId>org.iherus</groupId>
<artifactId>qrext4j</artifactId>
<version>1.3.1</version>
</dependency>

用户表中增加secretKey列

为用户绑定secretKey字段,用以生成二维码及后期校验

工具类

谷歌身份验证器工具类


/**
* 谷歌身份验证器工具类
*/
public class GoogleAuthenticator { /**
* 时间前后偏移量
* 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致
* 如果为0,当前时间为 10:10:15
* 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过
* 如果为1,则表明在
* 10:09:30-10:10:00
* 10:10:00-10:10:30
* 10:10:30-10:11:00 之间生成的TOTP 能校验通过
* 以此类推
*/
private static int WINDOW_SIZE = 0; /**
* 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
*/
private static final String CRYPTO = "HmacSHA1"; /**
* 生成密钥,每个用户独享一份密钥
*
* @return
*/
public static String getSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
String secretKey = base32.encodeToString(bytes);
// make the secret key more human-readable by lower-casing and
// inserting spaces between each group of 4 characters
return secretKey.toUpperCase();
} /**
* 生成二维码内容
*
* @param secretKey 密钥
* @param account 账户名
* @param issuer 网站地址(可不写)
* @return
*/
public static String getQrCodeText(String secretKey, String account, String issuer) {
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
try {
return "otpauth://totp/"
+ URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, "UTF-8").replace("+", "%20")
+ "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20")
+ (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20")) : "");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
} /**
* 获取验证码
*
* @param secretKey
* @return
*/
public static String getCode(String secretKey) {
String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
Base32 base32 = new Base32();
byte[] bytes = base32.decode(normalizedBase32Key);
String hexKey = Hex.encodeHexString(bytes);
long time = (System.currentTimeMillis() / 1000) / 30;
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6", CRYPTO);
} /**
* 检验 code 是否正确
*
* @param secret 密钥
* @param code code
* @param time 时间戳
* @return
*/
public static boolean checkCode(String secret, long code, long time) {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
// convert unix msec time into a 30 second "window"
// this is per the TOTP spec (see the RFC for details)
long t = (time / 1000L) / 30L;
// Window is used to check codes generated in the near past.
// You can use this value to tune how far you're willing to go.
long hash;
for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
try {
hash = verifyCode(decodedKey, t + i);
} catch (Exception e) {
// Yes, this is bad form - but
// the exceptions thrown would be rare and a static
// configuration problem
// e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
if (hash == code) {
return true;
}
}
return false;
} /**
* 根据时间偏移量计算
*
* @param key
* @param t
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] data = new byte[8];
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO);
Mac mac = Mac.getInstance(CRYPTO);
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
// We're using a long because Java hasn't got unsigned int.
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
// We are dealing with signed bytes:
// we just keep the first byte.
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return truncatedHash;
} public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
String secretKey = getSecretKey();
System.out.println("secretKey:" + secretKey);
String code = getCode(secretKey);
System.out.println("code:" + code);
boolean b = checkCode(secretKey, Long.parseLong(code), System.currentTimeMillis());
System.out.println("isSuccess:" + b);
}
}
}

二维码工具类

/**
* 验证码生成工具类
*/
public class TOTP { private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; /**
* This method uses the JCE to provide the crypto algorithm. HMAC computes a
* Hashed Message Authentication Code with the crypto hash algorithm as a
* parameter.
*
* @param crypto : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
* @param keyBytes : the bytes to use for the HMAC key
* @param text : the message or text to be authenticated
*/
private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
} /**
* This method converts a HEX string to Byte[]
*
* @param hex : the HEX string
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex) {
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); // Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
System.arraycopy(bArray, 1, ret, 0, ret.length);
return ret;
} /**
* This method generates a TOTP value for the given set of parameters.
*
* @param key : the shared secret, HEX encoded
* @param time : a value that reflects a time
* @param returnDigits : number of digits to return
* @param crypto : the crypto function to use
* @return: a numeric String in base 10 that includes
*/
public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
int codeDigits = Integer.decode(returnDigits);
String result = null; // Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16) {
time = "0" + time;
} // Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] k = hexStr2Bytes(key);
byte[] hash = hmac_sha(crypto, k, msg); // put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf; int binary = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); int otp = binary % DIGITS_POWER[codeDigits]; result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
}

Service

@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
@Service
public class TwoFAService {
@Autowired
private UserMapper userMapper; /**
* 获取SecureKey
*/
public String getSecureKey(Integer userId) {
User user = userMapper.selectUserById(userId);
return user.getSecretKey();
} /**
* 更新secureKey
*/
public Integer updateSecureKey(Integer userId, String secureKey) {
return userMapper.updateSecureKeyById(userId, secureKey);
} /**
* 校验动态码
*/
public boolean chek2FACode(User user, String twoFACode) throws Exception {
String secretKey = user.getSecretKey();
// 没绑定设备就先验证通过
if(secretKey == null || secretKey.isEmpty()) {
return true;
} else {
if(twoFACode.isEmpty()) { throw new Exception("已绑定设备,请输入动态码"); }
boolean checkRes = GoogleAuthenticator.checkCode(secretKey, Long.parseLong(twoFACode), System.currentTimeMillis());
if(!checkRes) {
throw new Exception("动态码错误");
} else {
return true;
}
}
}
}

Controller

用户登录中增加两步验证:

@Controller
@RequestMapping(value = "/mgr")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private LogService logService;
@Autowired
private TwoFAService twoFAService; /**
* @Description: 用户登录
*/
@RequestMapping(value = "/user/login", method = RequestMethod.POST)
@ResponseBody
public GlobalResult login(String userCode, String userPwd, String twoFACode) {
try {
UsernamePasswordToken token = new UsernamePasswordToken(userCode, userPwd);
Subject subject = SecurityUtils.getSubject();
subject.login(token); // 2FA验证
User user = (User) subject.getPrincipal();
twoFAService.chek2FACode(user, twoFACode); Log log = new Log();
.......
}
}
}

两步验证的Controler

@RestController
@RequestMapping(value = "/2fa")
public class TwoFAController {
@Autowired
private TwoFAService twoFAService; /**
* 生成二维码信息对象
*/
@GetMapping("/getQrcode")
public QrCodeResponse getQrcode(@RequestParam("userId") Integer userId, @RequestParam("userCode") String userCode, HttpServletResponse response) throws Exception {
try {
String secretKey = twoFAService.getSecureKey(userId);
QrCodeResponse qrCodeResponse = new QrCodeResponse();
if(secretKey == null || secretKey.isEmpty()) {
secretKey = GoogleAuthenticator.getSecretKey();
qrCodeResponse.setBind(false);
// userMapper.updateSecureKeyById(userId, secretKey);
} else {
qrCodeResponse.setBind(true);
} // 生成二维码内容
String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "suggest-mgr");
// 以流的形式返回生成二维码输出
// new SimpleQrcodeGenerator().generate(qrCodeText).toStream(response.getOutputStream());
BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
// 将图片转换为Base64字符串
String base64Image = convertImageToBase64(image);
qrCodeResponse.setQrCodeText(secretKey);
qrCodeResponse.setBase64Image(base64Image); return qrCodeResponse;
} catch (Exception e) {
// 处理异常
e.printStackTrace();
return null; // 或者返回适当的错误信息
}
} /**
* 更新SecretKey
* @param userId
* @param secretKey
*/
@GetMapping("/updateSecretKey")
public void updateSecretKey(@RequestParam("userId") Integer userId, @RequestParam("secretKey") String secretKey) {
twoFAService.updateSecureKey(userId, secretKey);
} /**
* 获取新的secretKey 重置用
* @param userId
* @param userCode
* @return
*/
@GetMapping("/getNewSecretKey")
public QrCodeResponse getNewSecretKey(@RequestParam("userId") Integer userId, @RequestParam("userCode") String userCode, HttpServletResponse response) throws Exception {
try {
String secretKey = secretKey = GoogleAuthenticator.getSecretKey();
QrCodeResponse qrCodeResponse = new QrCodeResponse();
qrCodeResponse.setBind(false); // 生成二维码内容
String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, userCode, "xxx-site");
BufferedImage image = new SimpleQrcodeGenerator().generate(qrCodeText).getImage();
// 将图片转换为Base64字符串
String base64Image = convertImageToBase64(image);
qrCodeResponse.setQrCodeText(secretKey);
qrCodeResponse.setBase64Image(base64Image); // 返回包含qrCodeText和Base64编码图片的信息
return qrCodeResponse;
} catch (Exception e) {
// 处理异常
e.printStackTrace();
return null; // 或者返回适当的错误信息
}
} /**
* 将图片文件流转为base64
*/
private String convertImageToBase64(BufferedImage image) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
} catch (Exception e) {
// 处理异常
return "";
}
} static public class QrCodeResponse {
private String secretKey;
private String base64Image;
private boolean isBind; public String getSecretKey() {
return secretKey;
} public void setSecretKeyt(String secretKey) {
this.secretKey = secretKey;
} public String getBase64Image() {
return base64Image;
} public void setBase64Image(String base64Image) {
this.base64Image = base64Image;
} public boolean isBind() {
return isBind;
} public void setBind(boolean bind) {
isBind = bind;
}
}
}

常用2FA验证工具

  1. Google Authenticator: google play, apple store
  2. Microsoft Authenticator: google play , apple store
  3. AuthenticatorPro(开源):https://github.com/jamie-mh/AuthenticatorPro

SpringBoot项目添加2FA双因素身份认证的更多相关文章

  1. 操作系统(AIX)双因素身份认证解决方案-中科恒伦CKEY DAS

      一.场景分析 操作系统是管理计算机硬件与软件资源的计算机程序,用于工作中的进程管理.存储管理.设备管理.文件管理.作业管理等,十分重要,安全等级极高! 二.问题分析 1.密码设置简单,非常容易被撞 ...

  2. PyPI提供双因素身份验证(2FA),已提高下载安全性

    前天,Python的核心开发团队宣布PyPI现在提供双因素身份验证(2FA),以提高Python包下载的安全性,从而降低未经授权的帐户访问的风险.该团队宣布将在Python Package Index ...

  3. (诊断)解决GitHub使用双因子身份认证“Two-Factor Athentication”后无法git push 代码的“fatal: Authentication failed for ...”错误

    在GitHub上采取双因子身份认证后,在git push 的时候将会要求填写用户的用户名和密码,用户名就是用户在GitHub上申请的用户名,但是密码不是普通登录GitHub的密码. 一旦采取双因子身份 ...

  4. SpringBoot学习:整合shiro(身份认证和权限认证),使用EhCache缓存

    项目下载地址:http://download.csdn.NET/detail/aqsunkai/9805821 (一)在pom.xml中添加依赖: <properties> <shi ...

  5. 为springboot项目添加springboot-admin监控

    我们知道spring-boot-actuator暴露了大量统计和监控信息的端点,spring-boot-admin 就是为此提供的监控项目. 先来看看大概会提供什么样的功能 从图中可以看出,主要内容都 ...

  6. springboot项目添加jsp支持

    一.创建springboot项目 使用 http://start.spring.io/ 快速创建一个springboot项目下载并导入 二.添加依赖 在pom.xml中添加支持jsp的依赖如下: &l ...

  7. IDEA中springboot项目添加yml格式配置文件

    1.先创建application.properties 文件,在resources文件夹,右键 new -> Resource Bundle  如下图所示,填写名称 2.生成如下图所示文件 3. ...

  8. springboot 项目添加jaeger调用链监控

    1.添加maven依赖<dependency> <groupId>io.opentracing.contrib</groupId> <artifactId&g ...

  9. springboot项目添加swagger2

    1.pom中添加swagger依赖 <!-- swagger-ui --> <dependency> <groupId>io.springfox</group ...

  10. ASP.NET Core & 双因素验证2FA 实战经验分享

    必读 本文源码核心逻辑使用AspNetCore.Totp,为什么不使用AspNetCore.Totp而是使用源码封装后面将会说明. 为了防止不提供原网址的转载,特在这里加上原文链接: https:// ...

随机推荐

  1. MYSQL 同步到ES 如何设计架构保持一致性

    简单使用某个组件很容易,但是一旦要搬到生产上就要考虑各种各样的异常,保证你方案的可靠性,可恢复性就是我们需要思考的问题.今天来聊聊我们部门在 MYSQL 同步到ES的方案设计. 在面对复杂条件查询时, ...

  2. tableau 工作表分页

    原创优阅达数据科技有限公司 https://mp.weixin.qq.com/s?__biz=MzA5MTU3NDI2NQ==&mid=2649465570&idx=1&sn= ...

  3. kingbase ES group by 语句优化

    1.group by 分组语句 在SQL中group by主要用来进行分组统计,分组字段放在group by的后面:分组结果一般需要借助聚合函数实现. group by语法结构 1.常用语法 语法结构 ...

  4. 2022福州大学MEM复试英语口语准备

    一.自我介绍 Dear professors, it's my honor to be here for my interview. My name is ,I finished my undergr ...

  5. Python列表list 分片实例

    1 a = list(range(10)) 2 print(a[::]) #复制一个列表 3 print(a[::2]) #每隔2个取一次 4 print(a[::3]) #每隔3个取一次 5 6 p ...

  6. 遵循这些MySQL设计规范,再也没被组长喷过

    分享是最有效的学习方式. 博客:https://blog.ktdaddy.com/ 故事 会议室里,小猫挠着头,心里暗暗叫苦着"哎,这代码都撸完了呀,改起来成本也太大了." 原来就 ...

  7. HarmonyOS音频开发指导:使用AudioRenderer开发音频播放功能

      AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处理,更适合有音频开发经验的开发者,以 ...

  8. HarmonyOS API Version 7版本特性说明

    2020年9月11日,HarmonyOS SDK发布了首个Beta版本,支持基于HarmonyOS的华为智慧屏.智能穿戴.车机设备开发,让广大的开发者正式步入了HarmonyOS应用开发之旅. 开发者 ...

  9. redis 简单整理——主从拓扑图[二十二]

    前言 Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性 可以分为以下三种:一主一从.一主多从.树状主从结构,下面分别介绍. 正文 一主一从结构 一主一从结构是最简单的复制拓扑结构,用 ...

  10. flask售后评分系统

    做软件行业的公司,一般都有专业的售前售后团队,还有客服团队,客服处理用户反馈的问题,会形成工单,然后工单会有一大堆工单流程,涉及工单的内部人员,可能会有赔付啥的,当然,这是有专业的售前.售后.客服团队 ...