refactor(encrypt): 拆分字段加密、API 加密模块

This commit is contained in:
2025-08-20 21:44:40 +08:00
parent e5002b8bfc
commit e9bf92ea1f
46 changed files with 525 additions and 343 deletions

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew.starter</groupId>
<artifactId>continew-starter-security</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-crypto</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 安全模块 - 加密</description>
<dependencies>
<!-- Hutool 加密解密模块(封装 JDK 中加密解密算法) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- Spring Security 附带的一个密码加密库 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- MyBatis PlusMyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,简化开发、提高效率) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,37 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.annotation;
import java.lang.annotation.*;
/**
* API加密注解
*
* @author lishuyan
* @since 2.14.0
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/**
* 默认API响应加密
*/
boolean response() default true;
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.annotation;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 字段加/解密注解
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldEncrypt {
/**
* 加密/解密算法
*/
Algorithm value() default Algorithm.DEFAULT;
/**
* 加密/解密处理器
* <p>
* 优先级高于加密/解密算法
* </p>
*/
Class<? extends IEncryptor> encryptor() default IEncryptor.class;
/**
* 对称加密算法密钥
*/
String password() default "";
/**
* 非对称加密算法公钥RSA需要
*/
String publicKey() default "";
/**
* 非对称加密算法私钥RSA需要
*/
String privateKey() default "";
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.DispatcherType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import top.continew.starter.core.constant.OrderedConstants;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.filter.ApiCryptoFilter;
/**
* API加/解密自动配置
*
* @author lishuyan
* @since 2.14.0
*/
@AutoConfiguration
@EnableConfigurationProperties(ApiCryptoProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_API_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class ApiCryptoAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(ApiCryptoAutoConfiguration.class);
/**
* API加/解密过滤器
*
* @param properties 配置
* @return 过滤器
*/
@Bean
public FilterRegistrationBean<ApiCryptoFilter> apiCryptoFilterRegistration(ApiCryptoProperties properties) {
FilterRegistrationBean<ApiCryptoFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new ApiCryptoFilter(properties));
registration.addUrlPatterns("/*");
registration.setName("apiCryptoFilter");
registration.setOrder(OrderedConstants.Filter.API_CRYPTO_FILTER);
return registration;
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Security-API-Crypto' completed initialization.");
}
}

View File

@@ -1,82 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* API加/解密属性配置
*
* @author lishuyan
* @since 2.14.0
*/
@ConfigurationProperties(PropertiesConstants.SECURITY_API_CRYPTO)
public class ApiCryptoProperties {
/**
* 是否启用
*/
private Boolean enabled;
/**
* 请求头中 AES 密钥 键名
*/
private String secretKeyHeader = "X-Api-Crypto";
/**
* 响应加密公钥
*/
private String publicKey;
/**
* 请求解密私钥
*/
private String privateKey;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getSecretKeyHeader() {
return secretKeyHeader;
}
public void setSecretKeyHeader(String secretKeyHeader) {
this.secretKeyHeader = secretKeyHeader;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.crypto.mybatis.MyBatisDecryptInterceptor;
import top.continew.starter.security.crypto.mybatis.MyBatisEncryptInterceptor;
import top.continew.starter.security.crypto.util.EncryptHelper;
import top.continew.starter.security.crypto.util.PasswordEncoderUtil;
import java.util.HashMap;
import java.util.Map;
/**
* 加/解密自动配置
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@AutoConfiguration
@EnableConfigurationProperties(CryptoProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
@PropertySource(value = "classpath:default-crypto.yml", factory = GeneralPropertySourceFactory.class)
public class CryptoAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(CryptoAutoConfiguration.class);
private final CryptoProperties properties;
public CryptoAutoConfiguration(CryptoProperties properties) {
this.properties = properties;
}
/**
* MyBatis 加密拦截器配置
*/
@Bean
@ConditionalOnMissingBean
public MyBatisEncryptInterceptor mybatisEncryptInterceptor() {
return new MyBatisEncryptInterceptor();
}
/**
* MyBatis 解密拦截器配置
*/
@Bean
@ConditionalOnMissingBean(MyBatisDecryptInterceptor.class)
public MyBatisDecryptInterceptor mybatisDecryptInterceptor() {
return new MyBatisDecryptInterceptor();
}
/**
* 密码编码器配置
*
* @see DelegatingPasswordEncoder
* @see PasswordEncoderFactories
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_CRYPTO + ".password-encoder", name = PropertiesConstants.ENABLED, havingValue = "true")
public PasswordEncoder passwordEncoder() {
PasswordEncoderProperties passwordEncoderProperties = properties.getPasswordEncoder();
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(PasswordEncoderAlgorithm.BCRYPT.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.BCRYPT));
encoders.put(PasswordEncoderAlgorithm.SCRYPT.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.SCRYPT));
encoders.put(PasswordEncoderAlgorithm.PBKDF2.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.PBKDF2));
encoders.put(PasswordEncoderAlgorithm.ARGON2.name().toLowerCase(), PasswordEncoderUtil
.getEncoder(PasswordEncoderAlgorithm.ARGON2));
PasswordEncoderAlgorithm algorithm = passwordEncoderProperties.getAlgorithm();
CheckUtils.throwIf(PasswordEncoderUtil.getEncoder(algorithm) == null, "不支持的加密算法: {}", algorithm);
return new DelegatingPasswordEncoder(algorithm.name().toLowerCase(), encoders);
}
@PostConstruct
public void postConstruct() {
EncryptHelper.init(properties);
log.debug("[ContiNew Starter] - Auto Configuration 'Security-Crypto' completed initialization.");
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.util.Objects;
/**
* 加密上下文
*
* @author lishuyan
* @since 2.13.2
*/
public class CryptoContext {
/**
* 默认算法
*/
private Algorithm algorithm;
/**
* 加密/解密处理器
* <p>
* 优先级高于加密/解密算法
* </p>
*/
Class<? extends IEncryptor> encryptor;
/**
* 对称加密算法密钥
*/
private String password;
/**
* 非对称加密算法公钥
*/
private String publicKey;
/**
* 非对称加密算法私钥
*/
private String privateKey;
public Algorithm getAlgorithm() {
return algorithm;
}
public void setAlgorithm(Algorithm algorithm) {
this.algorithm = algorithm;
}
public Class<? extends IEncryptor> getEncryptor() {
return encryptor;
}
public void setEncryptor(Class<? extends IEncryptor> encryptor) {
this.encryptor = encryptor;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CryptoContext that = (CryptoContext)o;
return algorithm == that.algorithm && Objects.equals(encryptor, that.encryptor) && Objects
.equals(password, that.password) && Objects.equals(publicKey, that.publicKey) && Objects
.equals(privateKey, that.privateKey);
}
@Override
public int hashCode() {
return Objects.hash(algorithm, encryptor, password, publicKey, privateKey);
}
}

View File

@@ -1,112 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.security.crypto.enums.Algorithm;
/**
* 加/解密配置属性
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@ConfigurationProperties(PropertiesConstants.SECURITY_CRYPTO)
public class CryptoProperties {
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 默认算法
*/
private Algorithm algorithm = Algorithm.AES;
/**
* 对称加密算法密钥
*/
private String password;
/**
* 非对称加密算法公钥
*/
private String publicKey;
/**
* 非对称加密算法私钥
*/
private String privateKey;
/**
* 密码编码器配置
*/
@NestedConfigurationProperty
private PasswordEncoderProperties passwordEncoder;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Algorithm getAlgorithm() {
return algorithm;
}
public void setAlgorithm(Algorithm algorithm) {
this.algorithm = algorithm;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public PasswordEncoderProperties getPasswordEncoder() {
return passwordEncoder;
}
public void setPasswordEncoder(PasswordEncoderProperties passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.autoconfigure;
import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
/**
* 密码编码器配置属性
*
* @author Jasmine
* @since 1.3.0
*/
public class PasswordEncoderProperties {
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 默认启用的编码器算法默认BCrypt 加密算法)
*/
private PasswordEncoderAlgorithm algorithm = PasswordEncoderAlgorithm.BCRYPT;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public PasswordEncoderAlgorithm getAlgorithm() {
return algorithm;
}
public void setAlgorithm(PasswordEncoderAlgorithm algorithm) {
this.algorithm = algorithm;
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* 加密器基类
*
* @author lishuyan
* @since 2.13.2
*/
public abstract class AbstractEncryptor implements IEncryptor {
protected AbstractEncryptor(CryptoContext context) {
// 配置校验与配置注入
}
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 对称加/解密处理器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public abstract class AbstractSymmetricCryptoEncryptor extends AbstractEncryptor {
/**
* 对称加密缓存
*/
private static final Map<String, SymmetricCrypto> CACHE = new ConcurrentHashMap<>();
/**
* 加密上下文
*/
private final CryptoContext context;
protected AbstractSymmetricCryptoEncryptor(CryptoContext context) {
super(context);
this.context = context;
}
@Override
public String encrypt(String plaintext) {
if (CharSequenceUtil.isBlank(plaintext)) {
return plaintext;
}
return this.getCrypto(context.getPassword()).encryptHex(plaintext);
}
@Override
public String decrypt(String ciphertext) {
if (CharSequenceUtil.isBlank(ciphertext)) {
return ciphertext;
}
return this.getCrypto(context.getPassword()).decryptStr(ciphertext);
}
/**
* 获取对称加密算法
*
* @param password 密钥
* @return 对称加密算法
*/
protected SymmetricCrypto getCrypto(String password) {
SymmetricAlgorithm algorithm = this.getAlgorithm();
String key = algorithm + StringConstants.UNDERLINE + password;
if (CACHE.containsKey(key)) {
return CACHE.get(key);
}
SymmetricCrypto symmetricCrypto = new SymmetricCrypto(algorithm, password.getBytes(StandardCharsets.UTF_8));
CACHE.put(key, symmetricCrypto);
return symmetricCrypto;
}
/**
* 获取对称加密算法类型
*
* @return 对称加密算法类型
*/
protected abstract SymmetricAlgorithm getAlgorithm();
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* AESAdvanced Encryption Standard 加/解密处理器
* <p>
* 美国国家标准与技术研究院(NIST)采纳的对称加密算法标准提供128位、192位和256位三种密钥长度以高效和安全性著称。
* </p>
*
* @author Charles7c
* @since 1.4.0
*/
public class AesEncryptor extends AbstractSymmetricCryptoEncryptor {
public AesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.AES;
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.core.codec.Base64;
/**
* Base64 加/解密处理器
* <p>
* 一种用于编码二进制数据到文本格式的算法,常用于邮件附件、网页传输等场合,但它不是一种加密算法,只提供数据的编码和解码,不保证数据的安全性。
* </p>
*
* @author Charles7c
* @since 1.4.0
*/
public class Base64Encryptor implements IEncryptor {
@Override
public String encrypt(String plaintext) {
return Base64.encode(plaintext);
}
@Override
public String decrypt(String ciphertext) {
return Base64.decodeStr(ciphertext);
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* DESData Encryption Standard 加/解密处理器
* <p>
* 一种对称加密算法使用相同的密钥进行加密和解密。DES 使用 56 位密钥(实际上有 64 位,但有 8 位用于奇偶校验)和一系列置换和替换操作来加密数据。
* </p>
*
* @author Charles7c
* @since 1.4.0
*/
public class DesEncryptor extends AbstractSymmetricCryptoEncryptor {
public DesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.DES;
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
/**
* 加/解密接口
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public interface IEncryptor {
/**
* 加密
*
* @param plaintext 明文
* @return 加密后的文本
*/
String encrypt(String plaintext);
/**
* 解密
*
* @param ciphertext 密文
* @return 解密后的文本
*/
String decrypt(String ciphertext);
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.autoconfigure.PasswordEncoderProperties;
/**
* 密码编码器加/解密处理器
*
* <p>
* 使用前必须注入 {@link PasswordEncoder},此加密方式不可逆,适合于密码场景
* </p>
*
* @see PasswordEncoder
* @see PasswordEncoderProperties
*
* @author Charles7c
* @since 2.13.3
*/
public class PasswordEncoderEncryptor extends AbstractEncryptor {
private final PasswordEncoder passwordEncoder = SpringUtil.getBean(PasswordEncoder.class);
private final CryptoProperties properties = SpringUtil.getBean(CryptoProperties.class);
public PasswordEncoderEncryptor(CryptoContext context) {
super(context);
}
@Override
public String encrypt(String plaintext) {
// 如果已经是加密格式,直接返回
if (properties.getPasswordEncoder().getAlgorithm().getPattern().matcher(plaintext).matches()) {
return plaintext;
}
return passwordEncoder.encode(plaintext);
}
@Override
public String decrypt(String ciphertext) {
return ciphertext;
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* PBEWithMD5AndDESPassword Based Encryption With MD5 And DES 加/解密处理器
* <p>
* 混合加密算法,结合了 MD5 散列算法和 DESData Encryption Standard加密算法
* </p>
*
* @author Charles7c
* @since 1.4.0
*/
public class PbeWithMd5AndDesEncryptor extends AbstractSymmetricCryptoEncryptor {
public PbeWithMd5AndDesEncryptor(CryptoContext context) {
super(context);
}
@Override
protected SymmetricAlgorithm getAlgorithm() {
return SymmetricAlgorithm.PBEWithMD5AndDES;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.encryptor;
import cn.hutool.core.codec.Base64;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
/**
* RSA 加/解密处理器
* <p>
* 非对称加密算法由罗纳德·李维斯特Ron Rivest、阿迪·沙米尔Adi Shamir和伦纳德·阿德曼Leonard Adleman于1977年提出安全性基于大数因子分解问题的困难性。
* </p>
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public class RsaEncryptor extends AbstractEncryptor {
/**
* 加密上下文
*/
private final CryptoContext context;
public RsaEncryptor(CryptoContext context) {
super(context);
this.context = context;
}
@Override
public String encrypt(String plaintext) {
return Base64.encode(SecureUtil.rsa(null, context.getPublicKey()).encrypt(plaintext, KeyType.PublicKey));
}
@Override
public String decrypt(String ciphertext) {
return new String(SecureUtil.rsa(context.getPrivateKey(), null)
.decrypt(Base64.decode(ciphertext), KeyType.PrivateKey));
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.enums;
import top.continew.starter.security.crypto.encryptor.*;
/**
* 加密/解密算法枚举
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public enum Algorithm {
/**
* 默认使用配置属性的算法
*/
DEFAULT(null),
/**
* AES
*/
AES(AesEncryptor.class),
/**
* DES
*/
DES(DesEncryptor.class),
/**
* PBE With MD5 And DES
*/
PBE_WITH_MD5_AND_DES(PbeWithMd5AndDesEncryptor.class),
/**
* RSA
*/
RSA(RsaEncryptor.class),
/**
* Base64
*/
BASE64(Base64Encryptor.class),
/**
* 密码编码器支持算法BCrypt、SCRYPT、PBKDF2、ARGON2
*/
PASSWORD_ENCODER(PasswordEncoderEncryptor.class);
/**
* 加密/解密处理器
*/
private final Class<? extends IEncryptor> encryptor;
Algorithm(Class<? extends IEncryptor> encryptor) {
this.encryptor = encryptor;
}
public Class<? extends IEncryptor> getEncryptor() {
return encryptor;
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.enums;
import java.util.regex.Pattern;
/**
* 密码编码器加密算法枚举
*
* @author Charles7c
* @since 2.13.3
*/
public enum PasswordEncoderAlgorithm {
/** BCrypt加密算法 */
BCRYPT(Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}")),
/** SCrypt加密算法 */
SCRYPT(Pattern.compile("\\A\\$s0\\$[0-9a-f]+\\$[0-9a-f]+\\$[0-9a-f]+")),
/** PBKDF2加密算法 */
PBKDF2(Pattern.compile("\\A\\$pbkdf2-sha256\\$\\d+\\$[0-9a-f]+\\$[0-9a-f]+")),
/** Argon2加密算法 */
ARGON2(Pattern.compile("\\A\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[0-9a-zA-Z+/]+\\$[0-9a-zA-Z+/]+"));
/** 正则匹配 */
private final Pattern pattern;
PasswordEncoderAlgorithm(Pattern pattern) {
this.pattern = pattern;
}
public Pattern getPattern() {
return pattern;
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.exception;
import top.continew.starter.core.exception.BaseException;
import java.io.Serial;
/**
* 密码编码异常
*
* @author Charles7c
* @since 2.13.3
*/
public class PasswordEncodeException extends BaseException {
@Serial
private static final long serialVersionUID = 1L;
public PasswordEncodeException() {
}
public PasswordEncodeException(String message) {
super(message);
}
public PasswordEncodeException(Throwable cause) {
super(cause);
}
public PasswordEncodeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.filter;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import top.continew.starter.security.crypto.annotation.ApiEncrypt;
import top.continew.starter.security.crypto.autoconfigure.ApiCryptoProperties;
import java.io.IOException;
/**
* API加/解密过滤器
*
* @author lishuyan
* @since 2.14.0
*/
public class ApiCryptoFilter implements Filter {
/**
* API加/密配置
*/
private final ApiCryptoProperties properties;
public ApiCryptoFilter(ApiCryptoProperties properties) {
this.properties = properties;
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
// 获取API加密注解
ApiEncrypt apiEncrypt = this.getApiEncryptAnnotation(request);
// 响应加密标识
boolean responseEncryptFlag = ObjectUtil.isNotNull(apiEncrypt) && apiEncrypt.response();
// 密钥标头
String secretKeyHeader = properties.getSecretKeyHeader();
ServletRequest requestWrapper = null;
ServletResponse responseWrapper = null;
ResponseBodyEncryptWrapper responseBodyEncryptWrapper = null;
// 是否为 PUT 或者 POST 请求
if (HttpMethod.PUT.matches(request.getMethod()) || HttpMethod.POST.matches(request.getMethod())) {
// 获取密钥值
String secretKeyValue = request.getHeader(secretKeyHeader);
if (CharSequenceUtil.isNotBlank(secretKeyValue)) {
// 请求解密
requestWrapper = new RequestBodyDecryptWrapper(request, properties.getPrivateKey(), secretKeyHeader);
}
}
// 响应加密,响应包装器替换响应体加密包装器
if (responseEncryptFlag) {
responseBodyEncryptWrapper = new ResponseBodyEncryptWrapper(response);
responseWrapper = responseBodyEncryptWrapper;
}
// 继续执行
chain.doFilter(ObjectUtil.defaultIfNull(requestWrapper, request), ObjectUtil
.defaultIfNull(responseWrapper, response));
// 响应加密,执行完成后,响应密文
if (responseEncryptFlag) {
servletResponse.reset();
// 获取密文
String encryptContent = responseBodyEncryptWrapper.getEncryptContent(response, properties
.getPublicKey(), secretKeyHeader);
// 写出密文
servletResponse.getWriter().write(encryptContent);
}
}
/**
* 获取 ApiEncrypt 注解
*
* @param request HTTP请求
* @return ApiEncrypt注解如果未找到则返回null
*/
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest request) {
try {
RequestMappingHandlerMapping handlerMapping = SpringUtil
.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain mappingHandler = handlerMapping.getHandler(request);
// 检查是否存在处理链
if (ObjectUtil.isNull(mappingHandler)) {
return null;
}
// 获取处理器
Object handler = mappingHandler.getHandler();
// 检查是否为HandlerMethod类型
if (!(handler instanceof HandlerMethod handlerMethod)) {
return null;
}
// 获取方法上的ApiEncrypt注解
return handlerMethod.getMethodAnnotation(ApiEncrypt.class);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -1,126 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.filter;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.springframework.http.MediaType;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 请求体解密包装类
*
* @author lishuyan
* @since 2.14.0
*/
public class RequestBodyDecryptWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RequestBodyDecryptWrapper(HttpServletRequest request,
String privateKey,
String secretKeyHeader) throws IOException {
super(request);
this.body = getDecryptContent(request, privateKey, secretKeyHeader);
}
/**
* 获取解密后的请求体
*
* @param request 请求对象
* @param privateKey RSA私钥
* @param secretKeyHeader 密钥头
* @return 解密后的请求体
* @throws IOException /
*/
public byte[] getDecryptContent(HttpServletRequest request,
String privateKey,
String secretKeyHeader) throws IOException {
// 通过 请求头 获取 AES 密钥,密钥内容经过 RSA 加密
String secretKeyByRsa = request.getHeader(secretKeyHeader);
// 通过 RSA 解密,获取 AES 密钥,密钥内容经过 Base64 编码
String secretKeyByBase64 = EncryptHelper.decryptByRsa(secretKeyByRsa, privateKey);
// 通过 Base64 解码,获取 AES 密钥
String aesSecretKey = EncryptHelper.decodeByBase64(secretKeyByBase64);
request.setCharacterEncoding(CharsetUtil.UTF_8);
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
// 通过 AES 密钥,解密 请求体
return EncryptHelper.decryptByAes(requestBody, aesSecretKey).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public int getContentLength() {
return body.length;
}
@Override
public long getContentLengthLong() {
return body.length;
}
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream stream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return stream.read();
}
@Override
public int available() {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}

View File

@@ -1,140 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.filter;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.springframework.http.HttpHeaders;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
/**
* 响应体加密包装类
*
* @author lishuyan
* @since 2.14.0
*/
public class ResponseBodyEncryptWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream byteArrayOutputStream;
private final ServletOutputStream servletOutputStream;
private final PrintWriter printWriter;
public ResponseBodyEncryptWrapper(HttpServletResponse response) throws IOException {
super(response);
this.byteArrayOutputStream = new ByteArrayOutputStream();
this.servletOutputStream = this.getOutputStream();
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));
}
@Override
public PrintWriter getWriter() {
return printWriter;
}
@Override
public void flushBuffer() throws IOException {
if (servletOutputStream != null) {
servletOutputStream.flush();
}
if (printWriter != null) {
printWriter.flush();
}
}
@Override
public void reset() {
byteArrayOutputStream.reset();
}
public byte[] getResponseData() throws IOException {
flushBuffer();
return byteArrayOutputStream.toByteArray();
}
public String getContent() throws IOException {
flushBuffer();
return byteArrayOutputStream.toString();
}
/**
* 获取加密内容
*
* @param response 响应对象
* @param publicKey RSA公钥
* @param secretKeyHeader 密钥头
* @return 加密内容
*/
public String getEncryptContent(HttpServletResponse response,
String publicKey,
String secretKeyHeader) throws IOException {
// 生成 AES 密钥
String aesSecretKey = RandomUtil.randomString(32);
// Base64 编码
String secretKeyByBase64 = EncryptHelper.encodeByBase64(aesSecretKey);
// RSA 加密
String secretKeyByRsa = EncryptHelper.encryptByRsa(secretKeyByBase64, publicKey);
// 设置响应头
response.addHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, secretKeyHeader);
response.setHeader(secretKeyHeader, secretKeyByRsa);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, StringConstants.ASTERISK);
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, StringConstants.ASTERISK);
response.setCharacterEncoding(CharsetUtil.UTF_8);
// 通过 AES 密钥,对原始内容进行加密
return EncryptHelper.encryptByAes(this.getContent(), aesSecretKey);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
byteArrayOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
byteArrayOutputStream.write(b, off, len);
}
};
}
}

View File

@@ -1,129 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.mapping.MappedStatement;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BaseException;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/**
* 字段解密拦截器
*
* @author Charles7c
* @since 1.4.0
*/
public abstract class AbstractMyBatisInterceptor {
private static final Map<Class<?>, List<Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap<>();
private static final Map<String, Map<String, FieldEncrypt>> ENCRYPT_PARAM_CACHE = new ConcurrentHashMap<>();
/**
* 获取所有字符串类型、需要加/解密的、有值字段
*
* @param obj 对象
* @return 字段列表
*/
protected List<Field> getEncryptFields(Object obj) {
if (obj == null) {
return Collections.emptyList();
}
return this.getEncryptFields(obj.getClass());
}
/**
* 获取所有字符串类型、需要加/解密的、有值字段
*
* @param clazz 类型对象
* @return 字段列表
*/
protected List<Field> getEncryptFields(Class<?> clazz) {
return CLASS_FIELD_CACHE.computeIfAbsent(clazz, key -> Arrays.stream(ReflectUtil.getFields(clazz))
.filter(field -> String.class.equals(field.getType()))
.filter(field -> field.getAnnotation(FieldEncrypt.class) != null)
.toList());
}
/**
* 获取加密参数
*
* @param mappedStatement 映射语句
* @return 获取加密参数
*/
protected Map<String, FieldEncrypt> getEncryptParameters(MappedStatement mappedStatement) {
String mappedStatementId = mappedStatement.getId();
return ENCRYPT_PARAM_CACHE.computeIfAbsent(mappedStatementId, key -> {
Method method = this.getMethod(mappedStatementId);
if (method == null) {
return Collections.emptyMap();
}
Map<String, FieldEncrypt> encryptMap = new HashMap<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
if (fieldEncrypt == null) {
continue;
}
String parameterName = this.getParameterName(parameter);
encryptMap.put(parameterName, fieldEncrypt);
if (String.class.equals(parameter.getType())) {
encryptMap.put("param" + (i + 1), fieldEncrypt);
}
}
return encryptMap;
});
}
/**
* 获取映射方法
*
* @param mappedStatementId 映射语句 ID
* @return 映射方法
*/
private Method getMethod(String mappedStatementId) {
String className = CharSequenceUtil.subBefore(mappedStatementId, StringConstants.DOT, true);
String methodName = CharSequenceUtil.subAfter(mappedStatementId, StringConstants.DOT, true);
try {
Method[] methods = ReflectUtil.getMethods(Class.forName(className));
return Stream.of(methods).filter(method -> method.getName().equals(methodName)).findFirst().orElse(null);
} catch (ClassNotFoundException e) {
throw new BaseException(e);
}
}
/**
* 获取参数名称
*
* @param parameter 参数
* @return 参数名称
*/
public String getParameterName(Parameter parameter) {
Param param = parameter.getAnnotation(Param.class);
return param != null ? param.value() : parameter.getName();
}
}

View File

@@ -1,131 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.type.SimpleTypeRegistry;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
* 字段解密拦截器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class MyBatisDecryptInterceptor extends AbstractMyBatisInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行结果
Object obj = invocation.proceed();
if (ObjectUtil.isNull(obj)) {
return null;
}
// 确保目标是 ResultSetHandler
if (!(invocation.getTarget() instanceof ResultSetHandler)) {
return obj;
}
// 处理查询结果
if (obj instanceof List<?> resultList) {
// 处理列表结果
this.decryptList(resultList);
} else if (obj instanceof Map<?, ?> map) {
// 处理Map结果
this.decryptMap(map);
} else {
// 处理单个对象结果
this.decryptObject(obj);
}
return obj;
}
/**
* 解密列表结果
*
* @param resultList 结果列表
*/
private void decryptList(List<?> resultList) {
if (CollUtil.isEmpty(resultList)) {
return;
}
for (Object result : resultList) {
decryptObject(result);
}
}
/**
* 解密Map结果
*
* @param resultMap 结果Map
*/
private void decryptMap(Map<?, ?> resultMap) {
if (CollUtil.isEmpty(resultMap)) {
return;
}
new HashSet<>(resultMap.values()).forEach(this::decryptObject);
}
/**
* 解密单个对象结果
*
* @param result 结果对象
*/
private void decryptObject(Object result) {
if (ObjectUtil.isNull(result)) {
return;
}
// String、Integer、Long 等简单类型对象无需处理
if (SimpleTypeRegistry.isSimpleType(result.getClass())) {
return;
}
// 获取所有字符串类型、需要解密的、有值字段
List<Field> fieldList = super.getEncryptFields(result);
if (fieldList.isEmpty()) {
return;
}
// 解密处理
for (Field field : fieldList) {
Object fieldValue = ReflectUtil.getFieldValue(result, field);
if (fieldValue == null) {
continue;
}
String strValue = String.valueOf(fieldValue);
if (CharSequenceUtil.isBlank(strValue)) {
continue;
}
ReflectUtil.setFieldValue(result, field, EncryptHelper.decrypt(strValue, field
.getAnnotation(FieldEncrypt.class)));
}
}
}

View File

@@ -1,225 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.mybatis;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.util.EncryptHelper;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字段加密拦截器
*
* @author Charles7c
* @author lishuyan
* @since 1.4.0
*/
public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor implements InnerInterceptor {
private static final Pattern PARAM_PAIRS_PATTERN = Pattern
.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");
@Override
public void beforeQuery(Executor executor,
MappedStatement mappedStatement,
Object parameterObject,
RowBounds rowBounds,
ResultHandler resultHandler,
BoundSql boundSql) {
if (parameterObject == null) {
return;
}
if (parameterObject instanceof Map parameterMap) {
this.encryptQueryParameter(parameterMap, mappedStatement);
}
}
@Override
public void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) {
if (parameterObject == null) {
return;
}
if (parameterObject instanceof Map parameterMap) {
// 带别名方法(使用 @Param 注解的场景)
this.encryptMap(parameterMap, mappedStatement);
} else {
// 无别名方法例如MP insert 等方法)
this.encryptEntity(super.getEncryptFields(parameterObject), parameterObject);
}
}
/**
* 加密查询参数(针对 Map 类型参数)
*
* @param parameterMap 参数
* @param mappedStatement 映射语句
*/
private void encryptQueryParameter(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
Map<String, FieldEncrypt> encryptParameterMap = super.getEncryptParameters(mappedStatement);
for (Map.Entry<String, Object> parameterEntrySet : parameterMap.entrySet()) {
String parameterName = parameterEntrySet.getKey();
Object parameterValue = parameterEntrySet.getValue();
if (parameterValue == null || ClassUtil.isBasicType(parameterValue
.getClass()) || parameterValue instanceof AbstractWrapper) {
continue;
}
if (parameterValue instanceof String str) {
FieldEncrypt fieldEncrypt = encryptParameterMap.get(parameterName);
if (fieldEncrypt != null) {
parameterMap.put(parameterName, this.doEncrypt(str, fieldEncrypt));
}
} else {
// 实体参数
this.encryptEntity(super.getEncryptFields(parameterValue), parameterValue);
}
}
}
/**
* 处理实体加密
*
* @param fieldList 加密字段列表
* @param entity 实体
*/
private void encryptEntity(List<Field> fieldList, Object entity) {
for (Field field : fieldList) {
Object fieldValue = ReflectUtil.getFieldValue(entity, field);
if (fieldValue == null) {
continue;
}
String strValue = String.valueOf(fieldValue);
if (CharSequenceUtil.isBlank(strValue)) {
continue;
}
ReflectUtil.setFieldValue(entity, field, EncryptHelper.encrypt(strValue, field
.getAnnotation(FieldEncrypt.class)));
}
}
/**
* 加密 Map 类型数据(使用 @Param 注解的场景)
*
* @param parameterMap 参数
* @param mappedStatement 映射语句
*/
private void encryptMap(Map<String, Object> parameterMap, MappedStatement mappedStatement) {
Object parameter;
// 别名带有 et针对 MP 的 updateById、update 等方法)
if (parameterMap.containsKey(Constants.ENTITY) && (parameter = parameterMap.get(Constants.ENTITY)) != null) {
this.encryptEntity(super.getEncryptFields(parameter), parameter);
}
// 别名带有 ew针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
if (parameterMap.containsKey(Constants.WRAPPER) && (parameter = parameterMap.get(Constants.WRAPPER)) != null) {
this.encryptUpdateWrapper(parameter, mappedStatement);
}
}
/**
* 处理 UpdateWrapper 类型参数加密(针对 MP 的 UpdateWrapper、LambdaUpdateWrapper 等参数)
*
* @param parameter Wrapper 参数
* @param mappedStatement 映射语句
* @author cary
* @author wangshaopeng@talkweb.com.cn<a
* href="https://blog.csdn.net/tianmaxingkonger/article/details/130986784">基于Mybatis-Plus拦截器实现MySQL数据加解密</a>
* @since 2.1.1
*/
private void encryptUpdateWrapper(Object parameter, MappedStatement mappedStatement) {
if (parameter instanceof AbstractWrapper updateWrapper) {
String sqlSet = updateWrapper.getSqlSet();
if (CharSequenceUtil.isBlank(sqlSet)) {
return;
}
// 将 name=#{ew.paramNameValuePairs.xxx},age=#{ew.paramNameValuePairs.xxx} 切出来
String[] elArr = sqlSet.split(StringConstants.COMMA);
Map<String, String> propMap = new HashMap<>(elArr.length);
Arrays.stream(elArr).forEach(el -> {
String[] elPart = el.split(StringConstants.EQUALS);
propMap.put(elPart[0], elPart[1]);
});
// 获取加密字段
Class<?> entityClass = this.getEntityClass(updateWrapper, mappedStatement);
List<Field> encryptFieldList = super.getEncryptFields(entityClass);
for (Field field : encryptFieldList) {
FieldEncrypt fieldEncrypt = field.getAnnotation(FieldEncrypt.class);
String el = propMap.get(field.getName());
if (CharSequenceUtil.isBlank(el)) {
continue;
}
Matcher matcher = PARAM_PAIRS_PATTERN.matcher(el);
if (matcher.matches()) {
String valueKey = matcher.group(1);
Object value = updateWrapper.getParamNameValuePairs().get(valueKey);
updateWrapper.getParamNameValuePairs().put(valueKey, this.doEncrypt(value, fieldEncrypt));
}
}
}
}
/**
* 处理加密
*
* @param parameterValue 参数值
* @param fieldEncrypt 字段加密注解
*/
private Object doEncrypt(Object parameterValue, FieldEncrypt fieldEncrypt) {
if (ObjectUtil.isNull(parameterValue)) {
return null;
}
String strValue = String.valueOf(parameterValue);
if (CharSequenceUtil.isBlank(strValue)) {
return null;
}
return EncryptHelper.encrypt(strValue, fieldEncrypt);
}
/**
* 获取实体类
*
* @param wrapper 查询或更新包装器
* @param mappedStatement 映射语句
* @return 实体类
*/
private Class<?> getEntityClass(AbstractWrapper wrapper, MappedStatement mappedStatement) {
// 尝试从 Wrapper 中获取实体类
Class<?> entityClass = wrapper.getEntityClass();
if (entityClass != null) {
return entityClass;
}
// 从映射语句中获取实体类
return mappedStatement.getParameterMap().getType();
}
}

View File

@@ -1,320 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
import top.continew.starter.security.crypto.autoconfigure.CryptoContext;
import top.continew.starter.security.crypto.autoconfigure.CryptoProperties;
import top.continew.starter.security.crypto.encryptor.IEncryptor;
import top.continew.starter.security.crypto.enums.Algorithm;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 加密助手
*
* @author lishuyan
* @since 2.13.2
*/
public class EncryptHelper {
private static final Logger log = LoggerFactory.getLogger(EncryptHelper.class);
/**
* 默认加密配置
*/
private static CryptoProperties defaultProperties;
/**
* 加密器缓存
*/
private static final Map<Integer, IEncryptor> ENCRYPTOR_CACHE = new ConcurrentHashMap<>();
private EncryptHelper() {
}
/**
* 初始化默认配置
*
* @param properties 加密配置
*/
public static void init(CryptoProperties properties) {
defaultProperties = properties;
}
/**
* 注册加密执行者到缓存
* <p>
* 计算 CryptoContext 对象的hashCode作为缓存中的key通过hashCode查询缓存中存在则直接返回不存在则创建并缓存
* </p>
*
* @param cryptoContext 加密执行者需要的相关配置参数
* @return 加密执行者
*/
public static IEncryptor registerAndGetEncryptor(CryptoContext cryptoContext) {
int key = cryptoContext.hashCode();
return ENCRYPTOR_CACHE.computeIfAbsent(key, k -> cryptoContext.getEncryptor().equals(IEncryptor.class)
? ReflectUtil.newInstance(cryptoContext.getAlgorithm().getEncryptor(), cryptoContext)
: ReflectUtil.newInstance(cryptoContext.getEncryptor(), cryptoContext));
}
/**
* 获取字段上的 @FieldEncrypt 注解
*
* @param obj 对象
* @param fieldName 字段名称
* @return 字段上的 @FieldEncrypt 注解
* @throws NoSuchFieldException /
*/
public static FieldEncrypt getFieldEncrypt(Object obj, String fieldName) throws NoSuchFieldException {
Field field = obj.getClass().getDeclaredField(fieldName);
return field.getAnnotation(FieldEncrypt.class);
}
/**
* 加密方法
*
* @param value 待加密字符串
* @param fieldEncrypt 待加密字段注解
* @return 加密后的字符串
*/
public static String encrypt(String value, FieldEncrypt fieldEncrypt) {
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
return value;
}
String ciphertext = value;
try {
CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
}
return ciphertext;
}
/**
* 加密方法
*
* @param value 待加密字符串
* @return 加密后的字符串
*/
public static String encrypt(String value) {
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
return value;
}
String ciphertext = value;
try {
CryptoContext cryptoContext = buildCryptoContext();
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
ciphertext = encryptor.encrypt(ciphertext);
} catch (Exception e) {
log.warn("加密失败,请检查加密配置,处理加密字段异常:{}", e.getMessage(), e);
}
return ciphertext;
}
/**
* 解密方法
*
* @param value 待解密字符串
* @param fieldEncrypt 待解密字段注解
* @return 解密后的字符串
*/
public static String decrypt(String value, FieldEncrypt fieldEncrypt) {
if (CharSequenceUtil.isBlank(value) || fieldEncrypt == null || !defaultProperties.isEnabled()) {
return value;
}
String plaintext = value;
try {
CryptoContext cryptoContext = buildCryptoContext(fieldEncrypt);
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
}
return plaintext;
}
/**
* 解密方法
*
* @param value 待解密字符串
* @return 解密后的字符串
*/
public static String decrypt(String value) {
if (CharSequenceUtil.isBlank(value) || !defaultProperties.isEnabled()) {
return value;
}
String plaintext = value;
try {
CryptoContext cryptoContext = buildCryptoContext();
IEncryptor encryptor = registerAndGetEncryptor(cryptoContext);
plaintext = encryptor.decrypt(plaintext);
} catch (Exception e) {
log.warn("解密失败,请检查加密配置,处理解密字段异常:{}", e.getMessage(), e);
}
return plaintext;
}
/**
* 构建加密上下文
*
* @param fieldEncrypt 字段加密注解
* @return 加密上下文
*/
private static CryptoContext buildCryptoContext(FieldEncrypt fieldEncrypt) {
CryptoContext cryptoContext = new CryptoContext();
cryptoContext.setAlgorithm(fieldEncrypt.value() == Algorithm.DEFAULT
? defaultProperties.getAlgorithm()
: fieldEncrypt.value());
cryptoContext.setEncryptor(fieldEncrypt.encryptor().equals(IEncryptor.class)
? IEncryptor.class
: fieldEncrypt.encryptor());
cryptoContext.setPassword(fieldEncrypt.password().isEmpty()
? defaultProperties.getPassword()
: fieldEncrypt.password());
cryptoContext.setPrivateKey(fieldEncrypt.privateKey().isEmpty()
? defaultProperties.getPrivateKey()
: fieldEncrypt.privateKey());
cryptoContext.setPublicKey(fieldEncrypt.publicKey().isEmpty()
? defaultProperties.getPublicKey()
: fieldEncrypt.publicKey());
return cryptoContext;
}
/**
* 构建加密上下文
*
* @return 加密上下文
*/
private static CryptoContext buildCryptoContext() {
CryptoContext cryptoContext = new CryptoContext();
cryptoContext.setAlgorithm(defaultProperties.getAlgorithm());
cryptoContext.setEncryptor(IEncryptor.class);
cryptoContext.setPassword(defaultProperties.getPassword());
cryptoContext.setPrivateKey(defaultProperties.getPrivateKey());
cryptoContext.setPublicKey(defaultProperties.getPublicKey());
return cryptoContext;
}
/**
* Base64编码
*
* @param data 待编码数据
* @return 编码后字符串
* @since 2.14.0
*/
public static String encodeByBase64(String data) {
return Base64.encode(data, StandardCharsets.UTF_8);
}
/**
* Base64解码
*
* @param data 待解码数据
* @return 解码后字符串
* @since 2.14.0
*/
public static String decodeByBase64(String data) {
return Base64.decodeStr(data, StandardCharsets.UTF_8);
}
/**
* AES加密
*
* @param data 待加密数据
* @param password 秘钥字符串
* @return 加密后字符串, 采用Base64编码
* @since 2.14.0
*/
public static String encryptByAes(String data, String password) {
if (CharSequenceUtil.isBlank(password)) {
throw new IllegalArgumentException("AES需要传入秘钥信息");
}
// AES算法的秘钥要求是16位、24位、32位
int[] array = {16, 24, 32};
if (!ArrayUtil.contains(array, password.length())) {
throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
}
return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8);
}
/**
* AES解密
*
* @param data 待解密数据
* @param password 秘钥字符串
* @return 解密后字符串
* @since 2.14.0
*/
public static String decryptByAes(String data, String password) {
if (CharSequenceUtil.isBlank(password)) {
throw new IllegalArgumentException("AES需要传入秘钥信息");
}
// AES算法的秘钥要求是16位、24位、32位
int[] array = {16, 24, 32};
if (!ArrayUtil.contains(array, password.length())) {
throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位");
}
return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8);
}
/**
* RSA公钥加密
*
* @param data 待加密数据
* @param publicKey 公钥
* @return 加密后字符串, 采用Base64编码
* @since 2.14.0
*/
public static String encryptByRsa(String data, String publicKey) {
if (CharSequenceUtil.isBlank(publicKey)) {
throw new IllegalArgumentException("RSA需要传入公钥进行加密");
}
RSA rsa = SecureUtil.rsa(null, publicKey);
return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey);
}
/**
* RSA私钥解密
*
* @param data 待解密数据
* @param privateKey 私钥
* @return 解密后字符串
* @since 2.14.0
*/
public static String decryptByRsa(String data, String privateKey) {
if (CharSequenceUtil.isBlank(privateKey)) {
throw new IllegalArgumentException("RSA需要传入私钥进行解密");
}
RSA rsa = SecureUtil.rsa(privateKey, null);
return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8);
}
}

View File

@@ -1,127 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.security.crypto.util;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import top.continew.starter.security.crypto.enums.PasswordEncoderAlgorithm;
import top.continew.starter.security.crypto.exception.PasswordEncodeException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 密码加密工具类
* <p>
* 支持多种加密算法可通过编码ID动态选择加密方式
* </p>
*
* @author Charles7c
* @since 2.13.3
*/
public final class PasswordEncoderUtil {
private static final Map<PasswordEncoderAlgorithm, PasswordEncoder> ENCODER_CACHE = new ConcurrentHashMap<>();
static {
// 初始化默认的加密算法实例
ENCODER_CACHE.put(PasswordEncoderAlgorithm.BCRYPT, new BCryptPasswordEncoder());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.SCRYPT, SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.PBKDF2, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
ENCODER_CACHE.put(PasswordEncoderAlgorithm.ARGON2, Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
}
private PasswordEncoderUtil() {
}
/**
* 使用指定的加密算法加密密码
*
* @param algorithm 加密算法
* @param rawPassword 原始密码
* @return 加密后的密码
* @throws IllegalArgumentException 如果不支持指定的加密算法
*/
public static String encode(PasswordEncoderAlgorithm algorithm, String rawPassword) {
// 参数校验
if (algorithm == null) {
throw new IllegalArgumentException("加密算法不能为空");
}
if (rawPassword == null) {
throw new IllegalArgumentException("原始密码不能为空");
}
// 获取对应的密码编码器
PasswordEncoder encoder = ENCODER_CACHE.get(algorithm);
if (encoder == null) {
throw new IllegalArgumentException("不支持的加密算法: " + algorithm);
}
try {
return encoder.encode(rawPassword);
} catch (Exception e) {
throw new PasswordEncodeException("密码加密失败: " + e.getMessage(), e);
}
}
/**
* 验证密码是否匹配
*
* @param algorithm 加密算法
* @param rawPassword 原始密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
* @throws IllegalArgumentException 如果不支持指定的加密算法
*/
public static boolean matches(PasswordEncoderAlgorithm algorithm, String rawPassword, String encodedPassword) {
// 参数校验
if (algorithm == null) {
throw new IllegalArgumentException("加密算法不能为空");
}
if (rawPassword == null || encodedPassword == null) {
return false;
}
// 获取对应的密码编码器
PasswordEncoder encoder = ENCODER_CACHE.get(algorithm);
if (encoder == null) {
throw new IllegalArgumentException("不支持的加密算法: " + algorithm);
}
try {
return encoder.matches(rawPassword, encodedPassword);
} catch (Exception e) {
return false;
}
}
/**
* 获取指定算法的密码编码器
*
* @param algorithm 加密算法
* @return 密码编码器实例不存在则返回null
*/
public static PasswordEncoder getEncoder(PasswordEncoderAlgorithm algorithm) {
if (algorithm == null) {
return null;
}
return ENCODER_CACHE.get(algorithm);
}
}

View File

@@ -1,2 +0,0 @@
top.continew.starter.security.crypto.autoconfigure.CryptoAutoConfiguration
top.continew.starter.security.crypto.autoconfigure.ApiCryptoAutoConfiguration

View File

@@ -1,6 +0,0 @@
--- ### 安全配置:字段加/解密配置
continew-starter.security:
crypto:
enabled: true
# 默认算法,即 @FieldEncrypt 默认采用的算法默认AES 对称加密算法)
algorithm: AES

View File

@@ -16,7 +16,6 @@
<description>ContiNew Starter 安全模块</description>
<modules>
<module>continew-starter-security-crypto</module>
<module>continew-starter-security-mask</module>
<module>continew-starter-security-xss</module>
<module>continew-starter-security-sensitivewords</module>