实现开放API接口签名验证

1、新建Certificate类

package com.wise.medical.common.utils;

import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.*;

/**
 * 实现开放API接口签名验证
 * 参考链接:https://www.jianshu.com/p/ad410836587a
 */
public class Certificate {

    public static Logger logger = LoggerFactory.getLogger(LoggerFactory.class);
    /**
     * Certificate构造对象,存储签名相关的参数
     */
    private CertificateBuilder property;
    /**
     * 假设允许客户端和服务端最多能存在15分钟的时间差,同时追踪记录在服务端的nonce集合。
     * 当有新的请求进入时,
     * 首先检查携带的timestamp是否在15分钟内,如超出时间范围,则拒绝,
     * 然后查询携带的nonce,如存在已有集合,则拒绝。否则,记录该nonce,并删除集合内时间戳大于15分钟的nonce
     * (可以使用redis的expire,新增nonce的同时设置它的超时失效时间为15分钟)。
     */
    private String timestamp;
    /**
     * 唯一的随机字符串,用来标识每个被签名的请求
     */
    private String nonce;
    /**
     * 签名
     */
    private String sign;

    public Certificate(CertificateBuilder builder) {
        this.property = builder;
    }

    /**
     * 获取最终请求参数列表
     * 注意:外部不要将timestamp、nonce、accessKey和sign这4个参数添加到方法参数params集合中
     */
    public String getUrlParamsWithSign(final LinkedHashMap<String, String> params) {
        this.sign = getSign(params);

        //6、最终请求Url参数
        params.put("timestamp", this.timestamp);
        params.put("nonce", this.nonce);
        params.put("sign", this.sign);
        params.put("token", property.token);

        List<String> urlParams = new ArrayList<>();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            String strParam = MessageFormat.format("{0}={1}", entry.getKey(), entry.getValue());
            urlParams.add(strParam);
        }
        return String.join("&", urlParams);
    }

    /**
     * 获取签名
     * 注意:外部不要将timestamp、nonce、accessKey和sign这4个参数添加到方法参数params集合中
     */
    public String getSign(final LinkedHashMap<String, String> params) {
        if (this.sign == null || "".equals(this.sign))
        {
            this.sign = createSign(params);
        }
        return this.sign;
    }

    /**
     * 创建签名
     * 注意:外部不要将timestamp、nonce、accessKey和sign这4个参数添加到方法参数params集合中
     */
    private String createSign(final LinkedHashMap<String, String> params) {
        //1、除去空值请求参数
        LinkedHashMap<String, String> newRequestParams = removeEmptyParam(params);

        //2、按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串
        newRequestParams.put("token", property.token);
        String strUrlParams = parseUrlString(newRequestParams);
        logger.info ("strUrlParams === "+strUrlParams);

        //3、生成当前时间戳timestamp=now和唯一随机字符串nonce=random
        strUrlParams = addUniqueParam(strUrlParams);
        logger.info("strUrlParams2 === "+strUrlParams);
        if (strUrlParams.indexOf ("&") == 0){
            strUrlParams = strUrlParams.substring (1);
        }
        //4、最后拼接上Secretkey得到字符串stringSignTemp
        String stringSignTemp = MessageFormat.format("{0}&SecretKey={1}", strUrlParams, property.secretKey);
        logger.info("stringSignTemp === "+stringSignTemp);

        //5、对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为大写,得到sign值
        return DigestUtils.md5Hex(stringSignTemp.getBytes(property.charset)).toUpperCase();
    }

    /**
     * 移除空请求参数
     */
    private LinkedHashMap<String, String> removeEmptyParam(final LinkedHashMap<String, String> params) {
        LinkedHashMap<String, String> newParams = new LinkedHashMap<>();
        if (params == null || params.size() <= 0) {
            return newParams;
        }
        for (String key : params.keySet()) {
            String value = params.get(key);
            if (value == null || value.equals("")){
                continue;
            }
            newParams.put(key, value);
        }
        return newParams;
    }

    /**
     * 使用URL键值对的格式(即key1=value1&key2=value2…)将参数列表拼接成字符串
     */
    private String parseUrlString(final Map<String, String> requestMap) {
        List<String> keyList = new ArrayList<>(requestMap.keySet());
        Collections.sort(keyList);

        List<String> entryList = new ArrayList<>();
        for (String key : keyList) {
            String value = requestMap.get(key);
//            try {
//                value = URLEncoder.encode(value, property.charset.name());
//            } catch (UnsupportedEncodingException e) {
//                e.printStackTrace();
//            }
            entryList.add(MessageFormat.format("{0}={1}", key, value));
        }
        return String.join("&", entryList);
    }

    /**
     * timestamp+nonce方案标识每个被签名的请求
     */
    private String addUniqueParam(final String urlParams) {
        if (property.requestTimestamp == null){
            this.timestamp = String.valueOf(System.currentTimeMillis());
        }else{
            this.timestamp = property.requestTimestamp;
        }

        if (property.requestNonce == null){
            this.nonce = UUID.randomUUID().toString().replaceAll("-", "");
        } else{
            this.nonce = property.requestNonce;
        }

        return MessageFormat.format("{0}×tamp={1}&nonce={2}", urlParams, this.timestamp, this.nonce);
    }

    public static class CertificateBuilder {

        /**
         * 公匙
         */
        private String token;

        /**
         * 私匙
         */
        private String secretKey;

        /**
         * 字符编码
         */
        private Charset charset = StandardCharsets.UTF_8;

        /**
         * 服务端接收到的请求参数
         */
        private String requestTimestamp;

        /**
         * 服务端接收到的请求参数
         */
        private String requestNonce;

        public CertificateBuilder setToken(String token) {
            this.token = token;
            return this;
        }

        public CertificateBuilder setSecretKey(String secretKey) {
            this.secretKey = secretKey;
            return this;
        }

        public CertificateBuilder setCharset(Charset charset) {
            this.charset = charset;
            return this;
        }

        public CertificateBuilder setRequestTimestamp(String requestTimestamp) {
            this.requestTimestamp = requestTimestamp;
            return this;
        }

        public CertificateBuilder setRequestNonce(String requestNonce) {
            this.requestNonce = requestNonce;
            return this;
        }

        public Certificate builder() {
            return new Certificate(this);
        }

    }

}

2、编写拦截器

@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

    public static Logger logger = LoggerFactory.getLogger(LoggerFactory.class);

    @Autowired
    private AppTokenService appTokenService;
    @Autowired
    private SignConfig signConfig;

    public static final String USER_KEY = "userId";
    public static final String LOGIN_TOKEN_KEY = "token";
    /**
     *
     * @param request
     * @return
     */
    private LinkedHashMap<String, String> getHeadersInfo(HttpServletRequest request) {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String> ();
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }

    /**
     *
     * @param request
     * @return
     */
    private LinkedHashMap<String, String> getParameterMap(HttpServletRequest request) {
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
        Enumeration parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String key = (String) parameterNames.nextElement();
            String value = request.getParameter(key);
            map.put(key, value);
        }
        return map;
    }

    /**
     * 功能描述 获取API接口签名实现类
     * @author Hades
     * @date 2020/6/2
     * @param  token token
     * @param  timestamp 当前时间戳
     * @param  nonce 随机字符串
     * @return com.wise.medical.common.utils.Certificate
     */
    private Certificate getCertificate(String token , String timestamp , String nonce) {
        Certificate.CertificateBuilder certificateBuilder = new Certificate.CertificateBuilder ();
        certificateBuilder.setToken (token);
        certificateBuilder.setRequestNonce (nonce);
        certificateBuilder.setRequestTimestamp (timestamp);
        certificateBuilder.setSecretKey (SystemConstant.SECRET_KEY);
        return new Certificate (certificateBuilder);
    }


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Login annotation;
        if(handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
        }else{
            return true;
        }
        if(annotation != null){
            return true;
        }

        //从header中获取token
        String token = request.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if(StringUtils.isBlank(token)){
            token = request.getParameter("token");
        }

        //token为空
        if(StringUtils.isBlank(token)){
            throw new RRException("token不能为空");
        }

        //查询token信息
        AppTokenEntity appTokenEntity = appTokenService.queryByToken(token);
        if(appTokenEntity == null || appTokenEntity.getExpiretime().getTime() < System.currentTimeMillis()){
            throw new RRException("登录已过期,请重新登录",-1);
        }

        //判断签名是否开启
        if (signConfig.getOpen ()){
            logger.info (JSONUtils.beanToJson (getParameterMap(request)));
            logger.info (JSONUtils.beanToJson (getHeadersInfo(request)));
            //获取所有请求参数
            LinkedHashMap<String, String> parameterMap = getParameterMap (request);
            //当前请求时间戳
            String timestamp = parameterMap.get ("timestamp");
            if (timestamp==null){
                throw new RRException ("请求超时");
            }
            long now = System.currentTimeMillis ();
            //判断timestamp是否在规定时间范围内 5分钟 如超出时间范围,则拒绝
            if (now - Long.parseLong (timestamp) >= DateUtils.FIVE_MINUTES){
                throw new RRException ("请求超时");
            }

            //查询携带的随机字符串nonce
            String nonce = parameterMap.get ("nonce");
            if (nonce==null){
                throw new RRException ("请求错误,请检查后再试");
            }
            //从缓存中查找是否有相同请求,,如存在已有集合 ,则拒绝
            String nonceCache = (String) ConcurrentHashMapCacheUtils.getCache(nonce);
            if (nonceCache != null){
                throw new RRException ("请求错误,请检查后再试");
            }else {
                // 否则,记录该nonce,并删除集合内时间戳大于5分钟的nonce,新增nonce的同时设置它的超时失效时间为5分钟
                ConcurrentHashMapCacheUtils.setCache(nonce,nonce,System.currentTimeMillis());
            }

            //获取签名
            String sign = parameterMap.get ("sign");
            if (sign==null){
                throw new RRException ("sign为空");
            }
            Certificate certificate = getCertificate (token , timestamp , nonce);

            //删除timestamp、nonce、token和sign这4个参数
            parameterMap.remove ("timestamp");
            parameterMap.remove ("nonce");
            parameterMap.remove ("sign");
            if (parameterMap.get (LOGIN_TOKEN_KEY)!=null){
                parameterMap.remove ("token");
            }
            //拼接密钥SecretKey
            String signStr = certificate.getSign (parameterMap);
            logger.info (signStr);
            //签名加密进行比对
            if (!sign.equals (signStr)){
                throw new RRException ("请求参数被篡改");
            }
        }

        appTokenService.updateExpireTime(appTokenEntity);
        return true;
    }
}

你可能感兴趣的:(Java)