RuoYi-Vue-Plus之api加解密

文档地址:点击这里

项目地址:点击这里


配置文件

# api接口加密
api-decrypt:
  # 是否开启全局接口加密
  enabled: true
  # AES 加密头标识
  headerFlag: encrypt-key
  # 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
  # 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
  publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
  # 请求解密私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
  # 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
  privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=

配置类

ApiDecryptProperties

/**
 * api解密属性配置类
 * @author wdhcr
 */
@Data
@ConfigurationProperties(prefix = "api-decrypt")
public class ApiDecryptProperties {

    /**
     * 加密开关
     */
    private Boolean enabled;

    /**
     * 头部标识
     */
    private String headerFlag;

    /**
     * 响应加密公钥
     */
    private String publicKey;

    /**
     * 请求解密私钥
     */
    private String privateKey;

}

ApiDecryptAutoConfiguration

@AutoConfiguration
@EnableConfigurationProperties(ApiDecryptProperties.class)
// 与配置文件api-decrypt的enabled属性对比,如果为enabled=true则加载bean,否则不加载
@ConditionalOnProperty(value = “api-decrypt.enabled”, havingValue = “true”)
public class ApiDecryptAutoConfiguration {

@Bean
public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
    // 该实例将用于注册过滤器
    FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
    // 设置过滤器的调度类型为 REQUEST,表示该过滤器将会拦截所有的 HTTP 请求
    registration.setDispatcherTypes(DispatcherType.REQUEST);
    // 设置过滤器
    registration.setFilter(new CryptoFilter(properties));
    // 指定过滤器所要拦截的 URL 模式,这里使用了 "/*" 表示拦截所有的 URL
    registration.addUrlPatterns("/*");
    // 设置过滤器名称
    registration.setName("cryptoFilter");
    // 设置过滤器的优先级为最高优先级,这样可以确保该过滤器在其他过滤器之前执行
    registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
    return registration;
}

对于@EnableConfigurationProperties的理解,可以查看点击这里

@ApiEncrypt

/**
 * 强制加密注解
 *
 * @author Michelle.Chung
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {

    /**
     * 响应加密忽略,默认不加密,为 true 时加密
     */
    boolean response() default false;

}

当在方法上加上@ApiEcrypts时,前端传过来的数据会进行加密,后端返回的数据不会加密。

当在方法上加上@ApiEcrypts,并且response设置为true时,前端传过来的数据会进行加密,后端返回的数据也会加密。

CryptoFilter

crypto过滤器主要过滤Content-Type为"application/json"以及请求方式为Post或者Put的web请求

/**
 * Crypto 过滤器
 *
 * @author wdhcr
 */
public class CryptoFilter implements Filter {
    private final ApiDecryptProperties properties;

    public CryptoFilter(ApiDecryptProperties properties) {
        this.properties = properties;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpServletResponse servletResponse = (HttpServletResponse) response;

        boolean responseFlag = false;
        ServletRequest requestWrapper = null;
        ServletResponse responseWrapper = null;
        EncryptResponseBodyWrapper responseBodyWrapper = null;

        // 是否为 json 请求
        if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            // 是否为 put 或者 post 请求
            if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
                // 是否存在加密标头
                String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
                // 获取加密注解
                ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(servletRequest);
                responseFlag = apiEncrypt != null && apiEncrypt.response();
                if (StringUtils.isNotBlank(headerValue)) {
                    // 请求解密
                    requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPrivateKey(), properties.getHeaderFlag());
                } else {
                    // 前端不设置加密,如果有注解,则报错,没有则放行
                    if (ObjectUtil.isNotNull(apiEncrypt)) {
                        HandlerExceptionResolver exceptionResolver = SpringUtils.getBean("handlerExceptionResolver", HandlerExceptionResolver.class);
                        exceptionResolver.resolveException(
                            servletRequest, servletResponse, null,
                            new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
                        return;
                    }
                }
                // 判断是否响应加密
                if (responseFlag) {
                    responseBodyWrapper = new EncryptResponseBodyWrapper(servletResponse);
                    responseWrapper = responseBodyWrapper;
                }
            }
        }

        // 请求下一个过滤器
        chain.doFilter(
            ObjectUtil.defaultIfNull(requestWrapper, request),
            ObjectUtil.defaultIfNull(responseWrapper, response));

        if (responseFlag) {
            servletResponse.reset();
            // 对原始内容加密
            String encryptContent = responseBodyWrapper.getEncryptContent(
                servletResponse, properties.getPublicKey(), properties.getHeaderFlag());
            // 对加密后的内容写出
            servletResponse.getWriter().write(encryptContent);
        }
    }

    /**
     * 获取 ApiEncrypt 注解
     */
    private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
        RequestMappingHandlerMapping handlerMapping = SpringUtils.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        // 获取注解
        try {
            HandlerExecutionChain mappingHandler = handlerMapping.getHandler(servletRequest);
            if (ObjectUtil.isNotNull(mappingHandler)) {
                Object handler = mappingHandler.getHandler();
                if (ObjectUtil.isNotNull(handler)) {
                    // 从handler获取注解
                    if (handler instanceof HandlerMethod handlerMethod) {
                        return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    @Override
    public void destroy() {
    }
}

对于RequestMappingHandlerMapping的理解,可以查看点击这里

测试案例

接口

查看标头

如果前端传来加密标头,则请求解密。

请求解密

这里说明一下前端的加密过程:前端通过请求标头 Encrypt-Key 传递ASC秘钥(一层加密),该秘钥是解析请求参数的密码,也就是解析login接口中参数body的密码。但是,这个ASC秘钥先经过Base64加密(二层加密),再经过RSA加密(三层加密)。

后端解密步骤:先从配置文件获取RSA私钥,用来解开第三层加密;然后解开第二层加密(解开Base64不需要密码),最后得到ASC秘钥。

public DecryptRequestBodyWrapper(HttpServletRequest request, String privateKey, String headerFlag) throws IOException {
        super(request);
        // 获取请求标头Encrypt-Key的值(ASC秘钥)
        String headerRsa = request.getHeader(headerFlag);
    	// 第三层解密(RSA解密,privateKey:私钥)
        String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
        // 第二层解密(Base64解密)
        String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
    	// 设置编码
        request.setCharacterEncoding(Constants.UTF8);
        byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
        String requestBody = new String(readBytes, StandardCharsets.UTF_8);
        // 第一层加密(ASC加密) 最终结果
        String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
        body = decryptBody.getBytes(StandardCharsets.UTF_8);
    }

解密完成后,根据过滤器链,如果该过滤器下还有过滤器,则执行下一个过滤器,否则,进行加密过程。

请求加密

/**
     * 获取加密内容
     *
     * @param servletResponse response
     * @param publicKey       RSA公钥 (用于加密 AES 秘钥)
     * @param headerFlag      请求头标志
     * @return 加密内容
     * @throws IOException
     */
    public String getEncryptContent(HttpServletResponse servletResponse, String publicKey, String headerFlag) throws IOException {
        // 生成秘钥
        String aesPassword = RandomUtil.randomString(32);
        // 秘钥使用 Base64 编码
        String encryptAes = EncryptUtils.encryptByBase64(aesPassword);
        // Rsa 公钥加密 Base64 编码
        String encryptPassword = EncryptUtils.encryptByRsa(encryptAes, publicKey);

        // 设置响应头
        servletResponse.setHeader(headerFlag, encryptPassword);
        servletResponse.setHeader("Access-Control-Allow-Origin", "*");
        servletResponse.setHeader("Access-Control-Allow-Methods", "*");
        servletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());

        // 获取原始内容
        String originalBody = this.getContent();
        // 对内容进行加密
        return EncryptUtils.encryptByAes(originalBody, aesPassword);
    }

执行过程

前端传来加密内容

后端解析内容

如果@ApiEncrypt.response设置为true,则对内容进行加密,返回给前端;否则内容不加密,直接返回给前端。


RuoYi-Vue-Plus之api加解密
http://example.com/2024/04/23/RuoYi-Vue-Plus之api加解密/
发布于
2024年4月23日
更新于
2024年5月11日
许可协议