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;
}
}