记得上下班打卡 | git大法好,push需谨慎

Commit 4b4ac020 authored by 姜秀龙's avatar 姜秀龙

feat(adam): 静默登录 silent_mobile_v2(HMAC 配置密钥)

新增 POST /adam/login/silent_mobile_v2,HMAC-SHA256 验签;v1 DES 保留。路径单段以匹配 login/* 白名单。
Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
parent 5188597e
...@@ -12,6 +12,10 @@ liquidnet: ...@@ -12,6 +12,10 @@ liquidnet:
expire-ttl: 43200 expire-ttl: 43200
refresh-ttl: 525600 refresh-ttl: 525600
blacklist_grace_period: 5 blacklist_grace_period: 5
adam:
# 静默登录 v2,与粉丝俱乐部 A 后端同配,生产请在配置中心设置
silent-otp-v2-secret: CHANGE_ME_SILENT_OTP_V2_SECRET
silent-otp-v2-window-seconds: 10
mysql: mysql:
urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306 urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306
username: zhengzai username: zhengzai
......
...@@ -12,6 +12,9 @@ liquidnet: ...@@ -12,6 +12,9 @@ liquidnet:
expire-ttl: 43200 expire-ttl: 43200
refresh-ttl: 525600 refresh-ttl: 525600
blacklist_grace_period: 5 blacklist_grace_period: 5
adam:
silent-otp-v2-secret: CHANGE_ME_SILENT_OTP_V2_SECRET
silent-otp-v2-window-seconds: 10
mysql: mysql:
urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306 urlHostAndPort: java-test.mysql.polardb.rds.aliyuncs.com:3306
username: zhengzai username: zhengzai
......
...@@ -20,6 +20,7 @@ import com.liquidnet.service.adam.service.AdamRdmService; ...@@ -20,6 +20,7 @@ import com.liquidnet.service.adam.service.AdamRdmService;
import com.liquidnet.service.adam.service.AdamWechatService; import com.liquidnet.service.adam.service.AdamWechatService;
import com.liquidnet.service.adam.service.IAdamUserService; import com.liquidnet.service.adam.service.IAdamUserService;
import com.liquidnet.service.adam.support.AdamUserSessionSupport; import com.liquidnet.service.adam.support.AdamUserSessionSupport;
import com.liquidnet.service.adam.support.SilentMobileOtpV2Support;
import com.liquidnet.service.base.ErrorMapping; import com.liquidnet.service.base.ErrorMapping;
import com.liquidnet.service.base.ResponseDto; import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.UserPathDto; import com.liquidnet.service.base.UserPathDto;
...@@ -75,6 +76,8 @@ public class AdamLoginController { ...@@ -75,6 +76,8 @@ public class AdamLoginController {
AdamWechatService adamWechatService; AdamWechatService adamWechatService;
@Autowired @Autowired
AdamUserSessionSupport adamUserSessionSupport; AdamUserSessionSupport adamUserSessionSupport;
@Autowired
SilentMobileOtpV2Support silentMobileOtpV2Support;
@Value("${liquidnet.reviewer.app-login.mobile}") @Value("${liquidnet.reviewer.app-login.mobile}")
private String reviewMobile; private String reviewMobile;
...@@ -348,7 +351,30 @@ public class AdamLoginController { ...@@ -348,7 +351,30 @@ public class AdamLoginController {
log.error("login by silent for mobile:{},{}/{},{}-{}", mobile, otp, otpDecrypt, l, reql); log.error("login by silent for mobile:{},{}/{},{}-{}", mobile, otp, otpDecrypt, l, reql);
return ResponseDto.failure(ErrorMapping.get("10005")); return ResponseDto.failure(ErrorMapping.get("10005"));
} }
return this.silentMobileLoginSuccess(mobile);
}
@ApiOperationSupport(order = 7)
@ApiOperation(value = "手机号静默登录 v2(HMAC)")
@ApiImplicitParams({
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "mobile", value = "手机号"),
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "otp", value = "HMAC-SHA256 十六进制,64 位"),
})
@PostMapping(value = {"login/silent_mobile_v2"})
public ResponseDto<AdamLoginInfoVo> loginBySilentMobileV2(@Pattern(regexp = "\\d{11}", message = "手机号格式有误")
@NotBlank(message = "手机号不能为空")
@RequestParam String mobile,
@NotBlank(message = "临时票据不能为空")
@RequestParam String otp) {
log.info("login by silent v2 for mobile:{}", mobile);
if (!silentMobileOtpV2Support.verify(mobile, otp)) {
log.error("login by silent v2 otp invalid, mobile:{}", mobile);
return ResponseDto.failure(ErrorMapping.get("10005"));
}
return this.silentMobileLoginSuccess(mobile);
}
private ResponseDto<AdamLoginInfoVo> silentMobileLoginSuccess(String mobile) {
String uid = adamRdmService.getUidByMobile(mobile); String uid = adamRdmService.getUidByMobile(mobile);
boolean toRegister = StringUtils.isEmpty(uid); boolean toRegister = StringUtils.isEmpty(uid);
......
package com.liquidnet.service.adam.support;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
/**
* 静默登录 v2:otp = hex(HMAC-SHA256(secret, mobile + Unix秒UTC)),与 v1 DES 隔离。
*/
@Component
public class SilentMobileOtpV2Support {
private static final String HMAC_SHA256 = "HmacSHA256";
@Value("${liquidnet.adam.silent-otp-v2-secret:}")
private String secret;
@Value("${liquidnet.adam.silent-otp-v2-window-seconds:10}")
private int windowSeconds;
public boolean verify(String mobile, String otp) {
if (StringUtils.isBlank(secret) || StringUtils.isBlank(mobile) || StringUtils.isBlank(otp)) {
return false;
}
if (!otp.matches("^[0-9a-fA-F]{64}$")) {
return false;
}
long now = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long from = now - windowSeconds;
if (from < 0) {
from = 0;
}
byte[] otpBytes = hexToBytes(otp);
if (otpBytes == null) {
return false;
}
for (long ts = from; ts <= now; ts++) {
byte[] expected = sign(mobile, ts);
if (expected != null && MessageDigest.isEqual(expected, otpBytes)) {
return true;
}
}
return false;
}
public String signHex(String mobile, long epochSecondUtc) {
byte[] raw = sign(mobile, epochSecondUtc);
return raw == null ? null : bytesToHex(raw);
}
private byte[] sign(String mobile, long epochSecondUtc) {
try {
String payload = mobile + epochSecondUtc;
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
return mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
return null;
}
}
private static byte[] hexToBytes(String hex) {
if (hex.length() % 2 != 0) {
return null;
}
byte[] out = new byte[hex.length() / 2];
for (int i = 0; i < hex.length(); i += 2) {
out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
}
return out;
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
}
...@@ -112,6 +112,27 @@ public class TestAdam { ...@@ -112,6 +112,27 @@ public class TestAdam {
System.out.println(post); System.out.println(post);
} }
@SneakyThrows
@Test
public void testLoginBySilentMobileV2() {
String mobile = "15811009011";
long ts = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
String secret = System.getenv("LIQUIDNET_ADAM_SILENT_OTP_V2_SECRET");
if (secret == null || secret.isEmpty()) {
System.out.println("skip v2 test: set env LIQUIDNET_ADAM_SILENT_OTP_V2_SECRET");
return;
}
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
mac.init(new javax.crypto.spec.SecretKeySpec(secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal((mobile + ts).getBytes(java.nio.charset.StandardCharsets.UTF_8));
StringBuilder otp = new StringBuilder();
for (byte b : raw) {
otp.append(String.format("%02x", b & 0xff));
}
String post = HttpUtil.post("http://testadam.zhengzai.tv/adam/login/silent_mobile_v2?mobile=" + mobile + "&otp=" + otp, null);
System.out.println(post);
}
@Test @Test
public void testTmp() { public void testTmp() {
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment