From 58f9687c581c121d4688e2ab99678d94d262c60a Mon Sep 17 00:00:00 2001 From: Charles7c Date: Tue, 22 Jul 2025 22:46:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(security/password):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E7=BC=96=E7=A0=81=E5=99=A8=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20PasswordEncoderUtil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PasswordEncoderAutoConfiguration.java | 45 +++---- .../PasswordEncoderProperties.java | 13 +- .../enums/PasswordEncoderAlgorithm.java | 51 +++++++ .../PasswordEncodeException.java} | 32 +++-- .../password/util/PasswordEncoderUtil.java | 127 ++++++++++++++++++ .../src/main/resources/default-password.yml | 6 + 6 files changed, 230 insertions(+), 44 deletions(-) create mode 100644 continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/enums/PasswordEncoderAlgorithm.java rename continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/{constant/PasswordEncoderConstants.java => exception/PasswordEncodeException.java} (52%) create mode 100644 continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/util/PasswordEncoderUtil.java create mode 100644 continew-starter-security/continew-starter-security-password/src/main/resources/default-password.yml diff --git a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderAutoConfiguration.java b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderAutoConfiguration.java index 52560516..b0bc1a5f 100644 --- a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderAutoConfiguration.java +++ b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderAutoConfiguration.java @@ -16,8 +16,6 @@ package top.continew.starter.security.password.autoconfigure; -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.text.CharSequenceUtil; import jakarta.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,18 +23,17 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +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 org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; -import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; 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.password.enums.PasswordEncoderAlgorithm; +import top.continew.starter.security.password.util.PasswordEncoderUtil; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -55,15 +52,11 @@ import java.util.Map; */ @AutoConfiguration @EnableConfigurationProperties(PasswordEncoderProperties.class) +@PropertySource(value = "classpath:default-password.yml", factory = GeneralPropertySourceFactory.class) @ConditionalOnProperty(prefix = PropertiesConstants.SECURITY_PASSWORD, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true) public class PasswordEncoderAutoConfiguration { private static final Logger log = LoggerFactory.getLogger(PasswordEncoderAutoConfiguration.class); - private final PasswordEncoderProperties properties; - - public PasswordEncoderAutoConfiguration(PasswordEncoderProperties properties) { - this.properties = properties; - } /** * 密码编码器 @@ -72,23 +65,19 @@ public class PasswordEncoderAutoConfiguration { * @see PasswordEncoderFactories */ @Bean - public PasswordEncoder passwordEncoder(List passwordEncoderList) { + public PasswordEncoder passwordEncoder(PasswordEncoderProperties properties) { Map encoders = new HashMap<>(); - encoders.put("bcrypt", new BCryptPasswordEncoder()); - encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); - encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); - encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); - // 添加自定义的密码编解码器 - if (CollUtil.isNotEmpty(passwordEncoderList)) { - passwordEncoderList.forEach(passwordEncoder -> { - String simpleName = passwordEncoder.getClass().getSimpleName(); - encoders.put(CharSequenceUtil.removeSuffix(simpleName, "PasswordEncoder") - .toLowerCase(), passwordEncoder); - }); - } - String encodingId = properties.getEncodingId(); - CheckUtils.throwIf(!encoders.containsKey(encodingId), "{} is not found in idToPasswordEncoder.", encodingId); - return new DelegatingPasswordEncoder(encodingId, encoders); + 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 = properties.getAlgorithm(); + CheckUtils.throwIf(PasswordEncoderUtil.getEncoder(algorithm) == null, "不支持的加密算法: {}", algorithm); + return new DelegatingPasswordEncoder(algorithm.name().toLowerCase(), encoders); } @PostConstruct diff --git a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderProperties.java b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderProperties.java index d8c9c092..110d8521 100644 --- a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderProperties.java +++ b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/autoconfigure/PasswordEncoderProperties.java @@ -18,6 +18,7 @@ package top.continew.starter.security.password.autoconfigure; import org.springframework.boot.context.properties.ConfigurationProperties; import top.continew.starter.core.constant.PropertiesConstants; +import top.continew.starter.security.password.enums.PasswordEncoderAlgorithm; /** * 密码编解码配置属性 @@ -34,9 +35,9 @@ public class PasswordEncoderProperties { private boolean enabled = true; /** - * 默认启用的编码器 ID(默认:BCryptPasswordEncoder) + * 默认启用的编码器算法(默认:BCrypt 加密算法) */ - private String encodingId = "bcrypt"; + private PasswordEncoderAlgorithm algorithm = PasswordEncoderAlgorithm.BCRYPT; public boolean isEnabled() { return enabled; @@ -46,11 +47,11 @@ public class PasswordEncoderProperties { this.enabled = enabled; } - public String getEncodingId() { - return encodingId; + public PasswordEncoderAlgorithm getAlgorithm() { + return algorithm; } - public void setEncodingId(String encodingId) { - this.encodingId = encodingId; + public void setAlgorithm(PasswordEncoderAlgorithm algorithm) { + this.algorithm = algorithm; } } \ No newline at end of file diff --git a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/enums/PasswordEncoderAlgorithm.java b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/enums/PasswordEncoderAlgorithm.java new file mode 100644 index 00000000..60836fa3 --- /dev/null +++ b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/enums/PasswordEncoderAlgorithm.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.password.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; + } +} diff --git a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/constant/PasswordEncoderConstants.java b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/exception/PasswordEncodeException.java similarity index 52% rename from continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/constant/PasswordEncoderConstants.java rename to continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/exception/PasswordEncodeException.java index 3940b644..94221cfa 100644 --- a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/constant/PasswordEncoderConstants.java +++ b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/exception/PasswordEncodeException.java @@ -14,23 +14,35 @@ * limitations under the License. */ -package top.continew.starter.security.password.constant; +package top.continew.starter.security.password.exception; -import java.util.regex.Pattern; +import top.continew.starter.core.exception.BaseException; + +import java.io.Serial; /** - * 密码编码器相关常量 + * 密码编码异常 * * @author Charles7c - * @since 2.12.0 + * @since 2.13.3 */ -public class PasswordEncoderConstants { +public class PasswordEncodeException extends BaseException { - /** - * BCrypt 正则表达式 - */ - public static final Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); + @Serial + private static final long serialVersionUID = 1L; - private PasswordEncoderConstants() { + public PasswordEncodeException() { + } + + public PasswordEncodeException(String message) { + super(message); + } + + public PasswordEncodeException(Throwable cause) { + super(cause); + } + + public PasswordEncodeException(String message, Throwable cause) { + super(message, cause); } } diff --git a/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/util/PasswordEncoderUtil.java b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/util/PasswordEncoderUtil.java new file mode 100644 index 00000000..1a86bfcb --- /dev/null +++ b/continew-starter-security/continew-starter-security-password/src/main/java/top/continew/starter/security/password/util/PasswordEncoderUtil.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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.password.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.password.enums.PasswordEncoderAlgorithm; +import top.continew.starter.security.password.exception.PasswordEncodeException; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 密码加密工具类 + *

+ * 支持多种加密算法,可通过编码ID动态选择加密方式 + *

+ * + * @author Charles7c + * @since 2.13.3 + */ +public final class PasswordEncoderUtil { + + private static final Map 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); + } +} \ No newline at end of file diff --git a/continew-starter-security/continew-starter-security-password/src/main/resources/default-password.yml b/continew-starter-security/continew-starter-security-password/src/main/resources/default-password.yml new file mode 100644 index 00000000..5f0ca875 --- /dev/null +++ b/continew-starter-security/continew-starter-security-password/src/main/resources/default-password.yml @@ -0,0 +1,6 @@ +--- ### 安全配置:密码编码器配置 +continew-starter.security: + password: + enabled: true + # 默认启用的编码器算法(默认:BCrypt 加密算法) + algorithm: BCRYPT \ No newline at end of file