密码存储安全设计与实践指南

HYF Lv4

每次看到 MD5、明文 甚至 base64 存储的密码,心就凉半截。密码安全是系统根基,存不好,一出事就是大事。

这篇就来讲讲怎么安全地存密码。不整虚的,核心就两条:​用对算法 + 关键实践做到位。这里会重点安利目前公认最强的 ​Argon2id​,并给出一套实际落地方案。

还会聊聊性能怎么调,毕竟安全也不能把服务器跑垮了…

密码存安全?先记住两条铁律

明文存密码?直接抬走!​​

甭管你数据库用了啥华丽加密(AES啥的),​存用户原始密码就是作大死。
运维手滑、备份泄露、甚至内部人使坏… 分分钟密码裸奔。一出事?用户完蛋,你也离凉凉不远了😇

“加密”救不了密码狗命!​​

别一听“加密”就觉得安全了!​对称加密(AES这种)根本不配存密码!​​

为啥?

  • ​钥匙丢了全完蛋:​​ 密钥管理是大坑,泄露风险高。
  • ​思路歪了:​​ 加密是“藏着掖着”,咱密码需要的是“挫骨扬灰”(不可逆)

真·救命稻草是 哈希(Hashing):

  • 把密码一顿数学猛操作,变成独一无二的乱码串(哈希值)​。
  • ​核心魔法:基本算不出原密码!​​(不可逆是王道)
  • 数据库就算被搬空,黑客拿到手的也是这堆“骨灰”,想还原?难如登天!

咱就是说,专业的事情交给专业的人来做好嘛?

算法选型?长江前浪推后浪…

传统哈希(MD5、SHA-1)早该进垃圾桶了!计算快得像火箭🚀—— ​这正是黑客暴力破解的最爱。现在玩的是 ​慢哈希​ —— 核心思想:​故意让算哈希变慢、变耗资源,破解成本upup

慢哈希三剑客

当前能打的

算法怎么卷死黑客?一句话点评
​bcrypt​主打“死命算”(可调迭代轮数) ​老牌劲旅,稳,但硬件扛性渐吃力​
​scrypt​主打“狂吃内存”+“死命算” ​内存墙高手,让定制硬件也肉疼​
​Argon2id​ ​内存+计算双重狂暴!​​ (抗GPU/ASIC卷王) ​卫冕冠军,全方位抗揍,现代首选👑​

别问了,直接拉踩!​​

为啥 Argon2id 是卷王?看它如何吊打前浪

算法抗硬件破解能力防偷窥能力(抗侧信道)官方(NIST)盖章结论
MD5/SHA1 ​❌ ​秒跪 ​❌ ​裸奔​❌ ​凉凉​坟头草三米高​
​bcrypt​⚠️ ​能撑几拳​✅ ​还行​❌ ​不推荐了 ​​老将迟暮​
PBKDF2​⚠️ ​吃老本​✅ ​还行​✅ ​还行(旧爱) ​保底备胎
Argon2id​✅ ​卷死硬件💪 ​✅ ​铜墙铁壁​✅ ​力推!​ ​​无脑首选!​

划重点:​​

闭眼 Argon2id!​​ 别留恋 bcrypt 了。至少上 PBKDF2!​​ MD5/SHA1 赶紧铲了!

参数怎么调?(核心就是一个字:慢!但别慢死自己)​​
每个算法都有“难度旋钮”(内存大小、迭代次数、线程数等),后面讲 Argon2id 实操会细调。​宗旨:在你能承受的最慢边缘疯狂试探!​​(让爆破/破解成本最大化)

Argon2:密码哈希卷王!

一句话总结:它用内存墙 + 算力地狱双重暴击,专治各种不服(GPU、ASIC、彩虹表)!

凭什么选它?—— 一场官方认证的登基

👑 2015年密码哈希大赛冠军​:不是野鸡奖!全球密码学大佬组局 PK,​Argon2 干翻所有对手登基​(bcryptscrypt 都是手下败将)。

​🚨 专为现代硬件杀手而生​:早年的 MD5、bcrypt 碰见如今的 ​显卡矿场级破解,就像小米加步枪打航母——白给!Argon2 生来就为对抗这些 ​暴力怪兽。

核心理念:卷!往死里卷破解/爆破的成本!​​

(别误会,不是卷程序员!参数可调,后面教你摸鱼调参大法)

慢到窒息(可控慢哈希)​​

核心战术:拖延战!​​
普通哈希眨眼就算完(MD5:我快死了,别拿我存密码!),​Argon2 故意搞得很慢很慢​(几百毫秒一级)。想象你试 100 亿个密码?等着天荒地老 + 电费爆炸吧!

内存黑洞(内存依赖 Memory-Hard)

​杀手锏:疯狂吃内存!​​
传统算法(如 bcrypt)主要卷 CPU,但黑客能用超多显卡并行狂飙​(成本骤降)。
Argon2 笑了:小样!给我狂吃内存!​​

破解时必须占用海量+连续内存​(调参能上几百MB/GB)

直接废掉显卡(显存小)+ ASIC矿机(内存成本高)的武功​!显存不够?卡死你

时间+内存+线程三重折磨(抗并行攻击)​​

​死亡三重奏:​​ 不仅能调算多久(时间)​、吃多大(内存)​,还能调几线程一起搞(并行度)​。全方位堵死硬件优化路线,想堆机器破解?先问问钱包君顶不顶得住!

Argon2 变体选谁?—— Argon2id 无脑冲!

更抗GPU,但可能被高手偷窥侧信道(安全场景慎用)

免疫侧信道攻击,但抗GPU稍软

前一半时间用 Argon2i(防偷窥),后一半切 Argon2d(抗GPU)​。​攻防一体!​​ NIST 官方力推,现代应用无脑选它准没错!

为什么说它比 bcrypt/scrypt 更猛?​​

选手抗GPU/ASIC防偷窥(侧信道)内存依赖度官方背书一句话判词
bcrypt ​⚠️ 弱(堆显卡可破)✅ 好(默认免疫)❌ ​很低!​​ (主要耗CPU)❌ 过气老将​显卡一上,原地破防! ​
scrypt​✅ 强(内存墙)✅ 好(基本免疫)✅ 高⚠️ 还行很强,但没拿总冠军🥈
​Argon2id​✅ ​卷王!​​(内存+时间+线程混合双打)✅ ​顶配!​​(混合防御)✅ ​超高+可调爆表​✅ ​NIST力推 🏆​全方位碾压,黑客看了想转行!

宗旨:在你的服务器能承受的极限内,让黑客的硬件成本最大化!​​ 后面实战环节手把手教你配!

实战!Spring Security 喂饱 Argon2id,密码安全稳如泰山!

核心目标:用户密码存库时,必须被 Argon2id 炼成“骨灰”;登录后,JWT双令牌搞起,让体验和安全我全都要!​​

引入核武器库

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.10</version>
</dependency>

<!-- Bouncy Castle (Argon2的底层依赖) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.72</version>
</dependency>

如果你的项目没有使用到 Spring Security,那你也可以选择只导入:

1
2
3
4
5
6
7
<!-- Spring Security Crypto -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<!-- 版本应与Spring Boot匹配 -->
<version>5.7.11</version>
</dependency>

核弹级密码工具类(Argon2id 炼金炉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.stereotype.Component;

/**
* 密码工具类
*
* @author youfeng
* @version 2025-07-14 22:52:36
*/
@Component
public class PassWordUtils {

/**
* 盐长度(给的32字节)
*/
private static final int SALT_LENGTH = 32;
/**
* 哈希值长度 (默认64,够用)
*/
private static final int HASH_LENGTH = 64;
/**
* 并行线程数 (普通服务器2够用了)
*/
private static final int PARALLELISM = 2;
/**
* 内存成本 (给的64MB)
*/
private static final int MEMORY = 65536;
/**
* 迭代次数 (目标:耗时0.5-1秒!)
*/
private static final int ITERATIONS = 5;


private static final Argon2PasswordEncoder ENCODER = new Argon2PasswordEncoder(
SALT_LENGTH,
HASH_LENGTH,
PARALLELISM,
MEMORY,
ITERATIONS
);

public String encode(String rawPassword) {
return ENCODER.encode(rawPassword);
}

public boolean matches(String rawPassword, String encodedPassword) {
return ENCODER.matches(rawPassword, encodedPassword);
}

public static PasswordEncoder getEncoder() {
return ENCODER;
}

}

Spring Security 配置 —— 武装到牙齿!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* Security 配置
*
* @author youfeng
* @version 2025-07-14 23:19:58
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class WebSecurityConfig {

private final CustomUserDetailsService userDetailService;

private final JwtAuthenticationEntryPoint unauthorizedHandler;

private final JwtAccessDeniedHandler accessDeniedHandler;

private final TokenProvider tokenProvider;

@Bean
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(true);
provider.setUserDetailsService(userDetailService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}

private JWTConfigurer securityConfigurerAdapter(AuthenticationManager authenticationManager) {
return new JWTConfigurer(tokenProvider, authenticationManager);
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AuthenticationManager authenticationManager) throws Exception {

http
.headers(headers -> headers.frameOptions().disable())
.exceptionHandling(handling -> handling
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(unauthorizedHandler)
)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(request ->
HttpMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())
).permitAll()

.requestMatchers(
new AntPathRequestMatcher("/401"),
new AntPathRequestMatcher("/404"),
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/register")
// ...
).permitAll()
.anyRequest().authenticated()
)
.apply(securityConfigurerAdapter(authenticationManager));

return http.build();
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(
new AntPathRequestMatcher("/doc.html"),
new AntPathRequestMatcher("/swagger-ui.html"),
);
}

@Bean
public PasswordEncoder passwordEncoder() {
return PassWordUtils.getEncoder();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig
) throws Exception {
return authConfig.getAuthenticationManager();
}

@Bean
public HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedPercent(true);
return firewall;
}

@Bean
public TokenProvider tokenProvider() {
return new TokenProvider();
}
}

流程图

1
2
3
用户提交密码 → Spring Security → DaoAuthenticationProvider → 
↓ ↓
CustomUserDetailsService (查数据库) Argon2PasswordEncoder (验密码骨灰)

用户查询服务(从数据库捞人)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class CustomUserDetailsService implements UserDetailsService {

private final UserService userService;

@Override
public User loadUserByUsername(String username) {

UserDTO userInfo = userService.getUserByLoginName(username);


if (userInfo == null) {
throw new AccountExpiredException (ErrorConstants.LOGIN_ERROR_NOTFOUND);
}
if (userInfo == null) {
throw new AccountExpiredException (ErrorConstants.LOGIN_ERROR_NOTFOUND);
}

List <GrantedAuthority> authorities = new ArrayList <> ();

return new User (
userInfo.getLoginName (),
userInfo.getPassword(),
authorities
);
}
}

登录/注册接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 用户登录(Security 全自动验证)
*
* @param loginForm
* @return
*/
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginForm loginForm) {
String username = PassWordUtils.desEncrypt(loginForm.getUsername());
String password = PassWordUtils.desEncrypt(loginForm.getPassword());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDTO userDTO = UserUtils.getByLoginName(username);
return new ResponseUtil().add(TokenProvider.TOKEN, TokenProvider.createAccessToken(username, userDTO.getPassword()))
.add(TokenProvider.REFRESH_TOKEN, TokenProvider.createRefreshToken(username, userDTO.getPassword())).ok();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 模拟一个注册方法(懒得写了。。。)
* @param loginForm
* @return
*/
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody LoginForm loginForm) {
if (userService.existsByUsername(loginForm.getUsername())) {
return ResponseUtil.fail("用户名已被注册");
}

String encryptedPassword = PassWordUtils.encode(loginForm.getPassword());
User user = User.builder()
.username(loginForm.getUsername())
.password(encryptedPassword)
.build();
userService.save(user);
return ResponseEntity.ok("注册成功");
}

神级解析

authenticationManager.authenticate() 会:

触发 CustomUserDetailsService 查数据库 → ​拿到Argon2id哈希值​

触发 Argon2PasswordEncoder.matches() → ​对比表单密码和数据库哈希值​

验证成功 → ​返回认证对象​

​全程你不用碰密码!Security 全包了!​​

关键避坑指南

避坑指南

字段长度别抠门!​​ password 字段至少 ​varchar(255)​,存不下哈希串直接悲剧!

参数调校是门艺术!​​

  • 迭代次数 (ITERATIONS) 靠 ​压测定生死​(0.5~1秒!)
  • 内存 (MEMORY) 在 ​服务器崩与不崩之间反复试探
  • 线上用 ​性能监控​ 盯着,别让登录接口成性能黑洞!

盐值别手贱!​​ Argon2PasswordEncoder ​自动管理盐值,别自己拆字段存!

无状态声明!​​ 配置里 SessionCreationPolicy.STATELESS 必须写死,不然Session偷袭你!

性能优化:让 Argon2id 学会“看服务器脸色”行事

核心矛盾:安全要慢(卷死黑客),性能要快(别拖垮服务器)!​​
​终极解法:动态调参!服务器忙就怂一点,闲就往死里卷!​​

代码核心:动态调整 Argon2id 参数的“智能小脑”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
* 密码工具类
*
* @author youfeng
* @version 2025-07-14 22:52:36
*/
@Slf4j
@Component
public class PassWordUtils {

/* 固定算法参数 */
/**
* 盐值长度(32字节)
*/
private static final int SALT_LENGTH = 32;
/**
* 哈希值长度(64字节)
*/
private static final int HASH_LENGTH = 64;
/**
* 并行处理线程数
*/
private static final int PARALLELISM = 2;

/* 基准安全参数 */
/**
* 基准内存成本(64MB)
*/
private static final int BASE_MEMORY = 65536;
/**
* 基准迭代次数(目标耗时0.5-1秒)
*/
private static final int BASE_ITERATIONS = 5;

/* 动态调整边界 */
/**
* 最低迭代次数安全底线
*/
private static final int MIN_ITERATIONS = 3;
/**
* 最高迭代次数性能上限
*/
private static final int MAX_ITERATIONS = 10;
/**
* 最低内存成本(32MB)
*/
private static final int MIN_MEMORY = 32768;
/**
* CPU高负载判定阈值(75%)
*/
private static final double HIGH_LOAD_THRESHOLD = 0.75;
/**
* 负载检查间隔(30秒)
*/
private static final int LOAD_CHECK_INTERVAL = 30000;

/* 动态运行时状态 */
private static volatile Argon2PasswordEncoder encoder;
/**
* 当前迭代次数
*/
private static volatile int currentIterations = BASE_ITERATIONS;
/**
* 当前内存成本
*/
private static volatile int currentMemory = BASE_MEMORY;
/**
* 最后一次负载检查时间
*/
private static volatile long lastLoadCheck = System.currentTimeMillis();
/**
* 锁对象
*/
private static final ReentrantLock CONFIG_LOCK = new ReentrantLock();

private PassWordUtils() {
}

/**
* 获取动态配置的Argon2编码器实例
* 自动触发负载检测和参数调整(每次调用时判断)
*/
public static Argon2PasswordEncoder getEncoder() {
// 检测是否到达配置更新检查点
if (System.currentTimeMillis() - lastLoadCheck > LOAD_CHECK_INTERVAL) {
adjustParameters();
}
return encoder;
}

/**
* 创建Argon2PasswordEncoder实例
* 使用当前动态参数配置
*/
private static Argon2PasswordEncoder createEncoder() {
return new Argon2PasswordEncoder(
SALT_LENGTH,
HASH_LENGTH,
PARALLELISM,
currentMemory,
currentIterations
);
}

/**
* 动态调整Argon2性能参数
* 基于服务器CPU负载状态智能调优
*/
private static void adjustParameters() {
final long currentTime = System.currentTimeMillis();
if (currentTime - lastLoadCheck <= LOAD_CHECK_INTERVAL) {
return;
}

double currentLoad = getCurrentCpuLoad();

int newIterations;
int newMemory;

if (currentLoad > HIGH_LOAD_THRESHOLD) {
newIterations = Math.max(MIN_ITERATIONS, currentIterations - 1);
newMemory = Math.max(MIN_MEMORY, currentMemory / 2);
} else if (currentLoad < HIGH_LOAD_THRESHOLD / 2) {
newIterations = Math.min(MAX_ITERATIONS, currentIterations + 1);
newMemory = Math.min(BASE_MEMORY * 2, currentMemory * 2);
} else {
newIterations = BASE_ITERATIONS;
newMemory = BASE_MEMORY;
}

if (newIterations != currentIterations || newMemory != currentMemory) {
applyNewConfiguration(newIterations, newMemory, currentTime, currentLoad);
} else {
updateLastCheckTime(currentTime);
}
}

/**
* 应用新配置(双检锁)
*/
private static void applyNewConfiguration(int iterations, int memory, long timestamp, double cpuLoad) {
CONFIG_LOCK.lock();
try {
if (timestamp - lastLoadCheck > LOAD_CHECK_INTERVAL) {
currentIterations = iterations;
currentMemory = memory;
encoder = createEncoder();
lastLoadCheck = timestamp;
log.info("Argon2动态配置更新 | CPU: {}% → 迭代: {}, 内存: {}MB",
String.format("%.1f", cpuLoad * 100),
iterations,
memory / 1024);
}
} finally {
CONFIG_LOCK.unlock();
}
}

/**
* 更新最后检测时间(线程安全)
*/
private static void updateLastCheckTime(long timestamp) {
CONFIG_LOCK.lock();
try {
lastLoadCheck = timestamp;
} finally {
CONFIG_LOCK.unlock();
}
}

/**
* 获取当前系统CPU负载
* 返回0.0-1.0之间的浮点数
*/
private static double getCurrentCpuLoad() {
try {
return ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class)
.getSystemCpuLoad();
} catch (Exception ex) {
// 监控异常时返回安全值
return 0.5;
}
}
}

动态策略图解:服务器状态 vs 安全强度

服务器状态CPU负载Argon2id动作安全强度性能影响
🔥 高负载(CPU >75%)​​快撑不住了!​降级! 迭代↓ 内存砍半↓⚠️ 中等✅ 大幅缓解
​😐 中负载(35%~75%)​​还能扛基准​ 用默认参数✅ 强⚖️ 平衡
🌪️ 低负载(CPU <35%)​​闲得抠脚​狂暴! 迭代↑ 内存翻倍↑🛡️ 超强⚠️ 略慢但可接受

智能效果

流量高峰时:​​ 自动降参数 → ​登录接口不崩,用户体验保住!​​

夜深人静时:​​ 自动加参数 → ​安全拉满,黑客想破头!

日常跑量时:​​ 稳定基准 → ​安全与性能的完美平衡点!​​

落地避坑指南

边界值谨慎设置

MIN_ITERATIONS 别低于3,否则不如用MD5!

MIN_MEMORY 建议32MB(32768),再低抗GPU能力暴跌!

侦察时间别太短

30秒(LOAD_CHECK_INTERVAL)是个平衡点,太频繁(如1秒)反而增加开销!

​突发流量怎么办?​​

动态调参是缓解,不是根治!真正的大流量要用水平扩容+限流​!

结语:密码安全——用动态的盾,挡最狠的刀

记住三句真言:​​
1️⃣ ​密码不是秘密,是数学的尸体!​​
—— Argon2id 炼成的「哈希骨灰」,才是对抗泄露的终极形态
2️⃣ ​安全不是摆设,是动态的战争!​​
—— 智能调参让 Argon2id 白天当保安,晚上变战神
3️⃣ ​性能不是借口,是精妙的平衡!​​
—— 服务器喘气时疯狂加码,流量爆炸时灵活缩骨

git commit -am “让密码安全,卷起来!”

  • 标题: 密码存储安全设计与实践指南
  • 作者: HYF
  • 创建于 : 2025-07-14 21:06:19
  • 更新于 : 2025-07-15 00:33:19
  • 链接: https://youfeng.ink/argon2id-implementation-978eeb601b15/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。