API接口签名验证,防止请求参数被篡改。
机制:前端利用请求参数+时间戳对参数进行加密,生成签名,将签名传给后端,后端通过同样的方式进行生成签名,判断签名是否一致。不一致则为非法请求。
1.前端(VUE)实现
1.1 前端生成签名工具类
生成签名工具,signatureUtil.js
// signatureUtil.js
import md5 from "js-md5";
export function signatureGenerate({data, url}){
// 参数签名 密钥 + token + 时间戳 + url
// 密钥,可以是前后端约定好的某一串,也可以是随机的,动态的。
let secret = "123123"
// 时间戳
let timestamp = new Date().getTime()
//当前用户的登陆token
let token ="456456";
// post参数
let dataStr = dataSerialize(dataSort(data))
// 生成签名
let str = dataStr + "secret=" + secret +"&token="+token+ "×tamp=" + timestamp + "&url=" + url
const sign = md5(str)
return {
signature: sign.toUpperCase(), // 将签名字母转为大写
timestamp,
secret
}
}
// 参数排序
function dataSort(obj){
if (JSON.stringify(obj) == "{}" || obj == null) {
return {}
}
let key = Object.keys(obj)?.sort()
let newObj = {}
for (let i = 0; i < key.length; i++) {
newObj[key[i]] = obj[key[i]]
}
return newObj
}
// 参数序列化
function dataSerialize(sortObj){
let strJoin = ''
for(let key in sortObj){
strJoin += key + "=" + sortObj[key] + "&"
}
// return strJoin.substring(0, strJoin.length - 1)
return strJoin
}
1.2 request请求拦截
// 导入axios
import axios from 'axios'
import {signatureGenerate} from "./signatureUtil"
// 通过axios.create方法创建一个axios实例,用request接收
const request = axios.create({
// 指定请求的根路径
baseURL: '/lisw-test'
})
// 请求拦截器
request.interceptors.request.use((config) => {
alert("拦截");
// 获取请求头参数
const {signature, timestamp} = signatureGenerate(config)
// 分别将签名、密钥、时间戳 至请求头
if(signature) config.headers["signature"] = signature
if(timestamp) config.headers["timestamp"] = timestamp
return config
});
export default request
2. 后端(SpringBoot)实现
后端通过拦截器或者AOP切面进行拦截请求进行校验签名是否合法,这里使用拦截器进行实现。
2.1 增加拦截器
参与加密的参数,前后端一定要一一对应,否则即使通过相同的加密算法,出来的签名也不一样。自然无法校验通过。
package com.lisw.test.liswtest.signature.interceptor;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import cn.hutool.json.JSONUtil;
import com.lisw.test.liswtest.signature.utils.RequestUtil;
import com.lisw.test.liswtest.signature.utils.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @author lisw
* @program lisw-test
* @description 签名拦截器
* @createDate 2022-09-19 15:40:59
**/
@Component
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取 signature
String signature = request.getHeader("signature");
log.info("前端生成的签名 = {}",signature);
if(StringUtils.isEmpty(signature)){
ResponseUtil.write(response, "该请求无签名信息", 400, false);
return false;
}
// 获取 timestamp
String timestamp = request.getHeader("timestamp");
if(StringUtils.isEmpty(timestamp)){
ResponseUtil.write(response, "无时间信息", 400, false);
return false;
}
Long aLong = Long.valueOf(timestamp);
boolean b = SignUtil.verifyTimestamp(aLong);
if(!b){
ResponseUtil.write(response, "请求超时", 408, false);
return false;
}
// 获取 secret
String secret = "123123";
// 获取 url
// 因为get请求的参数都在url上,直接对url加密就好了
String servletPath = request.getServletPath();
String queryString = request.getQueryString();
String url;
if(queryString != null){
url = servletPath + "?" + queryString;
}else{
url = servletPath;
}
// 获取请求方法
String method = request.getMethod();
// 获取data数据(有请求体的就获取,没有就跳过)
String postDataStr = "";
if(!method.equals("DELETE") && !method.equals("GET")){
String postData = RequestUtil.getPostData(request);
Map map = (Map) JSONUtil.parse(postData);
// data序列化
postDataStr = SignUtil.serializeData(map);
}
// 合成加密前字符串
String jointStr = postDataStr + "secret=" + secret+"&token=456456" + "×tamp=" + timestamp + "&url=" + url;
log.info("jointStr = {}",jointStr);
//(hutool工具类)
Digester digester = new Digester(DigestAlgorithm.MD5);
String encryptStr = digester.digestHex(jointStr).toUpperCase();
log.info("后端生成的签名 = {}", encryptStr);
if(signature.equals(encryptStr)){
log.info("签名验证成功!");
// 签名正确,拦截器放行
return true;
}else{
log.info("签名验证失败");
ResponseUtil.write(response, "请求参数不一致", 400, false);
// 签名失败,该请求不放行
return false;
}
}
}
2.2 配置拦截器
package com.lisw.test.liswtest.signature.config;
import com.lisw.test.liswtest.signature.interceptor.SignInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author lisw
* @program lisw-test
* @description
* @createDate 2022-09-19 17:15:11
**/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private SignInterceptor signInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* excludePathPatterns 为哪些请求不走该拦截器
* addPathPatterns 为拦截哪些请求
*/
registry.addInterceptor(this.signInterceptor).excludePathPatterns("").addPathPatterns("/**");
}
}
2.3 处理请求流无法被二次读取的问题
在post请求中读取请求体中的数据,需要对请求流进行读取,但是请求流只能被读取一次,所以在拦截器这里读取过一次后,在controller里面的@RequestBody再次读取的时候就会报错。error:说请求流已经被读取。
�----------添加一个filter 对流进行过滤,使得请求流可以被多次读取
�
package com.lisw.test.liswtest.signature.filter;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author lisw
* @program lisw-test
* @description
* @createDate 2022-09-19 15:55:22
**/
public class RequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 获取requestBody中的数据
body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 定义内存中的输入流
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
// 使用内存输入流读取数据
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
public static String getBodyString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
package com.lisw.test.liswtest.signature.filter;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author lisw
* @program lisw-test
* @description 过滤器
* 在post请求中读取请求体中的数据,需要对请求流进行读取,但是请求流只能被读取一次,所以在拦截器这里读取过一次后,在controller里面的@RequestBody再次读取的时候就会报错。error:说请求流已经被读取。
* 解决问题:
* 添加一个filter 对流进行过滤,使得请求流可以被多次读取
**/
@Component
public class ReplaceRequestBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest requestWrapper = new RequestWrapper(request);
try {
chain.doFilter(requestWrapper, response);
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 签名生成参数处理类
package com.lisw.test.liswtest.signature.interceptor;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* @author lisw
* @program lisw-test
* @description 签名前处理工具类
* @createDate 2022-09-19 15:59:25
**/
public class SignUtil {
// 序列化data数据
public static String serializeData(Map map){
TreeMap treeMap = sortData(map);
String serializeStr = "";
Set set = treeMap.keySet();
for (Object o : set) {
serializeStr += o + "=" + treeMap.get(o) + "&";
}
return serializeStr;
}
// 排序data数据
public static TreeMap sortData(Map map){
TreeMap treeMap = new TreeMap(map);
return treeMap;
}
//超时时效,超过此时间认为签名过期 (1 min)
private static long EXPIRE_TIME = 1 * 60 * 1000L;
// 判断请求是否超时
public static boolean verifyTimestamp(long timestamp){
Date date = new Date();
long time = date.getTime();
long dif = time - timestamp;
if(dif > 0 && dif < EXPIRE_TIME){
// 未过期
return true;
}else{
// 请求过期
return false;
}
}
}
2.5 相关工具类
package com.lisw.test.liswtest.signature.utils;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* @author lisw
* @program lisw-test
* @description 用于获取request的请求体
* @createDate 2022-09-19 15:58:27
**/
public class RequestUtil {
/**
* 获取post请求的请求体参数
* @param request 请求
* @return
* @throws UnsupportedEncodingException
* @throws UnsupportedEncodingException
*/
public static String getPostData(HttpServletRequest request) throws UnsupportedEncodingException, UnsupportedEncodingException {
request.setCharacterEncoding("UTF-8");
StringBuilder stringBuffer = new StringBuilder();
String str = null;
BufferedReader reader = null;
try {
reader = request.getReader();
while ((str = reader.readLine()) != null) {
stringBuffer.append(str);
}
} catch (IOException e) {
e.printStackTrace();
}
return stringBuffer.toString();
}
}
package com.lisw.test.liswtest.signature.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @author lisw
* @program lisw-test
* @description 响应工具类
* @createDate 2022-09-19 15:48:11
**/
@Component
public class ResponseUtil {
public static void write(HttpServletResponse response, String message, Integer code, boolean status) throws Exception {
Result<Object> result = new Result<>();
result.setMessage(message);
result.setCode(code);
result.setStatus(status);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(result));
out.flush();
out.close();
}
}
package com.lisw.test.liswtest.signature.utils;
import lombok.Data;
/**
* @author lisw
* @program lisw-test
* @description
* @createDate 2022-09-19 15:50:26
**/
@Data
public class Result<T> {
private String message;
private Integer code;
private boolean status;
}
2.6 maven依赖及代码结构图
<!--hutool工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version>
</dependency>
![image.png](https://img-blog.csdnimg.cn/img_convert/2f3ab1c2c4cb80b0311436247db002e2.png#clientId=ud63634e2-1767-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=130&id=u072db4e2&margin=[object Object]&name=image.png&originHeight=259&originWidth=300&originalType=binary&ratio=1&rotation=0&showTitle=false&size=43449&status=done&style=none&taskId=ubd0725fa-f2a7-46a4-8f93-feeafa6e51c&title=&width=150)