二、后端校验请求的签名
后端以同样的方式进行加密,与请求上的签名继续比对
1、思路
后端对签名的校验思路
。。其实原理非常简单,因为前端对参数进行加密是无法逆向解密的,所以我们只需要拿到对应的参数,然后以相同的方式、相同的算法进行加密,那么如果参数未被修改,那么前后端分别加密出来的签名就是一致的。就说明该请求没有问题(当然还需要判断请求的唯一id、时间戳,判断请求是否超时、请求是否重复)
。。我的后端是使用springboot写的,所以可以使用框架的拦截器或者AOP切面技术去实现对所有请求校验签名。其实拦截器和AOP实现起来的步骤都是差不多,但是AOP本身的功能非常强大可以对方法执行前进行拦截,这里就不赘述了。
因为使用拦截器比较简单,我就用拦截器去实现了。
2、springboot实现API接口校验签名
直接上代码 (工具类在最下面)
拦截器的关键就是获取到请求的对应参数(重点)
// SignInterceptor
@Configuration
public class SignInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取 signature
String signature = request.getHeader("signature");
System.out.println("前端生成的签名 = " + signature);
if(signature.isEmpty()){
ResponseUtil.write(response, "该请求无签名信息", 400, false);
return false;
}
// 获取 timestamp
String timestamp = request.getHeader("timestamp");
Long aLong = Long.valueOf(timestamp);
boolean b = SignUtil.verifyTimestamp(aLong);
if(!b){
ResponseUtil.write(response, "请求超时", 408, false);
return false;
}
// 获取 secret
String secret = request.getHeader("key");
// 获取 url
// 因为get请求的参数都在url上,直接对url加密就好了
StringBuffer requestURL = request.getRequestURL();
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 + "×tamp=" + timestamp + "&url=/api" + url;
System.out.println("jointStr = " + jointStr);
// md5 加密 (hutool工具类)
Digester md5 = new Digester(DigestAlgorithm.MD5);
String encryptStr = md5.digestHex(jointStr).toUpperCase();
System.out.println("后端生成的签名 = " + encryptStr);
if(signature.equals(encryptStr)){
System.out.println("签名验证成功!");
// 签名正确,拦截器放行
return true;
}else{
System.out.println("签名验证失败");
ResponseUtil.write(response, "请求参数不一致", 400, false);
// 签名失败,该请求不放行
return false;
}
}
}
3、请求流以被读取的问题
问题难点:
在post请求中读取请求体中的数据,需要对请求流进行读取,但是请求流只能被读取一次,所以在拦截器这里读取过一次后,在controller里面的@RequestBody再次读取的时候就会报错。error:说请求流已经被读取。
解决问题:
添加一个filter 对流进行过滤,使得请求流可以被多次读取
4、解决请求流无法二次读取的问题
这两个类直接粘贴进项目就好了。(要在包扫描的路径下)
// ReplaceRequestBodyFilter
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;
@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();
}
}
}
// RequestWrapper
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;
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();
}
}
5、测试签名效果
1、正常请求
2、使用postman将上述请求参数修改
这张是正常请求
这种修改了红框的参数
3、请求超时
6、工具类
// 用于获取request的请求体
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();
}
}
// 响应工具类
@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();
}
}
// 签名前处理工具类
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;
}
}
}
至此,后端对签名的校验也就完成了。有什么问题欢迎关注博主,评论区一起讨论啊。