SpringBoot+VUE接口签名认证

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值