Java API接口签名认证

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类如下
@Data
class User{
    
    
    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
@Component
public class VerifySignUtils {
    
    

    @Autowired
    private RedisRepository redisRepository;
   // 设置请求重放的时间差
    private final long SIGN_EXPIRE = 1000 * 10;

    public boolean verifySign(String appKey, String sign, String t, Object object) {
    
    
    // 参数判空,校验
        log.info("传入的时间戳:{}", t);
        if (null == appKey) {
    
    
            log.error("没有传入appKey");
            return false;
        }
        long now = System.currentTimeMillis();
        // 校验请求重放
        if (Long.parseLong(t) < now - SIGN_EXPIRE) {
    
    
            log.error("sign失效!传入时间戳{};当前时间戳:{}", t, now);
            return false;
        }
        // 取出缓存的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错误");
            return false;
        }
        // 根据请求,计算拼接参数,即data的拼接
        String objectFields;
        if (object instanceof Object[]) {
    
    
            StringBuilder builder = new StringBuilder();
            for (Object o : ((Object[]) object)) {
    
    
                builder.append(o);
            }
            objectFields = builder.toString();
        } else if (object instanceof List) {
    
    
            StringBuilder builder = new StringBuilder();
            ((List) object).forEach(it -> builder.append(getObjectFields(it)));
            objectFields = builder.toString();
        } else if (object instanceof String || object instanceof Long || object instanceof Integer || object instanceof Boolean) {
    
    
            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("计算验签与传入的验签不符");
            return false;
        }
        return true;
    }
	// 以下为通过反射拼接对象参数的方法
    private String getObjectFields(Object object) {
    
    
        final Field[] fields = object.getClass().getDeclaredFields();
        final TreeMap<String, Object> treeMap = new TreeMap<>();
        Stream.of(fields).map(Field::getName).forEach(it -> treeMap.put(it, getFieldValueByName(it, object)));
        final StringBuilder builder = new StringBuilder();
        treeMap.forEach((k, v) -> builder.append(v));
        return builder.toString();
    }

    private Object getFieldValueByName(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, new Class[]{
    
    });
            final Object value = method.invoke(o, new Object[]{
    
    });
            if (null == value) {
    
    
                return "";
            }
            if (value instanceof List) {
    
    
                return ((List) value).stream().map(it -> {
    
    
                    if (it instanceof String || it instanceof Long || it instanceof Integer || it instanceof Boolean) {
    
    
                        return it;
                    } else {
    
    
                        return getObjectFields(it);
                    }
                }).reduce((it1, it2) -> it1 + "" + it2).get().toString();
            }
            return value;
        } catch (Exception e) {
    
    
            return "";
        }
    }
}

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

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

@Aspect
@Slf4j
@Configuration
public class LogRecordAspect {
    
    

    @Autowired
    private VerifySignUtils verifySignUtils;

    @Autowired
    private AopLogUtil aopLogUtil;

    @Value("${spring.profiles.active}")
    private String active;

    private static final List<String> activeList = Arrays.asList("prod","test");

    ThreadLocal<Long> startTime = new ThreadLocal<Long>();

    @Pointcut("execution(* com.xx.xx.xx.controller..*.*(..))")
    public void webLog() {
    
    
    }

    @Before("webLog()")
    public void doBefore(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("验签失败!");
            throw new BaseException(R.SERVICE_VERIFY_SIGN_ERROR, "");
        }
    }

    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) {
    
    
        // 处理完请求,返回内容
        log.warn("开始响应:RESPONSE: {} ", ret);
        log.warn("响应时间: {} ms", System.currentTimeMillis() - startTime.get());
    }
}

猜你喜欢

转载自blog.csdn.net/wFitting/article/details/109672944