spring_boot集成MFA

一、maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.taimos</groupId>
<artifactId>totp</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>

二、代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

import de.taimos.totp.TOTP;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;

import java.security.SecureRandom;

public class MFAUtil {

private MFAUtil () {
throw new IllegalStateException("Utility class");
}

/**
* 生成MFA密钥
*/
public static String generateSecretKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
return base32.encodeToString(bytes);
}

/**
* 获取TOTP验证码
*/
public static String getTOTPCode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
return TOTP.getOTP(hexKey);
}

/**
* 校验用户输入的验证码是否正确
* @param secretKey MFA密钥
* @param userCode 用户输入的验证码
* @return true 为 通过 false 为不通过
*/
public static boolean verifyCode(String secretKey, String userCode) {
String totpCode = getTOTPCode(secretKey);
return totpCode.equals(userCode);
}

/**
* 获取OTP认证URL
* @param issuer 应用名称比如github.com
* @param account 用户名
* @param secretKey 生成的密钥
* @return 认证URL
*/
public static String getOtpAuthUrl(String issuer, String account, String secretKey) {
return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, account, secretKey, issuer);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class QrCodeUtil {

public static byte[] generateQrCode(String text, int width, int height) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return outputStream.toByteArray();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/mfa")
public class MfaController {


@GetMapping("/generate")
public Map<String, String> generateSecretKey() {
String secretKey = MFAUtil.generateSecretKey();
Map<String, String> response = new HashMap<>();
response.put("secretKey", secretKey);
return response;
}

@GetMapping("/qr-code")
public ResponseEntity<byte[]> generateQrCode(@RequestParam String secretKey) {
try {
String issuer = "combine";
String account = "unicmp";
String otpAuthUrl = MFAUtil.getOtpAuthUrl(issuer, account, secretKey);
byte[] qrCodeBytes = QrCodeUtil.generateQrCode(otpAuthUrl, 200, 200);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
return new ResponseEntity<>(qrCodeBytes, headers, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping("/verify")
public Map<String, Boolean> verifyCode(@RequestParam String secretKey, @RequestParam String userCode) {
boolean isValid = MFAUtil.verifyCode(secretKey, userCode);
Map<String, Boolean> response = new HashMap<>();
response.put("isValid", isValid);
return response;
}
}
1
2
3
4
5
6
7
8
9
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MfaApplication {
public static void main(String[] args) {
SpringApplication.run(MfaApplication.class, args);
}
}

三、使用

  • /mfa/generate:生成一个新的 MFA 密钥。
  • /mfa/qr-code:根据传入的密钥生成对应的二维码图片。
  • /mfa/verify:验证用户输入的一次性密码。

真正使用时,看用户表有没有secretKey,如果没有,生成一个secretKey并返回QR码给用户用于APP扫码,拿着用户绑定的secretKey和app的OTP验证,验证通过后返回token。


spring_boot集成MFA
http://hanqichuan.com/2025/04/07/spring/spring_boot集成MFA/
作者
韩启川
发布于
2025年4月7日
许可协议