后端校验请求签名

本文详细介绍了如何在SpringBoot后端实现API接口的签名校验,包括使用拦截器验证请求签名、处理请求流只能读取一次的问题,以及提供测试签名效果的方法。通过获取请求参数、加密对比签名,确保请求的安全性和一致性。
摘要由CSDN通过智能技术生成

二、后端校验请求的签名

后端以同样的方式进行加密,与请求上的签名继续比对

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 + "&timestamp=" + 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;
        }
    }
}

至此,后端对签名的校验也就完成了。有什么问题欢迎关注博主,评论区一起讨论啊。

在Lua中实现拦截请求校验sign可以通过OpenResty来实现。OpenResty是一个基于Nginx的Web应用开发框架,支持使用Lua脚本进行二次开发。以下是一个简单的示例,实现了拦截请求校验sign的功能: ``` -- 导入OpenResty的http库 local http = require "resty.http" -- 获取请求的URI和query参数 local uri = ngx.var.uri local args = ngx.req.get_uri_args() -- 获取请求头中的sign参数 local sign = ngx.req.get_headers()["sign"] -- 根据请求参数生成待校验签名 local signParam = "" for k, v in pairs(args) do signParam = signParam .. k .. "=" .. v .. "&" end signParam = string.sub(signParam, 1, -2) local signToCheck = ngx.md5(signParam) -- 校验签名是否正确 if sign ~= signToCheck then ngx.exit(ngx.HTTP_FORBIDDEN) end -- 发送请求后端服务 local httpc = http.new() local res, err = httpc:request_uri("http://backend_service" .. uri, { method = ngx.req.get_method(), headers = ngx.req.get_headers(), body = ngx.req.get_body_data(), keepalive_timeout = 60000, keepalive_pool = 10 }) -- 将后端服务的响应返回给客户端 ngx.status = res.status ngx.say(res.body) ngx.exit(ngx.HTTP_OK) ``` 以上代码中,首先获取请求的URI和query参数,并获取请求头中的sign参数。然后根据请求参数生成待校验签名,使用ngx.md5函数计算签名的MD5值。最后,校验签名是否正确,如果不正确则直接返回HTTP_FORBIDDEN状态码。如果签名校验通过,则使用resty.http库发送请求后端服务,并将响应返回给客户端。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值