最近做了一个开放平台接口的工程,我的接口只有一个为【post】代码如下:
所有的参数放在body请求体内,所以验签有点复杂。放header里会简单很多。下面代码解决了body参数io流不可重复读取的问题。
思路可以看这文章:
https://www.jianshu.com/p/ad410836587a
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;
}
其他代码出于安全考虑,但是不贴来。需要的可以发邮件