Java API接口签名认证

2022-09-14 10:39:25

Java API接口签名认证

我们在进行程序开发的时候,一定会开发一些API接口,供他人访问。当然这些接口中有可能是开放的,也有可能是需要登录才能访问的,也就是需要Token鉴权成功后才可以访问的。那么问题来了,我们这些开放的接口,难道不是一直暴露在外吗?该如何来保证这些接口的安全性呢?
本编文章,将通过API接口签名认证的方式来解决以上问题。

什么是接口签名认证

这个可以看成,比如,我申请了微信公众号或者小程序,公众号的基本信息中就会包含AppId和AppSecret两个数据。这两个数据需要我们用户进行保存,尤其是AppSecret更不能暴露在外,以保证安全。当我们需要请求微信进行授权登录的时候,我们需要根据微信的规则拼接请求链接,其中请求链接中会包含AppId,AppSecret等的一些信息,通过按规则拼接好的链接则可以成功请求微信,否则会请求失败。
而我们要做的就是根据微信的这个原理,实现自己程序的API接口签名验证。

接口签名参数规则

需要在每次请求的header中携带以下参数:

  1. appKey:相当于appId,一个请求来源的标识。
  2. sign:签名,由签名规则计算而来。
  3. t:时间戳,通过计算此时间戳与服务器当前时间差来防止请求重放问题。

签名(sign)计算公式、规则:

sign=MD5(data+AppSecret+t)
其中data为请求参数的拼接,其规则如下:

  1. path传参形式:如/api/user/{userId}/{mobile},单个或多个参数,按地址中参数的位置排序。则data=userId+mobile的字符串拼接。
  2. 对象形式传参,即json形式:需要按对象中的属性进⾏字典升序排序,然后对其属性值按此顺序进⾏拼接。如User类如下
@DataclassUser{private String userId;private String mobile;}

则data的计算为:userId,mobile两个属性名按字典排序。顺序为mobile->userId,如果mobile=17612345678;userId=123,则data的拼接顺序为 17612345678123。而sign=MD5(17612345678123+AppSecret+t)。问号拼接参数同理。
3. list形式传参:需要将list⾥⾯的内容进⾏依次拼接。

代码实现

以下代码中包含一些自己造的工具类,如AssertUtils,LocalCacheUtils等。

import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.List;import java.util.Map;import java.util.Optional;import java.util.TreeMap;import java.util.stream.Stream;@Slf4j@ComponentpublicclassVerifySignUtils{@Autowiredprivate RedisRepository redisRepository;// 设置请求重放的时间差privatefinallong SIGN_EXPIRE=1000*10;publicbooleanverifySign(String appKey, String sign, String t, Object object){// 参数判空,校验
        log.info("传入的时间戳:{}", t);if(null== appKey){
            log.error("没有传入appKey");returnfalse;}long now= System.currentTimeMillis();// 校验请求重放if(Long.parseLong(t)< now- SIGN_EXPIRE){
            log.error("sign失效!传入时间戳{};当前时间戳:{}", t, now);returnfalse;}// 取出缓存的AppId和AppSecret配置表,因为是对接多个程序,是以配置表的形式实现的
        Map<Integer, String> appInfos= Optional.ofNullable(LocalCacheUtils.get(BaseConstants.APP_CACHE_NAME)).map(it->(Map<Integer, String>) it).orElseGet(()->(Map<Integer, String>) LocalCacheUtils.setExpire(BaseConstants.APP_CACHE_NAME, Optional.ofNullable(redisRepository.get(BaseConstants.APP_CACHE_NAME)).map(it->(Map<Integer, String>) it).orElseGet(null),
                                BaseConstants.LOCAL_CACHE_APP_INFO_EXPIRE));
        String secret= appInfos.get(Integer.parseInt(appKey));if(null== secret){
            log.error("appKey错误");returnfalse;}// 根据请求,计算拼接参数,即data的拼接
        String objectFields;if(objectinstanceofObject[]){
            StringBuilder builder=newStringBuilder();for(Object o:((Object[]) object)){
                builder.append(o);}
            objectFields= builder.toString();}elseif(objectinstanceofList){
            StringBuilder builder=newStringBuilder();((List) object).forEach(it-> builder.append(getObjectFields(it)));
            objectFields= builder.toString();}elseif(objectinstanceofString|| objectinstanceofLong|| objectinstanceofInteger|| objectinstanceofBoolean){
            objectFields= object.toString();}else{
            objectFields=getObjectFields(object);}
        log.info("参数按顺序拼接:{}", objectFields);// 计算sign签名的值
        String tempSign= Md5Utils.getMD5((objectFields+ secret+ t).getBytes()).toUpperCase();
        log.info("计算出的sign:{}\t传入的sign:{}", tempSign, sign);// 校验传入的签名和服务端计算的签名是否一致,不一致则,签名认证失败if(!tempSign.equals(sign)){
            log.error("计算验签与传入的验签不符");returnfalse;}returntrue;}// 以下为通过反射拼接对象参数的方法private StringgetObjectFields(Object object){final Field[] fields= object.getClass().getDeclaredFields();final TreeMap<String, Object> treeMap=newTreeMap<>();
        Stream.of(fields).map(Field::getName).forEach(it-> treeMap.put(it,getFieldValueByName(it, object)));final StringBuilder builder=newStringBuilder();
        treeMap.forEach((k, v)-> builder.append(v));return builder.toString();}private ObjectgetFieldValueByName(String fieldName, Object o){try{
            String firstLetter= fieldName.substring(0,1).toUpperCase();
            String getter="get"+ firstLetter+ fieldName.substring(1);final Method method= o.getClass().getMethod(getter,newClass[]{});final Object value= method.invoke(o,newObject[]{});if(null== value){return"";}if(valueinstanceofList){return((List) value).stream().map(it->{if(itinstanceofString|| itinstanceofLong|| itinstanceofInteger|| itinstanceofBoolean){return it;}else{returngetObjectFields(it);}}).reduce((it1, it2)-> it1+""+ it2).get().toString();}return value;}catch(Exception e){return"";}}}

以上代码并不固定,也可根据自己的签名规则进行改造配置。

调用代码如下,通过对Controller配置AOP的形式进行调用:

@Aspect@Slf4j@ConfigurationpublicclassLogRecordAspect{@Autowiredprivate VerifySignUtils verifySignUtils;@Autowiredprivate AopLogUtil aopLogUtil;@Value("${spring.profiles.active}")private String active;privatestaticfinal List<String> activeList= Arrays.asList("prod","test");

    ThreadLocal<Long> startTime=newThreadLocal<Long>();@Pointcut("execution(* com.xx.xx.xx.controller..*.*(..))")publicvoidwebLog(){}@Before("webLog()")publicvoiddoBefore(JoinPoint joinPoint){
        startTime.set(System.currentTimeMillis());
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request= attributes.getRequest();
        List<Object> collect= aopLogUtil.verifySignLog(joinPoint);
        String appKey= request.getHeader("appKey");
        String sign= request.getHeader("sign");
        String t= request.getHeader("t");boolean flag;// get请求与其它请求进行区分,如果有delete,put请求,需要另加if("GET".equals(request.getMethod())){
            flag= verifySignUtils.verifySign(appKey, sign, t, collect.toArray());}else{
            flag= verifySignUtils.verifySign(appKey, sign, t, collect.get(0));}if(!flag&& activeList.contains(active)){
            log.error("验签失败!");thrownewBaseException(R.SERVICE_VERIFY_SIGN_ERROR,"");}}@AfterReturning(returning="ret", pointcut="webLog()")publicvoiddoAfterReturning(Object ret){// 处理完请求,返回内容
        log.warn("开始响应:RESPONSE: {} ", ret);
        log.warn("响应时间: {} ms", System.currentTimeMillis()- startTime.get());}}
  • 作者:wFitting
  • 原文链接:https://blog.csdn.net/wFitting/article/details/109672944
    更新时间:2022-09-14 10:39:25