SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计

加密算法参考: 浅谈常见的七种加密算法及实现

加密算法参考: 加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用

目的: 通过对API接口请求报文签名,后端进行验签处理,实现接口参数防篡改的目的。

一、前言

在现如今的Web开发,或者是涉及到H5Android APPIOS APP小程序移动端开发时,都是需要后端提供Api接口来给前端调用,那么接口安全问题就被大家重视起来了,尤其是一些架构师,在项目架构过程中,需要着重考虑安全问题;说到安全问题,最常见漏洞就是在传统的接口在传输的过程中,很容易被一些黑客技术者截取请求报文,对报文抓包分析,然后更改请求头里面的重要参数值,进而伪造请求信息达到目的。仔细思考一下如果我们的项目不对请求信息做一些防攻击、防抓包篡改的话,太过于危险,尤其涉及到金额的项目安全系数需要做到更严格,这时候针对一些常规项目我们可以通过对请求头参数及整条请求报文信息进行签名、加盐验签处理,然后做到请求的幂等性控制,这种做法使用上较为普遍。

二、加密和解密概念

2.1. 加密

数据加密 的基本过程,就是对原来为 明文 的文件或数据按 某种算法 进行处理,使其成为 不可读 的一段代码,通常称为 “密文”。通过这样的途径,来达到 保护数据 不被 非法人窃取、阅读的目的。

2.2. 解密

加密 的 逆过程 为 解密,即将该 编码信息 转化为其 原来数据 的过程。

三、常见的加密算法

加密技术通常分为三大类: 对称式、非对称式、Hash算法。

2.1 对称性加密算法:AES、DES、3DES -可解密

1. 对称式加密解释: 就是加密和解密使用同一个密钥。信息接收双方都需事先知道密匙和加解密算法且其密匙是相同的,之后便是对数据进行加解密了。对称加密算法用来对敏感数据等信息进行加密。

2. 数据加密过程: 在对称加密算法中,数据发送方 将 明文 (原始数据) 和 加密密钥 一起经过特殊 加密处理,生成复杂的 加密密文 进行发送。
数据加密过程:在对称加密算法中,数据发送方 将 明文 (原始数据) 和 加密密钥 一起经过特殊 加密处理,生成复杂的 加密密文 进行发送。

3. 数据解密过程: 数据接收方 收到密文后,若想读取原数据,则需要使用 加密使用的密钥 及相同算法的 逆算法 对加密的密文进行解密,才能使其恢复成 可读明文。

4. 优点: 对称加密算法的运行速度比非对称加密算法的速度快很多,当我们需要加密大量的数据时,建议采用对称加密算法,提高加解密速度。

5. 算法选择: 通常情况下,秘钥越长,需要解密的时间就越久,程序运行的速度就越慢,AES建议采用128位,对称性加密中使用最多的是AES加密算法

2.2 非对称性加密算法:RSA、DSA、ECC -可解密

1. 非对称式加密: 又称为 公开密钥加密算法。它需要两个密钥,一个称为 公开密钥 (public key),即 公钥,另一个称为 私有密钥 (private key),即 私钥。因为 加密 和 解密 使用的是两个不同的密钥,所以这种算法称为 非对称加密算法。

2. 加解密: 如果使用 公钥 对数据 进行加密,只有用对应的 私钥 才能 进行解密。

3. 加解密: 如果使用 私钥 对数据 进行加密,只有用对应的 公钥 才能 进行解密。

4. 使用场景: 甲方公司生成 一对密钥, 并将其中的一把作为 公钥 向其它第三方公开,任何第三方都可以得到该公钥,得到该公钥的第三方使用该密钥对机密信息进行加密后再发送给甲方,甲方再使用自己保存的另一把 专用密钥 (私钥),对 加密 后的信息 进行解密。( 现实中对接支付宝支付技术就是使用RSA非对称性加密。)

5. 缺点: 非对称加密的缺点是其加解密速度要远远慢于对称加密,密钥尺寸大,加解密速度慢,一般用来加密少量数据,在某些极端情况下,甚至能比非对称加密慢上1000倍,适用于少量数据加密的情况下。

6. 算法选择: 通常情况下,秘钥越长,需要解密的时间就越久,程序运行的速度就越慢,非对称性加密中使用最多的是RSA加密算法。

2.3 散列算法(签名算法):MD5、SHA1、HMAC -不可逆

1. 散列算法: 散列算法又称哈希函数(Hash算法),是一种单向加密算法。在信息安全技术中,经常需要验证消息的完整性,散列(Hash)函数提供了这一服务,它对不同长度的输入消息,产生固定长度的输出。这个固定长度的输出称为原输入消息的"散列"或"消息摘要"(Message digest)。散列算法不算加密算法,因为其结果是不可逆的,既然是不可逆的,那么当然不是用来加密的,而是签名。

2. 使用场景: 主要用于验证,防止信息被篡改。具体用途如: 文件校验、数字签名、HTTP请求报文验签、鉴权协议、一致性验证、安全访问认证。(a. nacos分布式配置中心原理就是基于MD5对文件签名校验,MD5不一致即配置文件需要被更新了。 b. 微信公众号开发接入开发者通过SHA1散列算法签名实现。)

3. 算法选择: 通常情况下,秘钥越长,需要解密的时间就越久,程序运行的速度就越慢, 散列算法中使用最多的是MD5居多,其次是SHA1也不少

2.4 其他常用算法:Base64

Base64不是安全领域下的加解密算法,只是一个编码算法,通常用于把二进制数据编码为可写的字符形式的数据,特别适合在httpmime协议下的网络快速传输数据。UTF-8GBK中文的Base64编码结果是不同的。采用Base64编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到,但这种方式很初级,很简单。经常使用Base64可以对图片文件进行编码传输。

2.5 算法应用总结:

  • 加密算法: 是可逆的,用来对敏感数据进行保护。散列算法(签名算法、哈希算法)是不可逆的,主要用于身份验证。
  • 对称加密算法: 使用同一个密匙加密和解密,速度快,适合给大量数据加密。对称加密客户端和服务端使用同一个密匙,存在被抓包破解的风险。
  • 非对称加密算法: 使用公钥加密,私钥解密,私钥签名,公钥验签。安全性比对称加密高,但速度较慢。非对称加密使用两个密匙,服务端和客户端密匙不一样,私钥放在服务端,黑客一般是拿不到的,安全性高。
  • Base64: 不是安全领域下的加解密算法,只是一个编码算法,通常用于把二进制数据编码为可写的字符形式的数据,特别适合在http,mime协议下的网络快速传输数据。UTF-8和GBK中文的Base64编码结果是不同的。采用Base64编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到,但这种方式很初级,很简单。Base64可以对图片文件进行编码传输。
  • https: 协议广泛用于万维网上安全敏感的通讯,例如交易支付方面。它的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
  • 大量数据加密建议采用对称加密算法,提高加解密速度;小量的机密数据,可以采用非对称加密算法。在实际的操作过程中,我们通常采用的方式是:采用非对称加密算法管理对称算法的密钥,然后用对称加密算法加密数据,这样我们就集成了两类加密算法的优点,既实现了加密速度快的优点,又实现了安全方便管理密钥的优点。
  • MD5标准密钥长度128位(128位是指二进制位。二进制太长,所以一般都改写成16进制,每一位16进制数可以代替4位二进制数,所以128位二进制数写成16进制就变成了128/4=32位。16位加密就是从32位MD5散列中把中间16位提取出来);sha1标准密钥长度160位(比MD5摘要长32位),Base64转换后的字符串理论上将要比原来的长1/3。

三、基于MD5加密加盐验签API接口实现

在环境搭建开始之前,先贴一下项目完整架子:
SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计_第1张图片
需求场景:
系统从外部获取数据时,通常采用API接口调用的方式来实现。请求方和接口提供方之间的通信过程,有这几个问题需要考虑:
1、请求参数是否被篡改;
2、请求来源是否合法;
3、请求是否具有唯一性。
今天跟大家探讨一下主流的通信安全解决方案。

设计思路:

  1. 接口提供方给调用方分配APP_IDAPP_SECRETAPP_SECRET存储与后端配置文件或者数据库中,APP_ID通过请求头参数传递过来。
  2. 调用方根据APP_IDAPP_SECRET以及请求参数,按照一定算法生成签名Sign,常用算法有SHA1MD5,这里我们使用MD5签名。
  3. 接口调用方调用接口前,需在请求头传入:appIdtimestamp(10位时间戳)nonce(随机数字符串)sign(签名结果)
  4. 接口调用方根据签名算法生成签名sign值: sign = MD5(timestamp + appId + appSecret + nonce + version) ,其中 version为接口提供方API接口的统一版本号,version默认值固定为1.0
  5. 接口提供方根据签名算法验证签名sign,这一步骤可以在拦截器中实现,如果项目使用SpringCloud可以在GateWay网关层实现。

实现思路:

  1. 接口调用方在调用接口之前获取系统当前时间戳(请求Unix时间戳)、生成随机字符串NonceAPPIDAPPSecretVersion
  2. 接口调用方通过MD5签名算法 sign = MD5(timestamp + appId + appSecret + nonce + version) ,实现请求头参数签名,得到sign值,然后将timestampappIdnoncesign通过请求头参数传递,进行调用API接口。
  3. 接口提供方获取请求头参数timestampappIdnoncesign,依次判断请求参数是否为空,为空则停止校验,返回响应。
  4. 接口提供方根据前端传过来的时间戳与服务器当前时间戳做减法运算,如果差值大于180,则当前请求的timestamp无效,如果小于180,则当前请求的timestamp为有效,防止API接口被非法份子大流量请求攻击。
  5. 接口提供方通过判断redis中的nonce,确认当前请求是否为重复请求,控制API接口幂等性。
  6. 接口提供方通过MD5签名算法 sign = MD5(timestamp + appId + appSecret + nonce + version) ,实现签名,得到签名signEcrypt值,然后与前端传过来的sign值作比对,不一致则为非法请求。
  7. 接口提供方将nonce存进rediskey= noncevalue = noncetime = 180(与时间戳差值一样)。

五、代码实现

1. pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.thinkingcao</groupId>
    <artifactId>springboot-md5-encrypt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-md5-encrypt</name>
    <description>SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计</description>

    <!--编码设置-->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>


    <dependencies>
        <!--springboot web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Lombok代码简化插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Apache摘要运算、编码解码工具包-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <!--commons-lang3-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- JSON解析fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.68</version>
        </dependency>

        <!-- springboot整合redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.1</version>
        </dependency>

        <!-- commons-pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2. application.yml

# 端口
server:
  port: 8025


spring:
  application:
    name: springboot-md5

  redis:
    # Redis数据库索引(默认为0)
    database: 0
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接超时时间(毫秒)
    timeout: 10000
    jedis:
      pool:
        #连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0

# 路径排除
project:
  apifilter:
    excludes: /api/user/login

# 设置日志级别为debug
logging:
  level:
    com.thinkingcao.encrypt: debug

3. SignAuthInterceptor拦截器实现API接口验签

package com.thinkingcao.encrypt.Inter;

import com.alibaba.fastjson.JSON;
import com.thinkingcao.encrypt.common.HeadRequest;
import com.thinkingcao.encrypt.constant.Constants;
import com.thinkingcao.encrypt.encrypt.MD5Util;
import com.thinkingcao.encrypt.result.ApiResult;
import com.thinkingcao.encrypt.utils.RedisUtils;
import com.thinkingcao.encrypt.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @desc:  API请求报文签名sign = timestamp+appId+appSecret+nonce+version
 * @author: cao_wencao
 * @date: 2020-05-18 14:54
 */
@Slf4j
@Component
public class SignAuthInterceptor implements HandlerInterceptor {

    private static final String NONCE_KEY = "x-nonce-";

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String appId = request.getHeader("appId");
        if (StringUtils.isBlank(appId)){
            log.debug("appId不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("appId不能为空")));
            return false;
        }
        String timestampStr = request.getHeader("timestamp");
        if (StringUtils.isBlank(timestampStr)){
            log.debug("timestamp不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("timestamp不能为空")));
            return false;
        }
        String sign = request.getHeader("sign");
        if (StringUtils.isBlank(sign)){
            log.debug("sign不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("sign不能为空")));
            return false;
        }
        String nonce = request.getHeader("nonce");
        if (StringUtils.isBlank(nonce)){
            log.debug("nonce不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("nonce不能为空")));
            return false;
        }
        String signEcrypt = MD5Util.md5(appId  + Constants.APP_SECRET + timestampStr + nonce + new HeadRequest().getVersion());
        long timestamp = 0;
        try {
            timestamp = Long.parseLong(timestampStr);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        //1.前端传过来的时间戳与服务器当前时间戳差值大于180,则当前请求的timestamp无效
        if (Math.abs(timestamp - System.currentTimeMillis() / 1000) > 180){
            log.debug("timestamp无效...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("timestamp无效")));
            return false;
        }
        //2.通过判断redis中的nonce,确认当前请求是否为重复请求,控制API接口幂等性
        boolean nonceExists = redisUtils.hasKey(nonce);
        if (nonceExists){
            log.debug("nonce重复...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("重复的请求")));
            return false;
        }
        //3.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改
        if (!(sign.equalsIgnoreCase(signEcrypt))){
            log.debug("sign签名校验失败...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.fail("sign签名校验失败")));
            return false;
        }
        //4.将nonce存进redis
        redisUtils.set(NONCE_KEY+nonce, nonce, 180);
        log.debug("签名校验通过,放行...........");
        //5.放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

4. 请求头公共请求参数HeadRequest

package com.thinkingcao.encrypt.common;

import lombok.Data;

/**
 * @desc: 请求头公共请求参数
 * @author: cao_wencao
 * @date: 2020-05-14 17:18
 */
@Data
public class HeadRequest {
    /**
     * appId
     */
    private String appId;

    /**
     * appSecret秘钥
     */
    private String appSecret;

    /**
     * 10位时间戳
     */
    private String timestamp;

    /**
     * 参数签名
     */
    private String sign;

    /**
     * 随机字符串
     */
    private String nonce;

    /**
     * api版本号
     */
    private String version = "1.0";

}

5. MD5Util签名工具

package com.thinkingcao.encrypt.encrypt;

import org.apache.commons.codec.digest.DigestUtils;

/**
 * @desc: 基于apache.commons.codec封装摘要运算、编码解码工具类
 * @auth: cao_wencao
 * @date: 2020-05-09 17:59
 */
public class MD5Util {
    
    /**
     * 加密方法
     * @param str
     * @return
     */
    public static String md5(String str) {
        return DigestUtils.md5Hex(str);
    }

    //固定盐
    private static final String salt = "30c722c6acc64306a88dd93a814c9f0a";
    
    /**
     * 将用户输入的明文密码与固定盐进行拼装后再进行MD5加密
     * @param inputPass
     * @return
     */
    public static String inputPassToFormPass(String inputPass) {
        String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
        System.out.println(str);
        return md5(str);
    }
    
    /**
     * 将form表单中的密码转换成数据库中存储的密码
     * @param formPass
     * @param salt 随机盐
     * @return
     */
    public static String formPassToDBPass(String formPass, String salt) {
        String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }
    
    public static String inputPassToDbPass(String inputPass, String saltDB) {
        String formPass = inputPassToFormPass(inputPass);
        String dbPass = formPassToDBPass(formPass, saltDB);
        return dbPass;
    }
    
}

6. RedisUtils缓存工具类

package com.thinkingcao.encrypt.utils;

import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.concurrent.TimeUnit;

/**
 * Redis工具类
 */
@Component
public class RedisUtils {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 默认过期时长,单位:秒
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;

    /**
     * 不设置过期时长
     */
    public final static long NOT_EXPIRE = -1;


    /**
     * 插入对象
     *
     * @param key   键
     * @param value 值
     * @author zmr
     */
    public void setObject(String key, Object value) {
        set(key, value, DEFAULT_EXPIRE);
    }

    /**
     * 删除缓存
     *
     * @param key 键
     * @author zmr
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }


    /**
     * 返回指定类型结果
     *
     * @param key   键
     * @param clazz 类型class
     * @return
     * @author zmr
     */
    public <T> T get(String key, Class<T> clazz) {
        String value = get(key);
        return value == null ? null : fromJson(value, clazz);
    }

    /**
     * Object转成JSON数据
     */
    public String toJson(Object object) {
        if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
                || object instanceof Boolean || object instanceof String) {
            return String.valueOf(object);
        }
        return JSON.toJSONString(object);
    }

    /**
     * JSON数据,转成Object
     */
    private <T> T fromJson(String json, Class<T> clazz) {
        return JSON.parseObject(json, clazz);
    }


    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public String get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key).toString();
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     *              94
     * @param value 值
     *              95
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     *              111
     * @param value 值
     *              112
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     *              113
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

7. ServletUtils响应客户端工具类

package com.thinkingcao.encrypt.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;

/**
 * 客户端工具类
 *
 */
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;
    }


}

8.配置拦截器DefaultWebMvcConfigurer

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-05-09 18:40
 */
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class DefaultWebMvcConfigurer implements WebMvcConfigurer {
    @Autowired
    private SignAuthInterceptor signAuthInterceptor;

    /**
     * //1.加入的顺序就是拦截器执行的顺序,
     * //2.按顺序执行所有拦截器的preHandle
     * //3.所有的preHandle 执行完再执行全部postHandle 最后是postHandle
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signAuthInterceptor)
                .addPathPatterns("/**");             //所有请求都需要进行报文签名sign
        registry.addInterceptor(permissionInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("api/user/login"); //排除登录Token拦截
    }
}

六、生成报文测试

1. HeadTest生成报文sign值

import com.alibaba.fastjson.JSONObject;
import com.thinkingcao.encrypt.common.HeadRequest;
import com.thinkingcao.encrypt.common.RequestMessage;
import com.thinkingcao.encrypt.constant.Constants;
import com.thinkingcao.encrypt.encrypt.MD5Util;

/**
 * @desc:
 * @author: cao_wencao
 * @date: 2020-05-15 11:54
 */
public class HeadTest {
    private static final String APP_ID = "572b8e84346a419687422346bb51d0b5";
    private static final String nonce = "62a439ab5d4e433c897f4b459181b2b3";
    private static final String timesTamp = "1585881033";

    public static void main(String[] args) {
        RequestMessage requestData = new RequestMessage();
        // String timesTamp = String.valueOf( System.currentTimeMillis() / 1000);
        // String nonce = UUIDUtil.getUuid();
        HeadRequest head = new HeadRequest();
        head.setAppId(APP_ID);
        head.setAppSecret(Constants.APP_SECRET);
        head.setTimestamp(timesTamp);
        head.setNonce(nonce);
        head.setVersion("1.0");
        JSONObject jsonObj = new JSONObject();
        jsonObj.put("username", "zhangsan");
        jsonObj.put("passward", "111111");
        requestData.setBody(jsonObj);
        requestData.setHead(head);
        String sign = MD5Util.md5(APP_ID + Constants.APP_SECRET + timesTamp +  nonce + "1.0");
        requestData.setSign(sign);
        System.out.println("sign = " + sign);

        String messageResult = JSONObject.toJSONString(requestData);
        System.out.println("messageResult = " + messageResult);
    }
}

2. PostMan请求

a. 请求头传递报文签名参数:
SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计_第2张图片
b. 请求体传递接口请求参数:
SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计_第3张图片

本文仅贴出设计思路中涉及的关键部分代码作以说明讲解,有兴趣的小可爱可以拉取GitHub完整代码进行研究、欢迎大家阅读、加以指正批评。

七、源码

源码: https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-md5-encrypt

你可能感兴趣的:(前后端分离架构设计,spring,boot,MD5加密,md5)