每次看到 MD5
、明文 甚至 base64
存储的密码,心就凉半截。密码安全是系统根基,存不好,一出事就是大事。
这篇就来讲讲怎么安全地存密码。不整虚的,核心就两条:用对算法 + 关键实践做到位。这里会重点安利目前公认最强的 Argon2id
,并给出一套实际落地方案。
还会聊聊性能怎么调,毕竟安全也不能把服务器跑垮了…
密码存安全?先记住两条铁律
明文存密码?直接抬走!
甭管你数据库用了啥华丽加密(AES
啥的),存用户原始密码就是作大死。
运维手滑、备份泄露、甚至内部人使坏… 分分钟密码裸奔。一出事?用户完蛋,你也离凉凉不远了😇
“加密”救不了密码狗命!
别一听“加密”就觉得安全了!对称加密(AES这种)根本不配存密码!
为啥?
- 钥匙丢了全完蛋: 密钥管理是大坑,泄露风险高。
- 思路歪了: 加密是“藏着掖着”,咱密码需要的是“挫骨扬灰”(不可逆)
真·救命稻草是 哈希(Hashing):
- 把密码一顿数学猛操作,变成独一无二的乱码串(哈希值)。
- 核心魔法:基本算不出原密码!(不可逆是王道)
- 数据库就算被搬空,黑客拿到手的也是这堆“骨灰”,想还原?难如登天!
咱就是说,专业的事情交给专业的人来做好嘛?
算法选型?长江前浪推后浪…
传统哈希(MD5、SHA-1)早该进垃圾桶了!计算快得像火箭🚀—— 这正是黑客暴力破解的最爱。现在玩的是 慢哈希 —— 核心思想:故意让算哈希变慢、变耗资源,破解成本upup
慢哈希三剑客
算法 | 怎么卷死黑客? | 一句话点评 |
---|
bcrypt | 主打“死命算”(可调迭代轮数) | 老牌劲旅,稳,但硬件扛性渐吃力 |
scrypt | 主打“狂吃内存”+“死命算” | 内存墙高手,让定制硬件也肉疼 |
Argon2id | 内存+计算双重狂暴! (抗GPU/ASIC卷王) | 卫冕冠军,全方位抗揍,现代首选👑 |
别问了,直接拉踩!
算法 | 抗硬件破解能力 | 防偷窥能力(抗侧信道) | 官方(NIST)盖章 | 结论 |
---|
MD5/SHA1 | ❌ 秒跪 | ❌ 裸奔 | ❌ 凉凉 | 坟头草三米高 |
bcrypt | ⚠️ 能撑几拳 | ✅ 还行 | ❌ 不推荐了 | 老将迟暮 |
PBKDF2 | ⚠️ 吃老本 | ✅ 还行 | ✅ 还行(旧爱) | 保底备胎 |
Argon2id | ✅ 卷死硬件💪 | ✅ 铜墙铁壁 | ✅ 力推! | 无脑首选! |
划重点:
闭眼 Argon2id
! 别留恋 bcrypt
了。至少上 PBKDF2
! MD5/SHA1
赶紧铲了!
参数怎么调?(核心就是一个字:慢!但别慢死自己)
每个算法都有“难度旋钮”(内存大小、迭代次数、线程数等),后面讲 Argon2id
实操会细调。宗旨:在你能承受的最慢边缘疯狂试探!(让爆破/破解成本最大化)
Argon2:密码哈希卷王!
一句话总结:它用内存墙 + 算力地狱双重暴击,专治各种不服(GPU、ASIC、彩虹表)!
凭什么选它?—— 一场官方认证的登基
👑 2015年密码哈希大赛冠军:不是野鸡奖!全球密码学大佬组局 PK,Argon2
干翻所有对手登基(bcrypt
、scrypt
都是手下败将)。
🚨 专为现代硬件杀手而生:早年的 MD5、bcrypt 碰见如今的 显卡矿场级破解,就像小米加步枪打航母——白给!Argon2 生来就为对抗这些 暴力怪兽。
核心理念:卷!往死里卷破解/爆破的成本!
(别误会,不是卷程序员!参数可调,后面教你摸鱼调参大法)
核心战术:拖延战!
普通哈希眨眼就算完(MD5
:我快死了,别拿我存密码!),Argon2
故意搞得很慢很慢(几百毫秒一级)。想象你试 100 亿个密码?等着天荒地老 + 电费爆炸吧!
杀手锏:疯狂吃内存!
传统算法(如 bcrypt
)主要卷 CPU
,但黑客能用超多显卡并行狂飙(成本骤降)。
Argon2
笑了:小样!给我狂吃内存!
破解时必须占用海量+连续内存(调参能上几百MB/GB)
直接废掉显卡(显存小)+ ASIC矿机(内存成本高)的武功!显存不够?卡死你
死亡三重奏: 不仅能调算多久(时间)、吃多大(内存),还能调几线程一起搞(并行度)。全方位堵死硬件优化路线,想堆机器破解?先问问钱包君顶不顶得住!
Argon2 变体选谁?—— Argon2id 无脑冲!
更抗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
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>3.1.10</version> </dependency>
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15to18</artifactId> <version>1.72</version> </dependency>
|
如果你的项目没有使用到 Spring Security
,那你也可以选择只导入:
1 2 3 4 5 6 7
| <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> <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;
@Component public class PassWordUtils {
private static final int SALT_LENGTH = 32;
private static final int HASH_LENGTH = 64;
private static final int PARALLELISM = 2;
private static final int MEMORY = 65536;
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
|
@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
|
@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
|
@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
|
@Slf4j @Component public class PassWordUtils {
private static final int SALT_LENGTH = 32;
private static final int HASH_LENGTH = 64;
private static final int PARALLELISM = 2;
private static final int BASE_MEMORY = 65536;
private static final int BASE_ITERATIONS = 5;
private static final int MIN_ITERATIONS = 3;
private static final int MAX_ITERATIONS = 10;
private static final int MIN_MEMORY = 32768;
private static final double HIGH_LOAD_THRESHOLD = 0.75;
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() { }
public static Argon2PasswordEncoder getEncoder() { if (System.currentTimeMillis() - lastLoadCheck > LOAD_CHECK_INTERVAL) { adjustParameters(); } return encoder; }
private static Argon2PasswordEncoder createEncoder() { return new Argon2PasswordEncoder( SALT_LENGTH, HASH_LENGTH, PARALLELISM, currentMemory, currentIterations ); }
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(); } }
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 “让密码安全,卷起来!”