近年来层出不穷的安全漏洞表明,单单只通过密码不能保障真正的安全,为了进一步增强身份认证的过程本文在校验用户名和密码之后再增加一层基于Goole Authenticator的认证来提高系统的安全性。
操作方式:
- 1、手机应用商店下载Google Authenticator
- 2、调用LoginService的generateGoogleAuthQRCode方法生成二维码
- 3、使用手机打开Google Authenticator扫描该二维码进行绑定,绑定后会每隔30秒生成一次6位动态验证码
- 4、调用LoginService的login方法进行登录(需传入手机上显示的6位动态验证码,否则校验不被通过)
以下分享一次Spring Boot集成Goole Authenticator的案例关键性代码
@Service
public class LoginServiceImpl implements LoginService {
private final UserService userService;
public LoginServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
public UserVo login(LoginParam loginParam, HttpServletRequest servletRequest) {
User user = getUserWithValidatePass(loginParam.getUsername(), loginParam.getPassword());
boolean verification = GoogleAuthenticatorUtils.verification(user.getGoogleAuthenticatorSecret(), loginParam.getMfaCode());
if (!verification) {
throw new BadRequestException("验证码校验失败");
}
servletRequest.getSession().setAttribute("user", user);
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user, userVo);
return userVo;
}
@Override
public String generateGoogleAuthQRCode(String username, String password) {
User user = getUserWithValidatePass(username, password);
String secretKey;
if (StringUtils.isEmpty(user.getGoogleAuthenticatorSecret())) {
secretKey = GoogleAuthenticatorUtils.createSecretKey();
}else {
secretKey = user.getGoogleAuthenticatorSecret();
}
String qrStr;
try(ByteArrayOutputStream bos = new ByteArrayOutputStream()){
String keyUri = GoogleAuthenticatorUtils.createKeyUri(secretKey, username, "Demo_System");
QRCodeUtils.writeToStream(keyUri, bos);
qrStr = Base64.encodeBase64String(bos.toByteArray());
}catch (WriterException | IOException e) {
throw new ServiceException("生成二维码失败", e);
}
if (StringUtils.isEmpty(qrStr)) {
throw new ServiceException("生成二维码失败");
}
user.setGoogleAuthenticatorSecret(secretKey);
userService.updateById(user);
return "data:image/png;base64," + qrStr;
}
private User getUserWithValidatePass(String username, String password) {
String mismatchTip = "用户名或者密码不正确";
// 根据用户名查询用户信息
User user = userService.getByUsername(username)
.orElseThrow(() -> new BadRequestException(mismatchTip));
// 比对密码是否正确
String encryptPassword = SecureUtil.md5(password);
if (!encryptPassword.equals(user.getPassword())) {
throw new BadRequestException(mismatchTip);
}
return user;
}
}
GoogleAuthenticatorUtils提供了生成密钥、校验验证码是否和密钥匹配等功能
public class GoogleAuthenticatorUtils {
private static final int TIME_OFFSET = 0;
public static String createSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
return Base32.encode(bytes).toLowerCase();
}
public static String generateTOTP(String secretKey, long time) {
byte[] bytes = Base32.decode(secretKey.toUpperCase());
String hexKey =HexUtil.encodeHexStr(bytes);
String hexTime = Long.toHexString(time);
return TOTP.generateTOTP(hexKey, hexTime, "6");
}
@SneakyThrows
public static String createKeyUri(String secret, String account, String issuer) throws UnsupportedEncodingException {
String qrCodeStr = "otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}";
ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
mapBuilder.put("account", URLEncoder.encode(account, "UTF-8").replace("+", "%20"));
mapBuilder.put("secret", URLEncoder.encode(secret, "UTF-8").replace("+", "%20"));
mapBuilder.put("issuer", URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
return StringSubstitutor.replace(qrCodeStr, mapBuilder.build());
}
public static boolean verification(String secretKey, String totpCode) {
long time = System.currentTimeMillis() / 1000 / 30;
if (totpCode.equals(generateTOTP(secretKey, time))) {
return true;
}
for (int i = -TIME_OFFSET; i <= TIME_OFFSET; i++) {
if (i != 0) {
if (totpCode.equals(generateTOTP(secretKey, time + i))) {
return true;
}
}
}
return false;
}
}
QRCodeUtils提供了生成二维码的功能,用于用户使用手机APP Google Authenticator扫描二维码进行绑定
public class QRCodeUtils {
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
private static final String FILE_FORMAT = "png";
private static final Map<EncodeHintType, Object> HINTS = new HashMap<EncodeHintType, Object>();
static {
HINTS.put(EncodeHintType.CHARACTER_SET, "utf-8");
HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
HINTS.put(EncodeHintType.MARGIN, 2);
}
public static BufferedImage toBufferedImage(String content) throws WriterException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
return MatrixToImageWriter.toBufferedImage(bitMatrix);
}
public static BufferedImage toBufferedImage(String content, int width, int height)
throws WriterException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
return MatrixToImageWriter.toBufferedImage(bitMatrix);
}
public static void writeToStream(String content, OutputStream stream) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
MatrixToImageWriter.writeToStream(bitMatrix, FILE_FORMAT, stream);
}
public static void writeToStream(String content, OutputStream stream, int width, int height)
throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
MatrixToImageWriter.writeToStream(bitMatrix, FILE_FORMAT, stream);
}
public static void createQRCodeFile(String content, String path) throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, HINTS);
MatrixToImageWriter.writeToPath(bitMatrix, FILE_FORMAT, new File(path).toPath());
}
public static void createQRCodeFile(String content, String path, int width, int height)
throws WriterException, IOException {
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, HINTS);
MatrixToImageWriter.writeToPath(bitMatrix, FILE_FORMAT, new File(path).toPath());
}
}
一次性验证码TOPT类
public class TOTP {
private TOTP() {
}
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);
}
}
private static byte[] hexStr2Bytes(String hex) {
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++) {
ret[i] = bArray[i + 1];
}
return ret;
}
private static final int[] DIGITS_POWER
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
public static String generateTOTP(String key,
String time,
String returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
public static String generateTOTP256(String key,
String time,
String returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
public static String generateTOTP512(String key,
String time,
String returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
public static String generateTOTP(String key,
String time,
String returnDigits,
String crypto) {
int codeDigits = Integer.decode(returnDigits).intValue();
String result = null;
while (time.length() < 16) {
time = "0" + time;
}
byte[] msg = hexStr2Bytes(time);
byte[] k = hexStr2Bytes(key);
byte[] hash = hmac_sha(crypto, k, msg);
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;
}
}
写在最后:如果需要完整Demo的小伙伴可以留言联系