❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
记得点赞、收藏、评论⭐️⭐️⭐️
认真学习!!!
为了保证安全性,不能让任何人都能调用接口。那么,我们如何在后端实现签名认证呢?我们需要两个东西,即 accessKey 和 secretKey。这和用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求。这样,即使你之前没来过,只要这次的状态正确,你就可以调用接口。所以我们需要这两个东西来标识用户。
他的本质就是签发签名,使用签名。同时需要运用到加密算法,对ak,sk加密成签名。
参数 1:accessKey:调用的标识 (复杂、无序、无规律)
参数 2:secretKey:密钥(复杂、无序、无规律)
参数 3:用户请求参数
参数 4:sign(加密的算法)
参数 5:加 nonce 随机数,只能用一次。服务端要保存用过的随机数,防止重放请求
参数 6:加 timestamp 时间戳,校验时间戳是否过期。防止重放请求。
防止重放作用
每个请求在发送时携带一个时间戳,后端会验证该时间戳是否在指定的时间范围内,例如不超过10分钟或5分钟。这可以防止对方使用昨天的请求在今天进行重放。通过这种方式,我们可以一定程度上控制随机数的过期时间。因为后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端会拒绝该请求。因此,时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下,这两种方法可以相互配合使用。
1.每个用户在注册的时候,就需要创建ak,sk
/**
* 盐值,混淆密码
*/
private static final String SALT = "123";
public long userRegister(String userAccount, String userPassword, String checkPassword) {
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短");
}
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短");
}
// 密码和校验密码相同
if (!userPassword.equals(checkPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致");
}
synchronized (userAccount.intern()) {
// 账户不能重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userAccount", userAccount);
long count = userMapper.selectCount(queryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//3.分配accessKey,secretKey
//使用DigestUtil.md5Hex将盐值、用户账户和5或8位随机数进行MD5加密
String accessKey = DigestUtil.md5Hex(SALT+userAccount+ RandomUtil.randomNumbers(5));
String secretKey = DigestUtil.md5Hex(SALT+userAccount+ RandomUtil.randomNumbers(8));
// 4. 插入数据
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
user.setAccessKey(accessKey);
user.setSecretKey(secretKey);
boolean saveResult = this.save(user);
if (!saveResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误");
}
return user.getId();
}
}
在我们注册之中,首先使用MD5对密码进行盐值加密。之后就是生成用户专属的ak,sk。利用DigestUtil加密算法,将盐值,用户账号,随机数进行MD5加密生成,这样就能生成用户专属的签名。
加密算法种类相当多。对称加密、非对称加密、md5 签名(不可解密)等等。我们在这里使用的是SHA256算法的Digester。当然使用其他的算法都是可以的,因为到时候验证签名的时候,只需要再加密一遍,得到的ak1,sk1和库里面加密后的ak,sk一样,即代表着验证成功,
public class SignUtils {
/**
* 生成签名
* @param body
* @param secretKey 密钥
* @return 生成的签名字符串
*/
public static String genSign(String body, String secretKey) {
// 使用SHA256算法的Digester
Digester md5 = new Digester(DigestAlgorithm.SHA256);
// 构建签名内容,将哈希映射转换为字符串并拼接密钥
String content = body + "." + secretKey;
// 计算签名的摘要并返回摘要的十六进制表示形式
return md5.digestHex(content);
}
}
将加密算法写成一个公共类,用的时候直接调用即可。
当用户调用接口时,就应该将ak,sk,以及timestamp时间戳加到请求头之中。使用加密算法生成签名。最后发送请求。
// 获取当前登录用户的ak和sk,这样相当于用户自己的这个身份去调用,
// 也不会担心它刷接口,因为知道是谁刷了这个接口,会比较安全
User loginUser = userService.getLoginUser(request);
String accessKey = loginUser.getAccessKey();
String secretKey = loginUser.getSecretKey();
JjlApiClient jjlApiClient = new JjlApiClient(accessKey, secretKey);
/**
* 通过请求方法获取http响应
* @param request 要求
* @return {@link HttpResponse}
* @throws ApiException 业务异常
*/
private <O, T extends ResultResponse> HttpRequest getHttpRequestByRequestMethod(BaseRequest<O, T> request) throws ApiException {
if (ObjectUtils.isEmpty(request)) {
throw new ApiException(ErrorCode.OPERATION_ERROR, "请求参数错误");
}
String path = request.getPath().trim();
String method = request.getMethod().trim().toUpperCase();
if (ObjectUtils.isEmpty(method)) {
throw new ApiException(ErrorCode.OPERATION_ERROR, "请求方法不存在");
}
if (StringUtils.isBlank(path)) {
throw new ApiException(ErrorCode.OPERATION_ERROR, "请求路径不存在");
}
log.info("请求方法:{},请求路径:{},请求参数:{}", method, path, request.getRequestParams());
HttpRequest httpRequest;
switch (method) {
case "GET": {
httpRequest = HttpRequest.get(path);
break;
}
case "POST": {
httpRequest = HttpRequest.post(path);
break;
}
default: {
throw new ApiException(ErrorCode.OPERATION_ERROR, "不支持该请求");
}
}
//添加请求头,发送请求,
return httpRequest.addHeaders(getHeaders(JSONUtil.toJsonStr(request), jjlApiClient)).body(JSONUtil.toJsonStr(request.getRequestParams()));
}
//添加请求头
private Map<String, String> getHeaders(String body, JjlApiClient jjlApiClient) {
Map<String, String> hashMap = new HashMap<>(4);
hashMap.put("accessKey", jjlApiClient.getAccessKey());
String encodedBody = SecureUtil.md5(body);
hashMap.put("body", encodedBody);
hashMap.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
hashMap.put("sign", SignUtils.genSign(encodedBody, jjlApiClient.getSecretKey()));
return hashMap;
}
因为请求接口的链接不同,做验证比较麻烦的话,我们可以写一个网关,将所有请求进行拦截。统一验证之后,再调用接口。这个之后篇章在实现。本文主要是介绍API签字认证功能。
// 首先从请求头中获取参数
HttpHeaders headers = request.getHeaders();
String accessKey = headers.getFirst("accessKey");
String nonce = headers.getFirst("nonce");
String timestamp = headers.getFirst("timestamp");
String sign = headers.getFirst("sign");
String body = headers.getFirst("body");
/获取当前用户
User invokeUser = null;
try{
invokeUser = innerUserService.getInvokeUser(accessKey);
}catch (Exception e){
log.error("getInvokeUser error",e);
}
if(invokeUser == null){
return handleNoAuth(response);
}
// 直接校验如果随机数大于1万,则抛出异常,并提示"无权限"
if (Long.parseLong(nonce) > 10000) {
return handleNoAuth(response);
}
// 时间和当前时间不能超过5分钟
// 首先,获取当前时间的时间戳,以秒为单位
Long currentTime = System.currentTimeMillis() / 1000;
final Long FIVE_MINUTES = 60 * 5L;
if ((currentTime - Long.parseLong(timestamp)) >= FIVE_MINUTES) {
return handleNoAuth(response);
}
//从库中查询出sk,然后利用加密算法得出签名,两者一直则代表验证完成
String secretKey = invokeUser.getSecretKey();
String serverSign = SignUtils.genSign(body,secretKey);
if (sign == null || !sign.equals(serverSign)) {
return handleNoAuth(response);
}
验证完之后就可以调用接口。返回接口值。
作为开发者,每次调用接口都需要处理这一堆繁琐的事情,这确实有些麻烦,需要自己生成时间戳,编写签名算法,生成随机数等等,这些都是相当繁琐的工作。因此,构建接口开放平台时,需要想办法让开发者能够以最简单的方式调用接口。开发者只需要关心传递哪些参数以及他们的密钥、APP等信息。一旦告诉了他们这些信息,他们就可以轻松地进行调用了。
对于具体的随机数生成和签名生成过程,开发者有必要关心吗?显然是不需要的。因此,我们需要为开发者提供一个易于使用的 SDK,使其能够便捷地调用接口。因此接下来会发布文章来教大家怎么开发SDK