整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

「JS 逆向百例」医保局 SM2+SM4 国产加密算

「JS 逆向百例」医保局 SM2+SM4 国产加密算法实战

本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!

逆向目标

  • 目标:医疗保障局公共查询
  • 主页:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL25hdGlvbmFsSGFsbFN0LyMvc2VhcmNoL21lZGljYWw=
  • 接口:aHR0cHM6Ly9mdXd1Lm5oc2EuZ292LmNuL2VidXMvZnV3dS9hcGkvbnRobC9hcGkvZml4ZWQvcXVlcnlGaXhlZEhvc3BpdGFs
  • 逆向参数:Request Payload 的 encDatasignData、Request Headers 的 x-tif-noncex-tif-signature

逆向过程

抓包分析

来到公共查询页面,点击翻页,就可以看到一个 POST 请求,Request Payload 的参数部分是加密的,主要是 appCode、encData 和 signData 参数,同样返回的数据也有这些参数,其加密解密方法是一样的,其中 encType 和 signType 分别为 SM4 和 SM2,所以大概率这是国密算法了,有关国密算法 K 哥前期文章有介绍:《爬虫逆向基础,认识 SM1-SM9、ZUC 国密算法》,此外请求头还有 x-tif-nonce 和 x-tif-signature 参数,如下图所示:

参数逆向

直接全局搜索 encData 或 signData,搜索结果仅在 app.1634197175801.js 有,非常明显,上面还有设置 header 的地方,所有参数都在这里,埋下断点,可以看到这里就是加密的地方,如下图所示:

这里的加密函数,主要都传入了一个 e 参数,我们可以先看一下这个 e,里面的参数含义如下:

  • addr:医疗机构详细地址,默认空;
  • medinsLvCode:医疗机构等级代码,默认空;
  • medinsName:医疗机构名称,默认空;
  • medinsTypeCode:医疗机构类型代码,默认空;
  • pageNum:页数,默认 1;
  • pageSize:每页数据条数,默认 10;
  • regnCode:医疗机构所在地代码,默认 110000(北京市);
  • sprtEcFlag:暂时不知其含义,默认空。

等级代码、类型代码、所在地代码,都是通过请求加密接口得到的,他们的加密和解密方法都一样,在最后的完整代码里有分享,这里不再赘述。其他参数比如 appCode,是在 JS 里写死的。

我们再观察一下整个 JS 文件,在头部可以看到 .call 语句,并且有 exports 关键字,很明显是一个 webpack 形式的写法。

我们回到加密的地方,从上往下看,整个函数引用了很多其他模块,如果想整个扣下来,花费时间肯定是无比巨大的,如果想直接拿下整个 JS,再将参数导出,这种暴力做法可是可以,但是整个 JS 有七万多行,运行效率肯定是有所影响的,所以观察函数,将不用的函数去掉,有用的留下来,是比较好的做法,观察 function d,第一行 var t=n("6c27").sha256,点进去来到 createOutputMethod 方法,这里整个是一个 SHA256 算法,从这个方法往下整个 copy 下来即可,如下图所示:

这里要注意的是,观察这个函数后面导出的 sha256 实际上是调用了 createMethod 这个方法,那么我们 copy 下来的方法直接调用 createMethod 即可,即 var t=createMethod(),不需要这些 exports 了。

另外还有一些变量需要定义,整个 copy 下来的结构如下:

接着前面的继续往下看,还有一句 o=Object(i.a)(),同样点进去直接 copy 下来即可,这里没有什么需要注意的地方。

再往下看就来到了 e.data.signData=p(e),点进 function p,将整个函数 copy 下来,这时候你本地调试会发现没有任何错误,实际上他这里使用了 try-catch 语句,捕获到了异常之后就没有任何处理,可以自己加一句 console.log(e) 来输出异常,实际上他这里会在 o.doSignature、e.from 两个位置提示未定义,同样的我们可以点进去将函数扣出来,但是后面会遇到函数不断引用其他函数,为了方便,我们可以将其写到 webpack 里,下面的 e.from 也是一样。

将模块写成 webpack 形式,在自执行方法里调用,然后定义全局变量来接收,再将原来的 o, e 换成全局变量即可,这里还需要注意的一个地方,那就是 o.doSignature 传入的 h,是一个定值,需要定义一下,不然后面解密是失败的。如下图所示:

这里扣 webpack 模块的时候也需要注意,不要把所有原方法里有的模块都扣出来,有些根本没用到,可以直接注释掉,这个过程是需要有耐心的,你如果全部扣,那将会是无穷无尽的,还不如直接使用整个 JS 文件,所有有用的模块如下(可能会多,但不会少):

接着原来的说,encData: v("SM4", e) 这里用到了 function v,v 里面又用到了 A、g 等函数,全部扣下来即可,同时还需要注意,前面所说的 e 在 A 函数里也用到了,同样需要换成我们自己定义的全局变量,如下图所示:

到此加密用到的函数都扣完了,此时我们可以写一个方法,对加密的过程进行封装,使用时只需要传入类似以下参数即可:

{
    "addr": "", 
    "regnCode": "110000", 
    "medinsName": "", 
    "sprtEcFlag": "", 
    "medinsLvCode": "", 
    "medinsTypeCode": "", 
    "pageNum": 1, 
    "pageSize": 10
}

如下图所示 getEncryptedData 就是加密方法:

那么解密方法呢?很明显返回的数据是 encData,直接搜索 encData 就只有三个结果,很容易找到就行 function y,同样的,这里要注意把 e.from 改成我们自定义的 e_.Buffer.from,另外我们也可以将 header 参数的生成方法也封装成一个函数,便于调用。

完整代码

GitHub 关注 K 哥爬虫,持续分享爬虫相关代码!欢迎 star !https://github.com/kgepachong/

以下只演示部分关键代码,不能直接运行!完整代码仓库地址:https://github.com/kgepachong/crawler/

JavaScript 加密关键代码架构

var sm2, sm4, e_;
!function (e) {
    var n={},
        i={app: 0},
        r={app: 0};

    function o(t) {}

    o.e=function (e) {}
    o.m=e
    o.c=n
    o.d=function (e, t, n) {}
    o.r=function (e) {}
    o.n=function (e) {}
    o.o=function (e, t) {}

    sm2=o('4d09')
    e_=o('b639')
    sm4=o('e04e')

}({
    "4d09": function (e, t, n) {},
    'f33e': function (e, t, n) {},
    "4d2d": function (e, t, n) {},
    'b381': function (e, t, n) {},
    // 此处省略 N 个模块
})

// 此处省略 N 个变量

var createOutputMethod=function (e, t) {},
    createMethod=function (e) {},
    nodeWrap=function (method, is224) {},
    createHmacOutputMethod=function (e, t) {},
    createHmacMethod=function (e) {};

function Sha256(e, t) {}

function HmacSha256(e, t, n) {}

// 此处省略 N 个方法

function i() {}

function p(t) {}

function m(e) {}

var c={
    paasId: undefined,
    appCode: "T98HPCGN5ZVVQBS8LZQNOAEXVI9GYHKQ",
    version: "1.0.0",
    appSecret: "NMVFVILMKT13GEMD3BKPKCTBOQBPZR2P",
    publicKey: "BEKaw3Qtc31LG/hTPHFPlriKuAn/nzTWl8LiRxLw4iQiSUIyuglptFxNkdCiNXcXvkqTH79Rh/A2sEFU6hjeK3k=",
    privateKey: "AJxKNdmspMaPGj+onJNoQ0cgWk2E3CYFWKBJhpcJrAtC",
    publicKeyType: "base64",
    privateKeyType: "base64"
    },
    l=c.appCode,
    u=c.appSecret,
    f=c.publicKey,
    h=c.privateKey,
    t=createMethod(),
    // t=n("6c27").sha256,
    r=Math.ceil((new Date).getTime() / 1e3),
    o=i(),
    a=r + o + r;

function getEncryptedData(data) {
    var e={"data": data}
    return e.data={
            data: e.data || {}
        },
        e.data.appCode=c.appCode,
        e.data.version=c.version,
        e.data.encType="SM4",
        e.data.signType="SM2",
        e.data.timestamp=r,
        e.data.signData=p(e),
        e.data.data={
            encData: v("SM4", e)
        },
        // e.data=JSON.stringify({
        //     data: e.data
        // }),
        e
}

function getDecryptedData(t) {
    if (!t)
        return null;
    var n=e_.Buffer.from(t.data.data.encData, "hex")
      , i=function(t, n) {
        var i=sm4.decrypt(n, t)
          , r=i[i.length - 1];
        return i=i.slice(0, i.length - r),
        e_.Buffer.from(i).toString("utf-8")
    }(g(l, u), n);
    return JSON.parse(i)
}

function getHeaders(){
    var headers={}
    return headers["x-tif-paasid"]=c.paasId,
        headers["x-tif-signature"]=t(a),
        headers["x-tif-timestamp"]=r.toString(),
        headers["x-tif-nonce"]=o,
        headers["Accept"]="application/json",
        headers["contentType"]="application/x-www-form-urlencoded",
        headers
}

Python 获取数据关键代码

家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

数据库对于在 Node.js 应用程序中持久保存数据至关重要。 根据应用程序的不同,开发者可以采用最多样化的实现,而 Node.js lowdb 是重量级数据库的一个精简替代品,而其也是本文的主角。

数据库的共同点是开发者必须安装数据库软件并运行服务器进程。 然而,在某些情况下,这意味着不必要的大量开销。 特别是如果开发者只是想快速测试某些内容或者处于无法安装任何其他软件的环境中。

SQLite 是一种轻量级替代方案,是基于文件的 SQL 数据库。 但开发者依然必须编译数据库驱动程序。 为了避免这种情况,一个名为 lowdb 的数据库出现了。 lowdb 数据库基于 Lodash ,数据保存在 JSON 文件中。

注意:由于数据库以纯文本形式存储数据,因此根本无法达到成熟数据库的性能,成熟数据库通常以优化的二进制格式存储信息。 因此,不建议在生产操作中使用 lowdb。

什么是 lowdb

lowdb 是简单易用的类型安全的本地 JSON 数据库 ,具有以下突出优势:

  • 非常轻量
  • 极简主义者
  • 支持 TypeScript
  • 纯 JavaScript
  • 安全原子写入
  • 可破解:更改存储、文件格式(JSON、YAML...)或通过适配器添加加密,支持用 lodash、ramda 扩展
  • 在测试期间自动切换到快速内存模式

当然,值得一提的是 Lowdb 不支持 Node 的 cluster 模块。

如果有大型 JavaScript 对象(~10-100MB)可能会遇到一些性能问题。 这是因为每当调用 db.write 时,整个 db.data 都会使用 JSON.stringify 序列化并写入存储。

当然,这也和具体用例有关系,可以通过执行批处理操作并仅在需要时调用 db.write 来缓解此问题。如要计划扩展,强烈建议使用 PostgreSQL 或 MongoDB 等数据库。

目前 lowdb 在 Github 通过 MIT 协议开源,有超过 20.5k 的 star、1k 的 fork、422k 的项目依赖量、是一个妥妥的前端优质开源项目。

如何使用 lowdb

Lowdb 是一个纯粹的 ESM 包,下面是基本使用方法:

import {JSONFilePreset} from 'lowdb/node'
// 读取或者创建 db.json
const defaultData={posts: [] }
const db=await JSONFilePreset('db.json', defaultData)

// 更新 db.json
await db.update(({posts})=> posts.push('hello world'))

// 或者可以稍后显式调用 db.write()
// 写入 db.json
db.data.posts.push('hello world')
await db.write()

输出数据如下:

// db.json
{
  "posts": ["hello world"]
}

TypeScript 支持

可以使用 TypeScript 检查数据类型:

type Data={
  messages: string[]
}

const defaultData: Data={messages: [] }
const db=await JSONPreset<Data>('db.json', defaultData)

db.data.messages.push('foo')
// ? Success
db.data.messages.push(1)
// ? TypeScript error

Lodash 扩展

可以使用 Lodash(或其他库)扩展 lowdb。为了能够扩展它,在这里不使用 JSONPreset。相反,使用更加底层的组件。

import {Low} from 'lowdb'
import {JSONFile} from 'lowdb/node'
import lodash from 'lodash'

type Post={
  id: number
  title: string
}
type Data={
  posts: Post[]
}
// Extend Low class with a new `chain` field
class LowWithLodash<T> extends Low<T> {
  chain: lodash.ExpChain<this['data']>=lodash.chain(this).get('data')
}
const defaultData: Data={
  posts: [],
}
const adapter=new JSONFile<Data>('db.json', defaultData)

const db=new LowWithLodash(adapter)
await db.read()

// Instead of db.data use db.chain to access lodash API
const post=db.chain.get('posts').find({ id: 1 }).value() // Important: value() must be called to execute chain

Lowdb adapters 适配器

JSONFile JSONFileSync

用于读取和写入 JSON 文件的适配器,用法也非常简单:

import {JSONFile, JSONFileSync} from 'lowdb/node'

new Low(new JSONFile(filename), {})
new LowSync(new JSONFileSync(filename), {})

Memory MemorySync

即内存适配器,对于加速单元测试很有用。

import {Memory, MemorySync} from 'lowdb'

new Low(new Memory(), {})
new LowSync(new MemorySync(), {})

LocalStorage SessionStorage

window.localStorage 和 window.sessionStorage 的同步适配器。

import {LocalStorage, SessionStorage} from 'lowdb/browser'
new LowSync(new LocalStorage(name), {})
new LowSync(new SessionStorage(name), {})

本文总结

本文主要和大家介绍 lowdb ,即一个简单易用的类型安全的本地 JSON 数据库。因为篇幅问题,关于 lowdb 只是做了一个简短的介绍,但是文末的参考资料提供了大量优秀文档以供学习,如果有兴趣可以自行阅读。如果大家有什么疑问欢迎在评论区留言。

参考资料

https://github.com/typicode/lowdb

https://morioh.com/a/76cb8aa98844/lowdb-simple-to-use-local-json-database

https://headty.medium.com/building-a-crud-app-with-node-express-and-lowdb-beginner-cec2d5d1b65e

https://medium.com/@billys.moustakas/node-js-lowdb-a-lightweight-database-alternative-309583f555b2

https://www.youtube.com/watch?app=desktop&v=jeochJ-hUao

https://morioh.com/a/76cb8aa98844/lowdb-simple-to-use-local-json-database

https://dbdb.io/db/lowdb/revisions/3

为有小伙伴刚好问到这个问题,松哥就抽空撸一篇文章和大家聊聊这个话题。

加密解密本身并不是难事,问题是在何时去处理?定义一个过滤器,将请求和响应分别拦截下来进行处理也是一个办法,这种方式虽然粗暴,但是灵活,因为可以拿到一手的请求参数和响应数据。不过 SpringMVC 中给我们提供了 ResponseBodyAdvice 和 RequestBodyAdvice,利用这两个工具可以对请求和响应进行预处理,非常方便。

所以今天这篇文章有两个目的:

  • 分享参数/响应加解密的思路。
  • 分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法。

好了,那么接下来就不废话了,我们一起来看下。

1.开发加解密 starter

为了让我们开发的这个工具更加通用,也为了复习一下自定义 Spring Boot Starter,这里我们就将这个工具做成一个 stater,以后在 Spring Boot 项目中直接引用就可以。

首先我们创建一个 Spring Boot 项目,引入 spring-boot-starter-web 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>
    <version>2.4.3</version>
</dependency>

因为我们这个工具是为 Web 项目开发的,以后必然使用在 Web 环境中,所以这里添加依赖时 scope 设置为 provided。

依赖添加完成后,我们先来定义一个加密工具类备用,加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法:

public class AESUtils {

    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

    // 获取 cipher
    private static Cipher getCipher(byte[] key, int model) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(model, secretKeySpec);
        return cipher;
    }

    // AES加密
    public static String encrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
        return Base64.getEncoder().encodeToString(cipher.doFinal(data));
    }

    // AES解密
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
        return cipher.doFinal(Base64.getDecoder().decode(data));
    }
}

这个工具类比较简单,不需要多解释。需要说明的是,加密后的数据可能不具备可读性,因此我们一般需要对加密后的数据再使用 Base64 算法进行编码,获取可读字符串。换言之,上面的 AES 加密方法的返回值是一个 Base64 编码之后的字符串,AES 解密方法的参数也是一个 Base64 编码之后的字符串,先对该字符串进行解码,然后再解密。

接下来我们封装一个响应工具类备用,这个大家如果经常看松哥视频已经很了解了:

public class RespBean {
    private Integer status;
    private String msg;
    private Object obj;

    public static RespBean build() {
        return new RespBean();
    }

    public static RespBean ok(String msg) {
        return new RespBean(200, msg, null);
    }

    public static RespBean ok(String msg, Object obj) {
        return new RespBean(200, msg, obj);
    }

    public static RespBean error(String msg) {
        return new RespBean(500, msg, null);
    }

    public static RespBean error(String msg, Object obj) {
        return new RespBean(500, msg, obj);
    }

    private RespBean() {
    }

    private RespBean(Integer status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }

    public Integer getStatus() {
        return status;
    }

    public RespBean setStatus(Integer status) {
        this.status = status;
        return this;
    }

    public String getMsg() {
        return msg;
    }

    public RespBean setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public Object getObj() {
        return obj;
    }

    public RespBean setObj(Object obj) {
        this.obj = obj;
        return this;
    }
}

接下来我们定义两个注解 @Decrypt@Encrypt

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}

这两个注解就是两个标记,在以后使用的过程中,哪个接口方法添加了 @Encrypt 注解就对哪个接口的数据加密返回,哪个接口/参数添加了 @Decrypt 注解就对哪个接口/参数进行解密。这个定义也比较简单,没啥好说的,需要注意的是 @Decrypt@Encrypt 多了一个使用场景就是 @Decrypt 可以用在参数上。

考虑到用户可能会自己配置加密的 key,因此我们再来定义一个 EncryptProperties 类来读取用户配置的 key:

@ConfigurationProperties(prefix = "spring.encrypt")
public class EncryptProperties {
    private final static String DEFAULT_KEY = "www.itboyhub.com";
    private String key = DEFAULT_KEY;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}

这里我设置了默认的 key 是 www.itboyhub.com,key 是 16 位字符串,松哥这个网站地址刚好满足。以后如果用户想自己配置 key,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可。

所有准备工作做完了,接下来就该正式加解密了。

因为松哥这篇文章一个很重要的目的是想和大家分享 ResponseBodyAdvice 和 RequestBodyAdvice 的用法,RequestBodyAdvice 在做解密的时候倒是没啥问题,而 ResponseBodyAdvice 在做加密的时候则会有一些局限,不过影响不大,还是我前面说的,如果想非常灵活的掌控一切,那还是自定义过滤器吧。这里我就先用这两个工具来实现了。

另外还有一点需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON 交互的时候,这两个才有用。不过一般来说接口加解密的场景也都是前后端分离的时候才可能有的事。

先来看接口加密:

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
    private ObjectMapper om = new ObjectMapper();
    @Autowired
    EncryptProperties encryptProperties;
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return returnType.hasMethodAnnotation(Encrypt.class);
    }

    @Override
    public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        byte[] keyBytes = encryptProperties.getKey().getBytes();
        try {
            if (body.getMsg()!=null) {
                body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));
            }
            if (body.getObj() != null) {
                body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return body;
    }
}

我们自定义 EncryptResponse 类实现 ResponseBodyAdvice 接口,泛型表示接口的返回类型,这里一共要实现两个方法:

  1. supports:这个方法用来判断什么样的接口需要加密,参数 returnType 表示返回类型,我们这里的判断逻辑就是方法是否含有 @Encrypt 注解,如果有,表示该接口需要加密处理,如果没有,表示该接口不需要加密处理。
  2. beforeBodyWrite:这个方法会在数据响应之前执行,也就是我们先对响应数据进行二次处理,处理完成后,才会转成 json 返回。我们这里的处理方式很简单,RespBean 中的 status 是状态码就不用加密了,另外两个字段重新加密后重新设置值即可。
  3. 另外需要注意,自定义的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解来标记。

再来看接口解密:

@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
    @Autowired
    EncryptProperties encryptProperties;
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        byte[] body = new byte[inputMessage.getBody().available()];
        inputMessage.getBody().read(body);
        try {
            byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
            final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
            return new HttpInputMessage() {
                @Override
                public InputStream getBody() throws IOException {
                    return bais;
                }

                @Override
                public HttpHeaders getHeaders() {
                    return inputMessage.getHeaders();
                }
            };
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
    }
}
  1. 首先大家注意,DecryptRequest 类我们没有直接实现 RequestBodyAdvice 接口,而是继承自 RequestBodyAdviceAdapter 类,该类是 RequestBodyAdvice 接口的子类,并且实现了接口中的一些方法,这样当我们继承自 RequestBodyAdviceAdapter 时,就只需要根据自己实际需求实现某几个方法即可。
  2. supports:该方法用来判断哪些接口需要处理接口解密,我们这里的判断逻辑是方法上或者参数上含有 @Decrypt 注解的接口,处理解密问题。
  3. beforeBodyRead:这个方法会在参数转换成具体的对象之前执行,我们先从流中加载到数据,然后对数据进行解密,解密完成后再重新构造 HttpInputMessage 对象返回。

接下来,我们再来定义一个自动化配置类,如下:

@Configuration
@ComponentScan("org.javaboy.encrypt.starter")
public class EncryptAutoConfiguration {

}

这个也没啥好说的,比较简单。

最后,resources 目录下定义 META-INF,然后再定义 spring.factories 文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration

这样当项目启动时,就会自动加载该配置类。

至此,我们的 starter 就开发完成啦。

2.打包发布

我们可以将项目安装到本地仓库,也可以发布到线上供他人使用。

2.1 安装到本地仓库

安装到本地仓库比较简单,直接 mvn install,或者在 IDEA 中,点击右边的 Maven,然后双击 install,如下:

2.2 发布到线上

发不到线上我们可以使用 JitPack 来做。

首先我们在 GitHub 上创建一个仓库,将我们的代码上传上去,这个过程应该不用我多说吧。

上传成功后,点击右边的 Create a new release 按钮,发布一个正式版,如下:

发布成功后,打开 jitpack,输入仓库的完整路径,点击 lookup 按钮,查找到之后,再点击 Get it 按钮完成构建,如下:

构建成功后,JitPack 上会给出项目引用方式:

注意引用时将 tag 改成你具体的版本号。

至此,我们的工具就已经成功发布了!小伙伴们可以通过如下方式引用这个 starter:

<dependencies>
    <dependency>
        <groupId>com.github.lenve</groupId>
        <artifactId>encrypt-spring-boot-starter</artifactId>
        <version>0.0.3</version>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

3.应用

我们创建一个普通的 Spring Boot 项目,引入 web 依赖,再引入我们刚刚的 starter 依赖,如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.lenve</groupId>
        <artifactId>encrypt-spring-boot-starter</artifactId>
        <version>0.0.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

然后再创建一个实体类备用:

public class User {
    private Long id;
    private String username;
    //省略 getter/setter
}

创建两个测试接口:

@RestController
public class HelloController {
    @GetMapping("/user")
    @Encrypt
    public RespBean getUser() {
        User user = new User();
        user.setId((long) 99);
        user.setUsername("javaboy");
        return RespBean.ok("ok", user);
    }

    @PostMapping("/user")
    public RespBean addUser(@RequestBody @Decrypt User user) {
        System.out.println("user = " + user);
        return RespBean.ok("ok", user);
    }
}

第一个接口使用了 @Encrypt 注解,所以会对该接口的数据进行加密(如果不使用该注解就不加密),第二个接口使用了 @Decrypt 所以会对上传的参数进行解密,注意 @Decrypt 注解既可以放在方法上也可以放在参数上。

接下来启动项目进行测试。

首先测试 get 请求接口:

可以看到,返回的数据已经加密。

再来测试 post 请求:

可以看到,参数中的加密数据已经被还原了。

如果用户想要修改加密密钥,可以在 application.properties 中添加如下配置:

spring.encrypt.key=1234567890123456

加密数据到了前端,前端也有一些 js 工具来处理加密数据,这个松哥后面有空再和大家说说 js 的加解密。

4.小结

好啦,今天这篇文章主要是想和大家聊聊 ResponseBodyAdvice 和 RequestBodyAdvice 的用法,一些加密思路,当然 ResponseBodyAdvice 和 RequestBodyAdvice 还有很多其他的使用场景,小伙伴们可以自行探索~本文使用了对称加密中的 AES 算法,大家也可以尝试改成非对称加密。

好啦,今天就聊这么多,小伙伴们可以去试试啦~公号后台回复 20210309 可以下载本文案例~