Access Token
Access Token
API 通信的token校验机制
参考:https://blog.csdn.net/CSDN_WYL2016/article/details/124872582
安全措施
timestamp
timestamp 请求方的时间戳,在服务端,校验此时间戳,超过5分钟的请求,可以直接德育。可以用来防止同一个请求参数被无限期的使用
nonce
nonce值是一个由接口请求方生成的随机数,在有需要的场景中,可以用它来实现请求一次性有效,也就是说同样的请求参数只能使用一次,这样可以避免接口重放攻击
接口请求方每次请求都会随机生成一个不重复的nonce值,接口提供方可以使用一个存储容器(redis),每次先在容器中看看是否存在接口请求方发来的nonce值, 如果不存在则表明是第一次请求,则放行,并且把当前nonce值保存到容器中,> 这样,如果下次再使用同样的nonce来请求则容器中一定存在,那么就可以判定是无效请求了。 timestamp 已经保证了请求有效期,此处缓存可以配置成 timestamp 有效期一致
其他安全机制
- 白名单
- 黑名单
- 限流、熔断、降级
功能设计
- 管理后台维护 access_token 信息,包含 app_id, app_secret 和附属相关信息
- app_id, app_secret 信息缓存至 app 中,在 GW, 或 Interceptor 中, 客户端请求过来,提取 access_token, 完成各种校验,最后放行
- 客户端,在请求服务端时,计算,并附加 access_token 信息
access token 管理
- 生成 access token
- 分发 access token 到可能作为服务端的应用中,以便在拦截器中获取 token 信息
- 缓存机制:另新文章介绍
加密演示
客户端配置
- 配置信息从服务提供方获取
yaml
cas:
sdk:
app-id: appId
app-secret: appSecret
加密解密
java
public class SignDemo {
/**
* 获取签名
*/
public String getSign() {
String appId = casSdkConfig.getAppId();
String appSecret = casSdkConfig.getAppSecret();
long timestamp = System.currentTimeMillis();
String nonce = SecretUtil.md5(UUID.randomUUID().toString() + timestamp);
// 生成签名
Map<String, String> data = new HashMap<>();
data.put("appId", appId);
data.put("nonce", nonce);
data.put("timestamp", timestamp + "");
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
// 排序
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
// 参数值为空,则不参与签名
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
return encrypt(appSecret, sb.substring(0, sb.length() - 1));
}
/**
* 解析并校验签名
*/
public boolean deSign(String appId, String sign) {
if (StringUtils.isBlank(appId) || StringUtils.isBlank(sign)) {
return false;
}
AppInfo appInfo = appInfoCache.get(casSdkConfig.getAppCode());
Map<String, AccessToken> accessTokens = appInfo.getAccessTokens();
AccessToken accessToken = accessTokens.get(appId);
if (accessToken == null) {
logger.error("appId: {} 不存在, 无法解密", appId);
return false;
}
// 签名验证
String decrypt;
try {
decrypt = decrypt(accessToken.getAppPublicKey(), sign);
} catch (Exception e) {
logger.error("appId: {} 解密失败", appId);
return false;
}
// 获取信息进行详情验证
String[] split = decrypt.split("&");
Map<String, String> data = new HashMap<>();
for (String s : split) {
String[] t = s.split("=");
if (t.length == 2) {
data.put(t[0], t[1]);
}
}
String appId2 = data.get("appId");
String timestamp = data.get("timestamp");
String nonce = data.get("nonce");
// 验证 appId1 是否被偷换
if (appId2 == null) {
logger.error("签名中缺少 appId: {}! ", appId);
return false;
}
if (!appId2.equals(appId)) {
logger.error("请求appId: {}, 验证appId: {} 不匹配,验证失败! ", appId, appId2);
return false;
}
// 验证 timestamp 是否已过期
if (timestamp == null) {
logger.error("签名中缺少 timestamp: {}! ", appId);
return false;
}
long l = Long.parseLong(timestamp);
if (System.currentTimeMillis() - l > 5 * 60 * 1000) {
logger.error("请求已过期,appId: {}, : timestamp: {}! ", appId, timestamp);
return false;
}
// 验证 nonce 是否重复请求
if (nonce == null) {
logger.error("签名中缺少 nonce: {}! ", appId);
return false;
}
String key = "cas:access:" + nonce;
boolean lock = redisLockHelper.lock(key, 5 * 60);
if (!lock) {
logger.error("重复的请求 appId: {}, nonce: {} ", appId, nonce);
return false;
}
return true;
}
}
最后放几个使用到的函数
java
public class SignDemo {
private static String encrypt(String privateKeyStr, String plainText) {
try {
byte[] keyBytes = SecretUtil.base64Decode(privateKeyStr);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory factory = KeyFactory.getInstance("RSA", "SunRsaSign");
PrivateKey privateKey = factory.generatePrivate(spec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
try {
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
} catch (InvalidKeyException e) {
//For IBM JDK, 原因请看解密方法中的说明
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey;
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPrivateExponent());
Key fakePublicKey = KeyFactory.getInstance("RSA").generatePublic(publicKeySpec);
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, fakePublicKey);
}
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
String encryptedString = SecretUtil.base64Encode(encryptedBytes);
return encryptedString;
} catch (Exception e) {
throw new RuntimeException("加密计算出现异常", e);
}
}
private static String decrypt(String publicKeyStr, String cipherText) {
try {
PublicKey publicKey = getPublicKey(publicKeyStr);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
try {
cipher.init(Cipher.DECRYPT_MODE, publicKey);
} catch (InvalidKeyException e) {
// 因为 IBM JDK 不支持私钥加密, 公钥解密, 所以要反转公私钥
// 也就是说对于解密, 可以通过公钥的参数伪造一个私钥对象欺骗 IBM JDK
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
RSAPrivateKeySpec spec = new RSAPrivateKeySpec(rsaPublicKey.getModulus(), rsaPublicKey.getPublicExponent());
Key fakePrivateKey = KeyFactory.getInstance("RSA").generatePrivate(spec);
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, fakePrivateKey);
}
if (cipherText == null || cipherText.length() == 0) {
return cipherText;
}
byte[] cipherBytes = SecretUtil.base64Decode(cipherText);
byte[] plainBytes = cipher.doFinal(cipherBytes);
return new String(plainBytes);
} catch (Exception e) {
throw new RuntimeException("解密计算出现异常", e);
}
}
private static PublicKey getPublicKey(String publicKeyStr) {
try {
byte[] publicKeyBytes = SecretUtil.base64Decode(publicKeyStr);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "SunRsaSign");
return keyFactory.generatePublic(x509KeySpec);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to get public key", e);
}
}
}