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


    最近做了一个开放平台接口的工程,我的接口只有一个为【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;
    
    }
    

      

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

  • 相关阅读:
    element-ui日期筛选:选择日期即触发查询
    js点击按钮复制内容到粘贴板
    axios配置及使用(发起请求时带上token)
    axios + vue导出excel文件
    textarea与标签组合,点击标签填入标签内容,再次点击删除内容(vue)
    vue复制textarea文本域内容到粘贴板
    ElementUI动态表格数据转换formatter
    elementUI图片墙上传
    高德地图模糊搜索地址(elementUI)
    elementUI表单验证
  • 原文地址:https://www.cnblogs.com/woshuaile/p/14153956.html
Copyright © 2020-2023  润新知