最近做了一个开放平台接口的工程,我的接口只有一个为【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; }
其他代码出于安全考虑,但是不贴来。需要的可以发邮件