前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Boot 接口解密是什么?

Spring Boot 接口解密是什么?

原创
作者头像
大发明家
发布2021-12-21 12:29:18
9770
发布2021-12-21 12:29:18
举报
文章被收录于专栏:技术博客文章

1.开发加解密 starter

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

Spring Boot 项目中直接引用就可以。

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

代码语言:txt
复制
<dependency>
代码语言:txt
复制
    <groupId>org.springframework.boot</groupId>
代码语言:txt
复制
    <artifactId>spring-boot-starter-web</artifactId>
代码语言:txt
复制
    <scope>provided</scope>
代码语言:txt
复制
    <version>2.4.3</version>
代码语言:txt
复制
</dependency>

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

依赖添加完成后,我们先来定义一个加密工具类备用,加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES

等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法:

代码语言:txt
复制
public class AESUtils {
代码语言:txt
复制
    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
代码语言:txt
复制
    // 获取 cipher
代码语言:txt
复制
    private static Cipher getCipher(byte[] key, int model) throws Exception {
代码语言:txt
复制
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
代码语言:txt
复制
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
代码语言:txt
复制
        cipher.init(model, secretKeySpec);
代码语言:txt
复制
        return cipher;
代码语言:txt
复制
    }
代码语言:txt
复制
    // AES加密
代码语言:txt
复制
    public static String encrypt(byte[] data, byte[] key) throws Exception {
代码语言:txt
复制
        Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
代码语言:txt
复制
        return Base64.getEncoder().encodeToString(cipher.doFinal(data));
代码语言:txt
复制
    }
代码语言:txt
复制
    // AES解密
代码语言:txt
复制
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
代码语言:txt
复制
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
代码语言:txt
复制
        return cipher.doFinal(Base64.getDecoder().decode(data));
代码语言:txt
复制
    }
代码语言:txt
复制
}

这个工具类比较简单,不需要多解释。需要说明的是,加密后的数据可能不具备可读性,因此我们一般需要对加密后的数据再使用 Base64

算法进行编码,获取可读字符串。换言之,上面的 AES 加密方法的返回值是一个 Base64 编码之后的字符串,AES 解密方法的参数也是一个 Base64

编码之后的字符串,先对该字符串进行解码,然后再解密。

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

代码语言:txt
复制
public class RespBean {
代码语言:txt
复制
    private Integer status;
代码语言:txt
复制
    private String msg;
代码语言:txt
复制
    private Object obj;
代码语言:txt
复制
    public static RespBean build() {
代码语言:txt
复制
        return new RespBean();
代码语言:txt
复制
    }
代码语言:txt
复制
    public static RespBean ok(String msg) {
代码语言:txt
复制
        return new RespBean(200, msg, null);
代码语言:txt
复制
    }
代码语言:txt
复制
    public static RespBean ok(String msg, Object obj) {
代码语言:txt
复制
        return new RespBean(200, msg, obj);
代码语言:txt
复制
    }
代码语言:txt
复制
    public static RespBean error(String msg) {
代码语言:txt
复制
        return new RespBean(500, msg, null);
代码语言:txt
复制
    }
代码语言:txt
复制
    public static RespBean error(String msg, Object obj) {
代码语言:txt
复制
        return new RespBean(500, msg, obj);
代码语言:txt
复制
    }
代码语言:txt
复制
    private RespBean() {
代码语言:txt
复制
    }
代码语言:txt
复制
    private RespBean(Integer status, String msg, Object obj) {
代码语言:txt
复制
        this.status = status;
代码语言:txt
复制
        this.msg = msg;
代码语言:txt
复制
        this.obj = obj;
代码语言:txt
复制
    }
代码语言:txt
复制
    public Integer getStatus() {
代码语言:txt
复制
        return status;
代码语言:txt
复制
    }
代码语言:txt
复制
    public RespBean setStatus(Integer status) {
代码语言:txt
复制
        this.status = status;
代码语言:txt
复制
        return this;
代码语言:txt
复制
    }
代码语言:txt
复制
    public String getMsg() {
代码语言:txt
复制
        return msg;
代码语言:txt
复制
    }
代码语言:txt
复制
    public RespBean setMsg(String msg) {
代码语言:txt
复制
        this.msg = msg;
代码语言:txt
复制
        return this;
代码语言:txt
复制
    }
代码语言:txt
复制
    public Object getObj() {
代码语言:txt
复制
        return obj;
代码语言:txt
复制
    }
代码语言:txt
复制
    public RespBean setObj(Object obj) {
代码语言:txt
复制
        this.obj = obj;
代码语言:txt
复制
        return this;
代码语言:txt
复制
    }
代码语言:txt
复制
}

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

代码语言:txt
复制
@Retention(RetentionPolicy.RUNTIME)
代码语言:txt
复制
@Target({ElementType.METHOD,ElementType.PARAMETER})
代码语言:txt
复制
public @interface Decrypt {
代码语言:txt
复制
}
代码语言:txt
复制
@Retention(RetentionPolicy.RUNTIME)
代码语言:txt
复制
@Target(ElementType.METHOD)
代码语言:txt
复制
public @interface Encrypt {
代码语言:txt
复制
}

这两个注解就是两个标记,在以后使用的过程中,哪个接口方法添加了 @Encrypt 注解就对哪个接口的数据加密返回,哪个接口/参数添加了 @Decrypt

注解就对哪个接口/参数进行解密。这个定义也比较简单,没啥好说的,需要注意的是 @Decrypt@Encrypt 多了一个使用场景就是

@Decrypt 可以用在参数上。

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

代码语言:txt
复制
@ConfigurationProperties(prefix = "spring.encrypt")
代码语言:txt
复制
public class EncryptProperties {
代码语言:txt
复制
    private final static String DEFAULT_KEY = "www.itboyhub.com";
代码语言:txt
复制
    private String key = DEFAULT_KEY;
代码语言:txt
复制
    public String getKey() {
代码语言:txt
复制
        return key;
代码语言:txt
复制
    }
代码语言:txt
复制
    public void setKey(String key) {
代码语言:txt
复制
        this.key = key;
代码语言:txt
复制
    }
代码语言:txt
复制
}

这里我设置了默认的 key 是 www.itboyhub.com,key 是 16 位字符串,松哥这个网站地址刚好满足。以后如果用户想自己配置

key,只需要在 application.properties 中配置 spring.encrypt.key=xxx 即可。

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

因为松哥这篇文章一个很重要的目的是想和大家分享 ResponseBodyAdvice 和 RequestBodyAdvice

的用法,RequestBodyAdvice 在做解密的时候倒是没啥问题,而 ResponseBodyAdvice

在做加密的时候则会有一些局限,不过影响不大,还是我前面说的,如果想非常灵活的掌控一切,那还是自定义过滤器吧。这里我就先用这两个工具来实现了。

另外还有一点需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice

在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON

交互的时候,这两个才有用。不过一般来说接口加解密的场景也都是前后端分离的时候才可能有的事。

先来看接口加密:

代码语言:txt
复制
@EnableConfigurationProperties(EncryptProperties.class)
代码语言:txt
复制
@ControllerAdvice
代码语言:txt
复制
public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
代码语言:txt
复制
    private ObjectMapper om = new ObjectMapper();
代码语言:txt
复制
    @Autowired
代码语言:txt
复制
    EncryptProperties encryptProperties;
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
代码语言:txt
复制
        return returnType.hasMethodAnnotation(Encrypt.class);
代码语言:txt
复制
    }
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
代码语言:txt
复制
        byte[] keyBytes = encryptProperties.getKey().getBytes();
代码语言:txt
复制
        try {
代码语言:txt
复制
            if (body.getMsg()!=null) {
代码语言:txt
复制
                body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));
代码语言:txt
复制
            }
代码语言:txt
复制
            if (body.getObj() != null) {
代码语言:txt
复制
                body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes));
代码语言:txt
复制
            }
代码语言:txt
复制
        } catch (Exception e) {
代码语言:txt
复制
            e.printStackTrace();
代码语言:txt
复制
        }
代码语言:txt
复制
        return body;
代码语言:txt
复制
    }
代码语言:txt
复制
}

我们自定义 EncryptResponse 类实现 ResponseBodyAdvice<RespBean>

接口,泛型表示接口的返回类型,这里一共要实现两个方法:

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

再来看接口解密:

代码语言:txt
复制
@EnableConfigurationProperties(EncryptProperties.class)
代码语言:txt
复制
@ControllerAdvice
代码语言:txt
复制
public class DecryptRequest extends RequestBodyAdviceAdapter {
代码语言:txt
复制
    @Autowired
代码语言:txt
复制
    EncryptProperties encryptProperties;
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
代码语言:txt
复制
        return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
代码语言:txt
复制
    }
代码语言:txt
复制
    @Override
代码语言:txt
复制
    public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
代码语言:txt
复制
        byte[] body = new byte[inputMessage.getBody().available()];
代码语言:txt
复制
        inputMessage.getBody().read(body);
代码语言:txt
复制
        try {
代码语言:txt
复制
            byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
代码语言:txt
复制
            final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
代码语言:txt
复制
            return new HttpInputMessage() {
代码语言:txt
复制
                @Override
代码语言:txt
复制
                public InputStream getBody() throws IOException {
代码语言:txt
复制
                    return bais;
代码语言:txt
复制
                }
代码语言:txt
复制
                @Override
代码语言:txt
复制
                public HttpHeaders getHeaders() {
代码语言:txt
复制
                    return inputMessage.getHeaders();
代码语言:txt
复制
                }
代码语言:txt
复制
            };
代码语言:txt
复制
        } catch (Exception e) {
代码语言:txt
复制
            e.printStackTrace();
代码语言:txt
复制
        }
代码语言:txt
复制
        return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
代码语言:txt
复制
    }
代码语言:txt
复制
}
  1. 首先大家注意,DecryptRequest 类我们没有直接实现 RequestBodyAdvice 接口,而是继承自 RequestBodyAdviceAdapter 类,该类是 RequestBodyAdvice 接口的子类,并且实现了接口中的一些方法,这样当我们继承自 RequestBodyAdviceAdapter 时,就只需要根据自己实际需求实现某几个方法即可。
  2. supports:该方法用来判断哪些接口需要处理接口解密,我们这里的判断逻辑是方法上或者参数上含有 @Decrypt 注解的接口,处理解密问题。
  3. beforeBodyRead:这个方法会在参数转换成具体的对象之前执行,我们先从流中加载到数据,然后对数据进行解密,解密完成后再重新构造 HttpInputMessage 对象返回。

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

代码语言:txt
复制
@Configuration
代码语言:txt
复制
@ComponentScan("org.javaboy.encrypt.starter")
代码语言:txt
复制
public class EncryptAutoConfiguration {
代码语言:txt
复制
}

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

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

代码语言:txt
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration

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

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

2.打包发布

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

2.1 安装到本地仓库

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

image

2.2 发布到线上

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

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

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

image

image

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

image

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

image

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

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

代码语言:txt
复制
<dependencies>
代码语言:txt
复制
    <dependency>
代码语言:txt
复制
        <groupId>com.github.lenve</groupId>
代码语言:txt
复制
        <artifactId>encrypt-spring-boot-starter</artifactId>
代码语言:txt
复制
        <version>0.0.3</version>
代码语言:txt
复制
    </dependency>
代码语言:txt
复制
</dependencies>
代码语言:txt
复制
<repositories>
代码语言:txt
复制
    <repository>
代码语言:txt
复制
        <id>jitpack.io</id>
代码语言:txt
复制
        <url>https://jitpack.io</url>
代码语言:txt
复制
    </repository>
代码语言:txt
复制
</repositories>

3.应用

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

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

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

代码语言:txt
复制
public class User {
代码语言:txt
复制
    private Long id;
代码语言:txt
复制
    private String username;
代码语言:txt
复制
    //省略 getter/setter
代码语言:txt
复制
}

创建两个测试接口:

代码语言:txt
复制
@RestController
代码语言:txt
复制
public class HelloController {
代码语言:txt
复制
    @GetMapping("/user")
代码语言:txt
复制
    @Encrypt
代码语言:txt
复制
    public RespBean getUser() {
代码语言:txt
复制
        User user = new User();
代码语言:txt
复制
        user.setId((long) 99);
代码语言:txt
复制
        user.setUsername("javaboy");
代码语言:txt
复制
        return RespBean.ok("ok", user);
代码语言:txt
复制
    }
代码语言:txt
复制
    @PostMapping("/user")
代码语言:txt
复制
    public RespBean addUser(@RequestBody @Decrypt User user) {
代码语言:txt
复制
        System.out.println("user = " + user);
代码语言:txt
复制
        return RespBean.ok("ok", user);
代码语言:txt
复制
    }
代码语言:txt
复制
}

第一个接口使用了 @Encrypt 注解,所以会对该接口的数据进行加密(如果不使用该注解就不加密),第二个接口使用了 @Decrypt

所以会对上传的参数进行解密,注意 @Decrypt 注解既可以放在方法上也可以放在参数上。

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

首先测试 get 请求接口:

image

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

再来测试 post 请求:

image

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

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

代码语言:txt
复制
spring.encrypt.key=1234567890123456

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.开发加解密 starter
  • 2.打包发布
    • 2.1 安装到本地仓库
      • 2.2 发布到线上
      • 3.应用
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档