一、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"); }
public static String generateSecretKey() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[20]; random.nextBytes(bytes); Base32 base32 = new Base32(); return base32.encodeToString(bytes); }
public static String getTOTPCode(String secretKey) { Base32 base32 = new Base32(); byte[] bytes = base32.decode(secretKey); String hexKey = Hex.encodeHexString(bytes); return TOTP.getOTP(hexKey); }
public static boolean verifyCode(String secretKey, String userCode) { String totpCode = getTOTPCode(secretKey); return totpCode.equals(userCode); }
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。