1.前言
当今互联网Web各种应用H5、Android、ios、web、小程序等开发时大都采用前后端分离架构,公司为了商业变现会开放自己系统接口给其它公司使用。例如: 调用微信支付。
既然涉及到前后端分离,前端页面调用后端API接口,那么接口的安全设计是非常重要的一项工作。项目的架构师在项目布局过程中,会着重考虑安全,最常见的安全问题就是,用户在移动端提交数据向后端传输,黑客在传输过程中拦截提交的数据,进行篡改,进而达到伪造请求数据的目的。
例如前端提交金额,商品编号信息,黑客中途拦截,修改成低价商品,然后请求下单,早年间国内某电商技术不成熟时,抓包分析下单是很常见的。这时如果我们对一些常规的项目可以通过请求数据报文进行签名、加密、加盐、加时间戳、后端根据数据再次加密,与报文中的签名进行对比是否一致来控制接口安全,这种做法在大厂项目中也是常用手法。
2.什么是加密解密
- 加密:数据加密的基本过程,就是对原来为明文,用户输入的数据通过某种处理,变成一串不可直接提取信息的代码,类似于英文字母加阿拉伯数字组合,通常称之为 密文。在战争年代的电报发报加密成密文,对方电台人员收到电文,根据约定的密码本进行破译便可得到明文,这就是为什么密码本对一个军队如此重要。
- 解密:加密的逆过程,也就是破译电报。
3.常见的加密算法
加密技术通常分为三大类:对称式、非对称式、散列算法。
- 对称式:通俗的说就是锁上一把锁与打开这把锁,用的都是同一样一把钥匙。常见的对称加密算法有:DES、3DES、AES等
非对称式:俗名公开秘钥加密算法,它需要一对代码,一个为公钥 (public key)、另一个为私钥(private key) 加密解密用的不是一个秘钥,所以被称之非对称加密。
- 使用公钥对明文加密,有且只有对应的私钥才能解开密文。
- 使用私钥对明文加密,有且只有对应的公钥才能解开密文。
- 大多数做法:公钥加密,私钥解密,公钥会在加密前发放给解密方。
例子:Git 中ssh连接Github,本地电脑生成public key,与private key,将public key提前配置到GitHub账户中,private key留在本地,上传文件时Git便会自动识别认证身份。
常见的非对称性加密算法:RSA、DSA 等
- 散列算法:主要用于验证,防止信息被修。具体用途如:文件校验、数字签名、鉴权协议。
常见的Hash散列算法:MD5、SHA1、SHA256、HMAC等等 - MD5: MD5是一种不可逆的加密算法,目前是最牢靠的加密算法之一,尚没有能够逆运算的程序被开发出来,它对应任何字符串都可以加密成一段唯一的固定长度的代码。本文采用md5加密算法进行签名加密
4.使用MD5算法开放接口加密验签实现
需求分析:
- 外部应用调用接口,做到极简丝滑调用。
- 接口提供方系统不能影响原有业务。
- 对接口需求方提交的数据进行校验,若不合法在接口被请求前就应终止这一次请求。
- 符合主流大厂接口开放方式。
实现思路:
- 接口提供方给接口需求方也就是第三方公司发放appid、secret,并要求严格保管。在系统内新建一个合作公司表,使用UUID生成appid与secret,对合作公司进行增删改查。简单 在此文章中略
- 接口需求方使用约定的MD5算法将appid应用唯一识别、secret秘钥、timestamp时间戳、nonce随机数、业务参数、生成sign签名并一起传递给接口提供方。
- 接口提供方接收获取appid、secret、timestamp、nonce、并逐个判断是否为空,为空就停止请求,并给第三方友好提示。
- 接口提供方获取第三方公司提交的timestamp与当前系统时间做对比,如果差值大于120秒,则timestamp无效,如果差值小于120秒,则timestamp有效。目的是防止过期的提交。
- 根据第三方提交的appid查询数据库内secret,与提交的secret进行对比,这一步可以根据appid判断权限 高级做法
- 接口提供方获取第三方公司提交的nonce,比较redis中存储的nonce,不一致则通过。防止暴力请求接口。
- 接口提供方将获取的appid、secret、timestamp、nonce、业务参数通过MD5算法运算得到sign2,与第三方公司提交的sign对比,如果不一致则为不合法请求。
- 将nonce存入redis,过期时间设置为120秒。
- 本文会演示采用拦截器和aop切面两种方式来拦截请求
5.上代码
pom.xml 引入依赖 springboot版本:2.3.7.RELEASE
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
</dependencies>
MD5签名算法
package com.wsh.signature.utils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5 {
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String md5(String data) {
StringBuilder sb = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key){
StringBuilder sb = null;
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return sb.toString().toUpperCase();
}
public static void main(String[] args) {
System.out.println(md5("mj1"));
System.out.println(md5("123456"));
}
}
签名是否一致验证工具
package com.wsh.signature.utils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* MD5 sign签名校验宇生成工具
*/
public class GenerateSignatureUtil {
public static final String FIELD_SIGN = "sign";
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key) {
if (!data.containsKey(FIELD_SIGN)) {
return false;
}
String sign = data.get(FIELD_SIGN);
return generateSignature(data, key).equals(sign);
}
public static String generateSignature(final Map<String, String> data, String key) {
try {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(FIELD_SIGN)) {
continue;
}
// 参数值为空,则不参与签名
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("key=").append(key);
return MD5.md5(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static void main(String[] args) {
Map<String, String> data = new HashMap<>();
data.put("user_phone", "18802011391");
data.put("coupon_code", "2167 3908 4433");
// data.put("status","100");
data.put("coupon_type", "A025");
data.put("timestamp", "1531730432");
data.put("partner_id", "84513315");
data.put("sign", "3389ACEBB3AEC93DB89C91614D3CC1C0");
String secret = "JYHS1yWXCW00ore71TXE4aDatAEN6j0G";
System.out.println(generateSignature(data, secret));
System.out.println("校验:" + isSignatureValid(data, secret));
}
}
错误信息提示工具类
package com.wsh.signature.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;
/**
* 客户端工具类
*
* @author ruoyi
*/
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;
}
}
第一种方法重写WebMvcConfigurer注册拦截器,根据配置文件来获取需要签名的就接口地址和不需要签名的地址
package com.wsh.signature.config;
import com.wsh.signature.interceptor.SignAuthInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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 wangsh
* @Date 2023/2/20 16:30
* @Version 1.0
* @deprecated 拦截器配置
*/
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
//需要签名的地址从配置文件取
@Value("${open-sign.interceptor.sign.include-paths}")
private String[] includePaths;
//不需要签名的地址从配置文件取
@Value("${open-sign.interceptor.sign.exclude-paths}")
private String[] excludePaths;
@Autowired
private SignAuthInterceptor signAuthInterceptor;
//注入拦截器
@Bean
public SignAuthInterceptor signAuthInterceptor () {
return new SignAuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册签名拦截器
registry.addInterceptor(signAuthInterceptor())
//配置需要签名的地址
.addPathPatterns(includePaths)
//配置不需要签名的地址
.excludePathPatterns(excludePaths);
//registry.addInterceptor(signAuthInterceptor());
}
}
SignAuthInterceptor 拦截器拦截请求
package com.wsh.signature.interceptor;
import com.alibaba.fastjson.JSON;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.utils.GenerateSignatureUtil;
import com.wsh.signature.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {
private static final String NONCE_KEY_STR = "nonce-";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("进入拦截器");
Map<String, String[]> map = request.getParameterMap();
// 从数组中取出参数放入Map中
Map<String,String> param = new ConcurrentHashMap<>(10);
for (Map.Entry<String, String[]> entry : map.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
for (int i = 0; i < values.length; i++) {
String value = values[i];
param.put(key,value);
}
}
// 1、获取请求参数appId
String appid = param.get("appid");
if (StringUtils.isBlank(appid)) {
log.info("appid不能为空");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("appid不能为空")));
return false;
}
// 2、获取请求参数secret
String secret =request.getParameter("secret");
if (StringUtils.isBlank(secret)){
log.info("secret不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret不能为空")));
return false;
}
/** 3、验证secret权限、来源是否合法
* 此处可以用appId,条件为 已开启,未封禁等进行数据库合作机构表查询,有可能已经终止合作禁止了此有用访问,
* 业务上达到一定条件的可以根据appId分配权限,选择不同的接口能力进行开放
*/
/* TDrivingCooperation drivingCooperationByPartnerkey = drivingCooperationService.getDrivingCooperationByPartnerkey(partnerkey);
if (drivingCooperationByPartnerkey == null || drivingCooperationByPartnerkey.getStatus().equals(StatusEnum.DISABLE.getCode())) {
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("partnerkey无法查询到合作公司信息或已被封禁")));
return false;
}*/
// 获取secret 与数据库值对比,判断请求来源是否合法
/* if (!secret.equals(drivingCooperationByPartnerkey.getSecret())) {
log.debug("secret与接口提供方不一致...........");
System.out.println("secret与接口提供方不一致...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret与接口提供方不一致")));
return false;
}*/
// 4、 获取请求参数timestamp 时间戳,
String timestamp = request.getParameter("timestamp");
if (StringUtils.isBlank(timestamp)){
log.info("timestamp不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp不能为空")));
return false;
}
/** 5、 防止过期时间的提交
* 从前端传递的timestamp 与服务器端当前系统时间之差大于120s,则此次请求的timestamp无效
* 留出短时间考虑网络问题提交速度慢,若时间过长中间时间足以挟持篡改参数,所以折中考虑了120秒
*/
Long time = System.currentTimeMillis()/1000;
if (Math.abs(Long.valueOf(timestamp)-time)>120) {
log.info("timestamp失效...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp失效")));
return false;
}
// 6、获取请求参数nonce随机数,防止重复的暴力请求
String nonce = param.get("nonce");
if (StringUtils.isBlank(nonce)) {
log.debug("nonce不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("nonce不能为空")));
return false;
}
/**
* 如果设计得规范一些可以防止重复提交,我这因为是小项目,Demo演示就不做redis缓存随机数了
* 流程:1、获取当前提交的随机数,作为key前往redis 查询,若有值则为重复提交
* 2、redis中查询不到结果,将当前随机数作为key,value为随机数,过期时间设置为120s
*/
// 7、获取请求sign签名参数,
String sign = param.get("sign");
if (StringUtils.isBlank(sign)){
log.info("sign不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign不能为空")));
return false;
}
//8.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改
boolean reuslt = GenerateSignatureUtil.isSignatureValid(param, secret);
if (!reuslt){
log.debug("sign签名校验失败...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign签名校验失败")));
return false;
}
log.info("签名校验通过,放行...........");
// 获取sign签名,与服务端生成的sign 签名对比
System.out.println("sign ====================");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("SignAuthInterceptor postHandle====== ");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("SignAuthInterceptor afterCompletion====== ");
}
}
application.yml配置文件
server:
port: 8080
open-sign:
interceptor:
# 配置需要进行签名拦截的接口地址
sign:
include-paths: /user/**,/static
exclude-paths: /admin
controller测试接口
package com.wsh.signature.controller;
import com.wsh.signature.annotation.Signature;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@RequestMapping(value = "/add",method = RequestMethod.POST)
public ApiResult<Boolean> addUser (User user) {
log.info("user=="+user);
return ApiResult.ok(true);
}
}
单元测试,这里采用HttpClient 跟接近真实开发方式 先来一次所有参数都正常的请求
- HttpClient工具类HttpClientUtil
package com.wsh.signature.utils;
import com.sun.deploy.net.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpEntity;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/*
* 利用HttpClient进行post请求的工具类
*/
public class HttpClientUtil {
public static String doGet(String url, Map<String, String> param) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
public static String doGet(String url) {
return doGet(url, null);
}
public static String doPost(String url, Map<String, Object> param) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (param != null) {
List<NameValuePair> paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, (String) param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, Charset.forName("UTF-8"));
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
public static String doPost(String url) {
return doPost(url, null);
}
public static String doPostJson(String url, String json) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return resultString;
}
}
单元测试代码
@Test public void testOpenSign() { Map<String, String> params = new ConcurrentHashMap<>(10); String secret = "1ae41230bd1b4383a44f1b114ceba13c"; params.put("appid","6ee781ae6ef4496a"); // 获取时间戳单位S Long timesTamp = System.currentTimeMillis()/1000; System.out.println("time ==" + timesTamp); params.put("timestamp",String.valueOf(timesTamp)); params.put("nonce", UUID.randomUUID().toString()); params.put("secret",secret); params.put("name", "张三"); params.put("address","中国"); params.put("sex","0"); // 调用MD5算法加密生成签名 String signature = GenerateSignatureUtil.generateSignature(params, secret); System.out.println("sign = " + signature); // 签名加入请求参数 params.put("sign",signature); log.info("开始请求open-sign接口==========:{}",params); String result = HttpClientUtil.doPost("http://127.0.0.1:8080/user/add",new HashMap<>(params)); System.out.println(result); }
使用aop切面形式,运用注解来拦截需要
- 自定义注解
package com.wsh.signature.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* 接口签名注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Signature {
/**
* 签名的配置代码
*/
@AliasFor("signatureCode")
String value() default "";
/**
* 签名的配置代码
*/
@AliasFor("value")
String signatureCode() default "";
}
- 签名aop配置类
package com.wsh.signature.aspect;
import com.alibaba.fastjson.JSON;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.utils.GenerateSignatureUtil;
import com.wsh.signature.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* AOP的形式来拦截请求
*/
@Configuration
@Slf4j
@Aspect
public class SignatureAspect {
ThreadLocal<Long> startTime = new ThreadLocal<Long>();
@Pointcut("@annotation(com.wsh.signature.annotation.Signature) " +
"&& (@annotation(org.springframework.web.bind.annotation.RequestMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
"|| @annotation(org.springframework.web.bind.annotation.PatchMapping))")
public void signature() {
}
@Before("signature()")
public boolean doBefore(JoinPoint joinPoint) {
startTime.set(System.currentTimeMillis());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
HttpServletResponse response = attributes.getResponse();
Map<String, String[]> map = request.getParameterMap();
// 从数组中取出参数放入Map中
Map<String,String> param = new ConcurrentHashMap<>(10);
for (Map.Entry<String, String[]> entry : map.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
for (int i = 0; i < values.length; i++) {
String value = values[i];
param.put(key,value);
}
}
// 1、获取请求参数appId
String appid = param.get("appid");
if (StringUtils.isBlank(appid)) {
log.info("appid不能为空");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("appid不能为空")));
return false;
}
// 2、获取请求参数secret
String secret =request.getParameter("secret");
if (StringUtils.isBlank(secret)){
log.info("secret不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret不能为空")));
return false;
}
/** 3、验证secret权限、来源是否合法
* 此处可以用appId,条件为 已开启,未封禁等进行数据库合作机构表查询,有可能已经终止合作禁止了此有用访问,
* 业务上达到一定条件的可以根据appId分配权限,选择不同的接口能力进行开放
*/
/* TDrivingCooperation drivingCooperationByPartnerkey = drivingCooperationService.getDrivingCooperationByPartnerkey(partnerkey);
if (drivingCooperationByPartnerkey == null || drivingCooperationByPartnerkey.getStatus().equals(StatusEnum.DISABLE.getCode())) {
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("partnerkey无法查询到合作公司信息或已被封禁")));
return false;
}*/
// 获取secret 与数据库值对比,判断请求来源是否合法
/* if (!secret.equals(drivingCooperationByPartnerkey.getSecret())) {
log.debug("secret与接口提供方不一致...........");
System.out.println("secret与接口提供方不一致...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret与接口提供方不一致")));
return false;
}*/
// 4、 获取请求参数timestamp 时间戳,
String timestamp = request.getParameter("timestamp");
if (StringUtils.isBlank(timestamp)){
log.info("timestamp不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp不能为空")));
return false;
}
/** 5、 防止过期时间的提交
* 从前端传递的timestamp 与服务器端当前系统时间之差大于120s,则此次请求的timestamp无效
* 留出短时间考虑网络问题提交速度慢,若时间过长中间时间足以挟持篡改参数,所以折中考虑了120秒
*/
Long time = System.currentTimeMillis()/1000;
if (Math.abs(Long.valueOf(timestamp)-time)>120) {
log.info("timestamp失效...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp失效")));
return false;
}
// 6、获取请求参数nonce随机数,防止重复的暴力请求
String nonce = param.get("nonce");
if (StringUtils.isBlank(nonce)) {
log.debug("nonce不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("nonce不能为空")));
return false;
}
/**
* 如果设计得规范一些可以防止重复提交,我这因为是小项目,Demo演示就不做redis缓存随机数了
* 流程:1、获取当前提交的随机数,作为key前往redis 查询,若有值则为重复提交
* 2、redis中查询不到结果,将当前随机数作为key,value为随机数,过期时间设置为120s
*/
// 7、获取请求sign签名参数,
String sign = param.get("sign");
if (StringUtils.isBlank(sign)){
log.info("sign不能为空...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign不能为空")));
return false;
}
//8.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改
boolean reuslt = GenerateSignatureUtil.isSignatureValid(param, secret);
if (!reuslt){
log.debug("sign签名校验失败...........");
ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign签名校验失败")));
return false;
}
log.info("签名校验通过,放行...........");
// 获取sign签名,与服务端生成的sign 签名对比
System.out.println("sign ====================");
return true;
}
@AfterReturning(returning = "ret", pointcut = "signature()")
public void doAfterReturning(Object ret) {
// 处理完请求,返回内容
log.warn("开始响应:RESPONSE: {} ", ret);
log.warn("响应时间: {} ms", System.currentTimeMillis() - startTime.get());
}
}
- 测试方法
package com.wsh.signature.controller;
import com.wsh.signature.annotation.Signature;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@RequestMapping(value = "/list",method = RequestMethod.POST)
@Signature
public ApiResult<Boolean> getList (Order order) {
log.info("order=="+order);
return ApiResult.ok(true);
}
}
- 单元测试
@Test
public void testOpenSign2() {
Map<String, String> params = new ConcurrentHashMap<>(10);
String secret = "1ae41230bd1b4383a44f1b114ceba13c";
params.put("appid","6ee781ae6ef4496a");
// 获取时间戳单位S
Long timesTamp = System.currentTimeMillis()/1000;
System.out.println("time ==" + timesTamp);
params.put("timestamp",String.valueOf(timesTamp));
params.put("nonce", UUID.randomUUID().toString());
params.put("secret",secret);
params.put("name", "张三");
params.put("address","中国");
params.put("sex","0");
// 调用MD5算法加密生成签名
String signature = GenerateSignatureUtil.generateSignature(params, secret);
System.out.println("sign = " + signature);
// 签名加入请求参数
params.put("sign",signature);
log.info("开始请求open-sign接口==========:{}",params);
String result = HttpClientUtil.doPost("http://127.0.0.1:8080/order/list",new HashMap<>(params));
System.out.println(result);
}
6.注意事项
项目中使用了安全框架例如Shiro、SpringSecurity需要提前放开权限校验,否则请求还没有到签名拦截器就被安全框架拦截了,我这项目因为核心是校验签名所以没搭建shiro认证,我们使用签名校验简化了权限校验,不再需要注册账号,通过颁发token方式给第三方公司