如何在项目中应用“API签名认证”

❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
记得点赞、收藏、评论⭐️⭐️⭐️
认真学习!!!

文章目录

  • 为什么需要API签字认证?
  • API签名认证需要的参数
  • API签名认证实现
    • 1.用户注册创建ak,sk
    • 2.加密算法
    • 3.发送调用接口请求
    • 4.验证签名是否正确
  • 总结如何改善开发:

 
  最近在做一个API开放平台,因为开发者可以上传接口到这个平台上面,然后用户可以浏览平台进行付费次数调用。但值得思考的是,如果用户量很大,或者攻击者疯狂请求这个接口,这会导致安全问题,同时也会耗尽服务器性能,影响正常用户的使用。
  因此需要进行保护措施,所以我使用的API签名认证进行权限认证。

为什么需要API签字认证?

  为了保证安全性,不能让任何人都能调用接口。那么,我们如何在后端实现签名认证呢?我们需要两个东西,即 accessKey 和 secretKey。这和用户名和密码类似,不过每次调用接口都需要带上,实现无状态的请求。这样,即使你之前没来过,只要这次的状态正确,你就可以调用接口。所以我们需要这两个东西来标识用户。
  他的本质就是签发签名,使用签名。同时需要运用到加密算法,对ak,sk加密成签名。

 

API签名认证需要的参数

参数 1:accessKey:调用的标识 (复杂、无序、无规律)
参数 2:secretKey:密钥(复杂、无序、无规律)
参数 3:用户请求参数
参数 4:sign(加密的算法)
参数 5:加 nonce 随机数,只能用一次。服务端要保存用过的随机数,防止重放请求
参数 6:加 timestamp 时间戳,校验时间戳是否过期。防止重放请求。
 
防止重放作用
  每个请求在发送时携带一个时间戳,后端会验证该时间戳是否在指定的时间范围内,例如不超过10分钟或5分钟。这可以防止对方使用昨天的请求在今天进行重放。通过这种方式,我们可以一定程度上控制随机数的过期时间。因为后端需要同时验证这两个参数,只要时间戳过期或随机数被使用过,后端会拒绝该请求。因此,时间戳可以在一定程度上减轻后端保存随机数的负担。通常情况下,这两种方法可以相互配合使用。

 

API签名认证实现

 

1.用户注册创建ak,sk

  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加密生成,这样就能生成用户专属的签名。
 

2.加密算法

  加密算法种类相当多。对称加密、非对称加密、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);
    }
}

将加密算法写成一个公共类,用的时候直接调用即可。

 

3.发送调用接口请求

当用户调用接口时,就应该将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;
 }

 

4.验证签名是否正确

  因为请求接口的链接不同,做验证比较麻烦的话,我们可以写一个网关,将所有请求进行拦截。统一验证之后,再调用接口。这个之后篇章在实现。本文主要是介绍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

你可能感兴趣的:(API签字认证,java,服务器,API签名认证,登录权限验证,加密算法)