开放平台接口安全问题,接口验签

最近做了一个开放平台接口的工程,我的接口只有一个为【post】代码如下:

所有的参数放在body请求体内,所以验签有点复杂。放header里会简单很多。下面代码解决了body参数io流不可重复读取的问题。

思路可以看这文章:

https://www.jianshu.com/p/ad410836587a


获取post请求里的body参数可以参考:

https://blog.csdn.net/weixin_44560245/article/details/90700720


a  拦截器

package application.handler;

import application.enums.SignTypeEnum;
import application.utils.DateUtils;
import application.utils.RedisUtils;
import application.utils.ServletUtils;
import application.utils.SignUtil;
import application.wrapper.RequestWrapper;
import com.alibaba.fastjson.JSON;

import com.alibaba.fastjson.JSONObject;
import com.fadada.core.common.remote.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @desc: API请求报文签名sign
 * @author: kql
 * @date: 2020-05-18 14:54
 */
@Slf4j
//@Component
public class SignAuthInterceptor implements HandlerInterceptor {

    private static final String NONCE_KEY = "x-nonce-";

//我写死了一个appId
private static final String APP_ID = "XXX"; private String ErrorCode = "-1";
//写死的密钥
private static String APP_KEY = "XXXXX"; private static int size=32; @Autowired private RedisUtils redisUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { RequestWrapper requestWrapper = new RequestWrapper(request); String body = requestWrapper.getBodyString(); JSONObject jsonObject = JSONObject.parseObject(body); String thirdAppId = jsonObject.getString("thirdAppId"); if (StringUtils.isBlank(thirdAppId)) { log.error("appId不能为空..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "thirdAppId不能为空"))); return false; } else { //TODO 验证 appID是否存在 后续接入appID的查询 if (!APP_ID.equals(thirdAppId)) { log.error("appId非法或者不存在"); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "thirdAppId非法或者不存在"))); return false; } } //加密方式 String signType = jsonObject.getString("signType"); SignTypeEnum signTypeEnum = SignTypeEnum.getSignType(signType); if (null == signTypeEnum) { log.error("签名加密暂时只支持SHA256、SHA1和MD5"); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "签名加密暂时只支持SHA256、SHA1和MD5"))); return false; } //时间戳 String timestampStr = jsonObject.getString("timestamp"); if (StringUtils.isBlank(timestampStr)) { log.error("timestamp不能为空..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "timestamp不能为空"))); return false; } //参数签名 String sign = jsonObject.getString("sign"); if (StringUtils.isBlank(sign)) { log.error("sign不能为空..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "sign不能为空"))); return false; } String nonce = jsonObject.getString("nonce"); if (StringUtils.isBlank(nonce)) { log.error("nonce不能为空..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "nonce不能为空"))); return false; } //随机数非法 if (size!=StringUtils.length(nonce)) { log.error("nonce位数应该为32位"); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "nonce位数应该为32位"))); return false; } //1.前端传过来的时间戳与服务器当前时间戳差值大于180,则当前请求的timestamp无效 if (DateUtils.isTimeOut(timestampStr)) { log.debug("timestamp无效..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "timestamp无效"))); return false; } //2.通过判断redis中的nonce,确认当前请求是否为重复请求,控制API接口幂等性 boolean nonceExists = redisUtils.hasKey(nonce); if (nonceExists) { log.debug("nonce重复..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "重复的请求"))); return false; } //3.通过后台重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改 String bizContent = jsonObject.getString("bizContent"); String signEncrypt = SignUtil.getSign(thirdAppId, APP_KEY, signType, timestampStr, bizContent,nonce); if (!(sign.equals(signEncrypt))) { log.debug("sign签名校验失败..........."); ServletUtils.renderString(response, JSON.toJSONString(Result.error(ErrorCode, "sign签名校验失败"))); return false; } //4.将nonce存进redis redisUtils.set(NONCE_KEY + nonce, nonce, 120); log.debug("签名校验通过,放行..........."); //5.放行 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }

2  拦截器注入

package application.config;

import application.handler.SignAuthInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 01
 * @program wrapper-demo
 * @description
 * @create 2018-12-24 21:16
 * @since 1.0
 **/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Bean
    public SignAuthInterceptor getSignatureInterceptor(){
        return new SignAuthInterceptor();
    }

    /**
     * 注册拦截器
     *
     * @param registry registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getSignatureInterceptor())
                .addPathPatterns("/**");
    }
}

3 过滤器:

package application.filter;

import application.wrapper.RequestWrapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @author 01
 * @program wrapper-demo
 * @description 替换HttpServletRequest
 * @create 2018-12-24 21:04
 * @since 1.0
 **/
@Slf4j
public class ReplaceStreamFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("StreamFilter初始化...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {
        log.info("StreamFilter销毁...");
    }
}

  4  过滤器注入

package application.config;

import application.filter.ReplaceStreamFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

/**
 * @author 01
 * @program wrapper-demo
 * @description 过滤器配置类
 * @create 2018-12-24 21:06
 * @since 1.0
 **/
@Configuration
public class FilterConfig {
    /**
     * 注册过滤器
     *
     * @return FilterRegistrationBean
     */
    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(replaceStreamFilter());
        registration.addUrlPatterns("/*");
        registration.setName("streamFilter");
        return registration;
    }

    /**
     * 实例化StreamFilter
     *
     * @return Filter
     */
    @Bean(name = "replaceStreamFilter")
    public Filter replaceStreamFilter() {
        return new ReplaceStreamFilter();
    }
}

  5 重写 

HttpServletRequestWrapper
package application.wrapper;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * @author 01
 * @program wrapper-demo
 * @description 包装HttpServletRequest,目的是让其输入流可重复读
 * @create 2018-12-24 20:48
 * @since 1.0
 **/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
    /**
     * 存储body数据的容器
     */
    private final byte[] body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        // 将body数据存储起来
        String bodyStr = getBodyString(request);
        body = bodyStr.getBytes(Charset.defaultCharset());
    }

    /**
     * 获取请求Body
     *
     * @param request request
     * @return String
     */
    public String getBodyString(final ServletRequest request) {
        try {
            return inputStream2String(request.getInputStream());
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取请求Body
     *
     * @return String
     */
    public String getBodyString() {
        final InputStream inputStream = new ByteArrayInputStream(body);

        return inputStream2String(inputStream);
    }

    /**
     * 将inputStream里的数据读取出来并转换成字符串
     *
     * @param inputStream inputStream
     * @return String
     */
    private String inputStream2String(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;

        try {
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    log.error("", e);
                }
            }
        }

        return sb.toString();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

  6 servlet 工具类

package application.utils;

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * 客户端工具类
 *
 */
public class ServletUtils {

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取session
     */
    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    /**
     * 获取ServletRequestAttributes
     */
    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderResultString(ServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


}

  8  加密工具类

package application.utils;


import application.constant.GlobalConstants;
import application.enums.SignTypeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * 签名工具类
 *
 * @author zhangq2@fadada.com
 * @version 1.0.0
 * @date 2018/12/4
 */
@Transactional(rollbackFor = Exception.class)
public class SignUtil {

    private static final Logger logger = LoggerFactory.getLogger(SignUtil.class);


    /**
     * 根据数据获取签名
     *
     * @param appId
     * @param appKey
     * @param signType
     * @param timestamp
     * @param bizContent
     * @return java.lang.String
     * @author zhangq2@fadada.com
     * @date 2019/1/2
     */
    public static String getSign(String appId, String appKey, String signType,
                                 String timestamp, String bizContent,String nonce) {
        String sign = "";
        try {
            Map<String, Object> map = new HashMap<>(10);
            //注意 这里是openAPI给op的id  切记 切记
            map.put("thirdAppId", appId);
            map.put("signType", signType);
            map.put("timestamp", timestamp);
            map.put("bizContent", bizContent);
            map.put("nonce", nonce);
            List<String> list = new ArrayList<>(map.keySet());
            Collections.sort(list);
            StringBuilder builder = new StringBuilder();
            for (String key : list) {
                Object value = map.get(key);
                if (null != value && !"".equals(value)) {
                    builder.append(key).append("=").append(value).append("&");
                }
            }
            String content = builder.substring(0, builder.length() - 1);
            logger.info("getSign content:{}, appKey:{}", content, appKey);
            switch (SignTypeEnum.valueOf(signType)) {
                case SHA256:
                    String sha256 = CryptTool.sha256(CryptTool.sha256(content) + appKey);
                    sign = CryptTool.encodeBase64String(sha256.getBytes(GlobalConstants.DEFAULT_CHARSET));
                    break;
                case SHA1:
                    String sha1 = CryptTool.sha1(CryptTool.sha1(content) + appKey);
                    sign = CryptTool.encodeBase64String(sha1.getBytes(GlobalConstants.DEFAULT_CHARSET));
                    break;
                case MD5:
                    String md5 = CryptTool.md5(CryptTool.md5(content) + appKey);
                    sign = CryptTool.encodeBase64String(md5.getBytes(GlobalConstants.DEFAULT_CHARSET));
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            logger.error("生成签名错误 ==> ", e);
        }
        return sign.trim();
    }


}

  9 redis工具类

package application.utils;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.concurrent.TimeUnit;

/**
 * Redis工具类
 */
@Component
public class RedisUtils {
    @Autowired
    @Qualifier("opStringKeyRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 默认过期时长,单位:秒
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;

    /**
     * 不设置过期时长
     */
    public final static long NOT_EXPIRE = -1;


    /**
     * 插入对象
     *
     * @param key   键
     * @param value 值
     * @author zmr
     */
    public void setObject(String key, Object value) {
        set(key, value, DEFAULT_EXPIRE);
    }

    /**
     * 删除缓存
     *
     * @param key 键
     * @author zmr
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }


    /**
     * 返回指定类型结果
     *
     * @param key   键
     * @param clazz 类型class
     * @return
     * @author zmr
     */
    public <T> T get(String key, Class<T> clazz) {
        String value = get(key);
        return value == null ? null : fromJson(value, clazz);
    }

    /**
     * Object转成JSON数据
     */
    public String toJson(Object object) {
        if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
                || object instanceof Boolean || object instanceof String) {
            return String.valueOf(object);
        }
        return JSON.toJSONString(object);
    }

    /**
     * JSON数据,转成Object
     */
    private <T> T fromJson(String json, Class<T> clazz) {
        return JSON.parseObject(json, clazz);
    }


    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public String get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key).toString();
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     *              94
     * @param value 值
     *              95
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     *              111
     * @param value 值
     *              112
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     *              113
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

  启动类处加入redis的注入:


@Bean(
name = {"opStringKeyRedisTemplate"}
)
public RedisTemplate<String, Object> globalStringRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(factory);
CustomPrefixStringRedisSerializer customPrefixStringRedisSerializer = new CustomPrefixStringRedisSerializer("op-cloud-service:");
Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setKeySerializer(customPrefixStringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

10 DTO:

package application.bean;

import application.validate.TimeValid;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotEmpty;

/**
 * 通用参数
 * @author: 
 * @date: 2020/12/14 10:44
 * @description: TODO
 */

@Data
public class OpCommonDto {

    @ApiModelProperty(value = "", required = true)
    @NotEmpty(message = "[thirdAppId]不能为空")
    @Length(max = 10, min = 10, message = "[third_appId]不合法")
    private String thirdAppId;


    @ApiModelProperty(value = "请求的url", required = true)
    @NotEmpty(message = "[sign]不能为空")
    private String url;

    @ApiModelProperty(value = "请求接口的加密参数,参数格式参考对应的中台接口文档", required = true)
        private String bizContent;


    /**
     * 请求参数的签名
     */
    @ApiModelProperty(value = "请求参数的签名",required = true)
    @NotEmpty(message = "[sign]不能为空")
    private String sign;


    /**
     * 签名算法类型,如RSA2、RSA或者MD5等。目前只支持MD5
     */
    @ApiModelProperty(value = "签名算法类型,如RSA2、RSA或者MD5等。目前只支持MD5",required = true)
    private String signType;



    /**
     * 发送请求的时间,格式:yyyy-MM-dd HH:mm:ss
     */
    @ApiModelProperty(value = "发送请求的时间,格式:yyyy-MM-dd HH:mm:ss",required = true)
    @NotEmpty(message = "[timestamp]请求时间不能为空")
    @TimeValid(message = "[timestamp]请求时间格式不对,正确的格式是:yyyy-MM-dd HH:mm:ss")
    private String timestamp;



    /**
     * 随机字符串
     */
    @ApiModelProperty(value = "随机字符串",required = true)
    @NotEmpty(message = "[随机字符串]不能为空")
    @Length(max = 32, min = 32, message = "[随机字符串]不合法")
    private String nonce;

}

  

其他代码出于安全考虑,但是不贴来。需要的可以发邮件

原文地址:https://www.cnblogs.com/woshuaile/p/14153956.html