
1. 项目缘起从一次“密码泄露”事件说起前阵子团队里一个刚接手维护老项目的同事在排查一个线上用户登录异常问题时无意中在日志里看到了明文密码。这事儿说大不大说小不小但着实把我们吓了一跳。项目用的是国内流行的快速开发框架Jeecg-Boot按理说这种基础的安全问题不应该出现。我们顺着日志回溯发现是某个老接口在调试时为了方便直接把前端传过来的密码用System.out.println打了出来后来功能上线这段调试代码忘了删。虽然这个接口调用频率极低且很快被修复但它像一根刺扎进了我们心里——我们真的了解在Jeecg-Boot里密码应该怎么被“使用”吗这里的“使用”远不止是调用BCryptPasswordEncoder加密一下那么简单。它贯穿了从用户输入、前端传输、后端接收、加密存储、比对验证到密码策略、重置流程、安全审计的完整链路。很多开发者包括曾经的我可能只关注了“加密存储”这一个环节却忽略了其他环节可能存在的风险点。比如前端是否做了基础过滤传输过程是否绝对安全即使在HTTPS下也有讲究加密算法和强度是否足够密码比对是否存在时序攻击风险重置密码的链接如何防爆破日志、异常信息里是否会无意泄露密码这些问题任何一个环节的疏忽都可能让“加密存储”的努力付诸东流。因此这篇文章我想结合在Jeecg-Boot项目中的多次实践、踩坑和优化经历系统地梳理一下“密码的使用”这个看似基础实则暗藏玄机的话题。我们不谈空洞的理论只聚焦于Jeecg-Boot这个具体的技术栈下那些你必须知道、必须做对的实操细节和避坑指南。无论你是Jeecg-Boot的新手还是正在维护一个存量的Jeecg-Boot项目相信这些经验都能帮你构建起更稳固的密码安全防线。2. 核心安全观密码的完整生命周期与Jeecg-Boot的默认实现在动手改代码之前我们必须先建立正确的认知密码安全是一个覆盖其完整生命周期的系统性工程。这个生命周期通常包括创建、传输、验证、存储、重置和废止。Jeecg-Boot作为一个优秀的开源框架在它的“开箱即用”配置中已经为我们做了不少安全工作但了解其原理和边界是我们进行定制和强化安全的前提。2.1 Jeecg-Boot的密码处理核心Spring Security BCryptJeecg-Boot的后台安全模块深度集成了Spring Security。在用户密码的核心处理上它默认使用了BCryptPasswordEncoder作为密码编码器。这是一个非常明智且强大的选择。为什么是BCrypt这背后有几个关键考量自适应哈希算法BCrypt算法内部有一个“工作因子”work factor的概念通常表示为强度strength或迭代次数rounds。这个因子可以随着硬件计算能力的提升而增加从而使得暴力破解的成本始终保持在很高的水平。在Jeecg-Boot的默认配置中这个强度通常是10对应2^10次哈希迭代。这意味着即使多年后服务器算力大增我们也可以通过简单地提高这个因子来维持密码的安全性而无需让用户修改密码。内置盐值SaltBCrypt在哈希过程中会自动生成一个随机的盐值并将其与哈希结果一起存储。这意味着即使两个用户的密码完全相同他们存储在数据库中的哈希值也完全不同。这有效防御了彩虹表攻击。算法成熟度BCrypt经受住了长时间的安全社区考验被认为是目前存储密码的首选算法之一。相比MD5、SHA-1甚至SHA-256等普通哈希算法它在设计上就慢得多慢是优点增加破解成本并且专为密码哈希而设计。在Jeecg-Boot的代码中你通常能在配置类里找到类似下面的Bean定义Bean public PasswordEncoder passwordEncoder() { // 默认强度为10 return new BCryptPasswordEncoder(); }这个PasswordEncoderBean会被Spring Security自动用于所有密码的编码和匹配操作。2.2 默认流程的“安全边界”理解了核心组件我们再来看看Jeecg-Boot默认提供的用户管理模块sys_user表及相关API是如何运作的注册/修改密码当通过前端调用/sys/user/add或/sys/user/edit接口时前端会将明文密码传到后端。后端Controller接收到SysUser对象后会调用Service层。在Service层中框架通常会通过AOP或直接调用passwordEncoder.encode(rawPassword)方法将明文密码转换为BCrypt哈希值然后存入数据库的password字段。这里有一个关键点默认情况下这个转换过程发生在业务Service中而不是在Controller或更早的层。这意味着明文密码在进入Service层之前在内存中是存在的。登录验证用户登录时调用/sys/login接口这背后是Spring Security的UsernamePasswordAuthenticationFilter。过滤器会获取用户名和密码然后Spring Security会使用我们配置的PasswordEncoder的matches(rawPassword, encodedPassword)方法将用户输入的明文密码与数据库中存储的哈希值进行比对。BCryptPasswordEncoder.matches()方法是精心设计的能有效防止基于响应时间的时序攻击。密码重置Jeecg-Boot通常提供了“忘记密码”功能流程是用户输入用户名或邮箱 - 系统发送包含令牌Token的链接到邮箱 - 用户点击链接进入重置页面 - 输入新密码并提交。这里的令牌需要是随机的、一次性且有时效的。那么默认实现的“安全缺口”可能在哪里传输层依赖HTTPS框架默认不强制也不处理传输加密这完全依赖于部署时的HTTPS配置。如果生产环境误用了HTTP所有密码在传输过程中都是裸奔。明文密码的短暂存在如上所述在Controller到Service的间隙明文密码存在于Java对象中。如果此时发生内存转储尽管概率极低或者有恶意的调试代码如开篇的例子就会泄露。默认密码策略较弱框架可能没有强制要求密码复杂度长度、大小写、数字、特殊字符。重置令牌的安全令牌的生成算法、存储方式和失效机制需要检查是否足够健壮。日志泄露这是最常见的坑。Spring Security、业务代码或第三方组件可能会将包含敏感信息的请求参数记录到日志中。认识到这些边界我们接下来的工作就有了明确的方向在认可和利用框架已有安全能力的基础上查漏补缺构建纵深防御。3. 纵深防御实践从传输到存储的强化策略了解了默认实现的原理与边界后我们可以着手构建更坚固的防线。安全领域讲究“纵深防御”即不依赖单一安全措施而是在多个层面设置障碍。下面我们就从密码生命周期的各个环节逐一探讨如何在Jeecg-Boot中实施强化。3.1 传输安全强制HTTPS与敏感参数脱敏1. 生产环境强制HTTPS这是最基本也是最重要的一条。在application-prod.yml中你应该配置服务器强制使用HTTPS并将HTTP请求重定向。server: port: 443 ssl: key-store: classpath:keystore.p12 key-store-password: your-strong-password key-store-type: PKCS12 key-alias: your-alias # 如果使用Tomcat还可以配置连接器重定向同时在Nginx或Apache等反向代理层也应配置强制HTTPS和HSTS头确保浏览器始终通过安全连接通信。2. 敏感参数日志脱敏这是防止“开篇事件”重演的关键。我们需要确保在任何情况下密码都不会被记录到日志文件。主要有以下几个地方需要处理Spring Boot Actuator端点如果开启了/actuator/httptrace等端点它们会记录请求详情需要过滤。应用业务日志在Controller或Service中严禁使用log.debug(“收到用户密码{}”, user.getPassword())这样的代码。更好的做法是在接收到SysUser对象后立即将其密码字段置空或替换为占位符再记录日志。全局日志过滤器最彻底的方式是创建一个Servlet Filter或Spring MVC的HandlerInterceptor在请求进入Controller之前就对特定参数如password、newPassword、oldPassword等进行脱敏处理。你可以修改请求参数副本将值替换为******这样后续所有日志组件看到的都是脱敏后的值。Jeecg-Boot本身可能提供了类似的过滤器需要检查或自定义。一个简单的拦截器示例思路Component public class SensitiveParamInterceptor implements HandlerInterceptor { private static final SetString SENSITIVE_PARAMS Set.of(password, newPassword, oldPassword, passwd, pwd); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request instanceof ContentCachingRequestWrapper) { // 对参数进行脱敏处理这里需要根据实际请求类型表单、JSON解析并处理 // 这是一个复杂但一劳永逸的方案 } // 更简单的方式在后续的业务代码中对特定DTO的字段进行注解标记结合AOP在序列化日志时脱敏 return true; } }实操心得对于JSON请求体修改起来比较麻烦更实用的做法是在业务代码的入口处Controller方法一开始就进行脱敏。定义一个注解如Sensitive标注在SysUser的password字段上然后利用Jackson的序列化器或自定义AOP在打印这个对象日志时自动将对应字段替换为星号。3.2 存储安全强化BCrypt与密码策略1. 调整BCrypt强度如前所述BCrypt的强度因子是可调的。对于新的项目可以考虑将强度从默认的10提高到12。这会使哈希计算时间稍微变长可能从几十毫秒增加到一百多毫秒但对登录体验影响微乎其微却极大地增加了暴力破解的难度。Bean public PasswordEncoder passwordEncoder() { // 使用强度12 return new BCryptPasswordEncoder(12); }注意修改这个强度后新注册或修改密码的用户会使用新强度进行哈希。但数据库中已存在的、用旧强度10哈希的密码在验证时依然可以正常通过因为BCryptPasswordEncoder.matches()方法能够识别并处理不同强度的哈希值。这是一个非常友好的特性允许我们平滑升级。2. 实施密码复杂度策略Jeecg-Boot的用户管理模块可能没有内置强制的密码复杂度检查。我们需要在用户注册和修改密码的地方加入校验。前端校验使用正则表达式在用户输入时进行实时提示提升用户体验。但这不能替代后端校验。后端校验在Service层执行密码保存逻辑之前添加校验规则。推荐使用成熟的库如org.passay它提供了丰富的密码规则定义。import org.passay.*; public class PasswordPolicyValidator { private final PasswordValidator validator; public PasswordPolicyValidator() { validator new PasswordValidator( // 长度规则8-30位 new LengthRule(8, 30), // 至少一个大写字母 new CharacterRule(EnglishCharacterData.UpperCase, 1), // 至少一个小写字母 new CharacterRule(EnglishCharacterData.LowerCase, 1), // 至少一个数字 new CharacterRule(EnglishCharacterData.Digit, 1), // 至少一个特殊字符 new CharacterRule(EnglishCharacterData.Special, 1), // 不允许有空格 new WhitespaceRule() ); } public boolean isValid(String password) { PasswordData passwordData new PasswordData(password); RuleResult result validator.validate(passwordData); return result.isValid(); } }在Service中调用该校验器如果校验失败则抛出明确的业务异常告知用户密码不符合强度要求。3. 密码历史与重复使用对于安全性要求更高的系统可以防止用户在一段时间内重复使用旧密码。这需要在数据库中记录用户的密码历史存储密码哈希值而非明文。当用户修改密码时检查新密码的哈希值是否与历史记录中的任何一条匹配。Jeecg-Boot没有默认实现此功能需要自行扩展sys_user表或创建新表并在修改密码的Service中增加校验逻辑。3.3 验证与会话安全防御暴力破解与会话固定1. 登录失败锁定与延迟这是防御在线暴力破解的关键。Spring Security提供了相应的机制。账户锁定可以实现一个AuthenticationFailureHandler在用户登录失败时记录失败次数。当同一用户名在短时间内如5分钟失败次数超过阈值如5次则将该账户临时锁定一段时间如15分钟并给出友好提示“账户已锁定请15分钟后重试或联系管理员解锁”。锁定信息可以存储在Redis或数据库里。增加延迟即使不锁定账户也可以在登录失败后故意增加一个随机的、短暂的延迟如1-3秒再返回响应。这能显著降低自动化脚本的尝试速度。注意这个延迟应该在验证用户名无效之后也进行否则攻击者可以通过判断响应时间来枚举有效用户名。2. 防范时序攻击幸运的是使用BCryptPasswordEncoder.matches()方法我们已经在密码比对环节防御了时序攻击。但需要注意的是用户是否存在的判断可能会引入时序差。如果用户不存在系统可能快速返回“用户不存在”如果用户存在但密码错误则经过BCrypt计算后返回“密码错误”。这个时间差可能被用来枚举有效用户名。因此无论用户是否存在都应该进行密码哈希比对可以对比一个固定的、无意义的哈希值使响应时间一致。3. 会话管理确保登录后的会话安全。使用安全的Cookie属性HttpOnly防止JS窃取、Secure仅HTTPS传输。设置合理的会话超时时间。在用户修改密码后应使其所有其他会话立即失效。这可以通过在修改密码成功后调用Spring Security的SessionRegistry获取该用户所有SessionInformation并执行expireNow()来实现。4. 密码重置流程的安全加固与常见陷阱密码重置功能是攻击者非常喜欢攻击的入口因为它往往绕过了主登录接口的防护如验证码、失败锁定。一个安全的密码重置流程必须考虑周全。4.1 安全的令牌生成与存储Jeecg-Boot的忘记密码功能其核心是一个发送到用户邮箱或手机的重置链接链接中包含一个令牌Token。这个令牌必须是足够随机使用密码学安全的随机数生成器如java.security.SecureRandom生成足够长的字符串如32位十六进制数或Base64编码字符串。一次性使用后立即失效。有时效性通常设置为15分钟到1小时过期。与用户关联在存储令牌时必须与用户ID、过期时间一起存储。常见陷阱使用可预测的令牌。绝对不要使用用户ID、时间戳的简单哈希或加密作为令牌。应该使用UUID或专门生成的随机字符串。存储方案推荐使用Redis存储并设置TTL生存时间。键可以设计为reset_token:{token}值为userId:timestamp。这样既能利用Redis的过期自动清理特性又能快速验证。如果使用数据库则需要一个单独的表并需要有定时任务来清理过期的令牌。4.2 重置接口的防爆破与限流重置密码的验证接口/sys/user/resetPassword和提交新密码的接口/sys/user/doResetPassword需要特别保护。令牌验证防爆破攻击者可能会尝试大量随机令牌来“撞库”。因此在验证令牌是否有效的接口中无论令牌有效与否返回的HTTP状态码和响应时间都应尽可能一致。例如无效令牌返回“链接已失效或错误”有效令牌进入重置页面但两者响应结构类似时间都经过一个固定的、短暂的延迟处理。提交新密码防爆破这个接口应该与主登录接口享受同等级别的防护。必须添加图形验证码并且对IP地址进行频率限制限流防止攻击者不断尝试用同一个令牌提交不同的新密码如果令牌未立即失效的话。Spring Security可以配合RateLimiter或使用网关层的限流功能实现。立即失效令牌在用户成功提交新密码后除了使当前使用的令牌失效外最好将该用户所有未使用的重置令牌一并失效。防止用户点击了邮件中的多个链接导致最后一个链接仍然有效。4.3 密码重置后的安全措施会话终止如上文所述密码修改后应立即使该用户的所有其他活跃会话失效。发送通知向用户的注册邮箱或备用邮箱/手机发送密码已修改成功的通知。如果这次修改不是用户本人操作的用户可以立即知晓并采取行动如通过“忘记密码”功能重置并联系管理员。避免在重置链接中预填用户名有些系统为了体验在重置链接里通过参数传递了用户名这可能导致用户名泄露。令牌本身应足以唯一标识一次重置请求。5. 运维与审计看不见的防线安全不仅仅是开发阶段的事情运维和审计同样重要。很多安全问题是在系统运行过程中暴露或产生的。5.1 数据库中的密码字段安全字段长度BCrypt哈希后的字符串长度是固定的60字符。确保数据库表中password字段的类型是varchar(100)或更长为未来可能的算法升级留有余地。禁止明文存储通过代码审查和数据库审计确保没有任何流程、任何Job会将密码以明文形式写入数据库的任何字段。这是一个底线。备份安全数据库备份文件同样包含密码哈希值。虽然哈希值不能直接反推密码但如果备份文件泄露攻击者可以对其进行离线暴力破解。因此数据库备份文件的存储和传输也必须加密。5.2 密钥与配置管理BCryptPasswordEncoder本身不需要密钥但系统中其他环节可能需要比如JWT签名密钥如果使用了JWT数据库连接密码邮件服务器密码第三方API密钥这些绝不能硬编码在代码中或明文写在application.yml文件里然后提交到Git。应该使用环境变量、配置中心如Apollo, Nacos或专门的密钥管理服务如HashiCorp Vault, AWS KMS来管理。在application.yml中应该使用占位符spring: datasource: password: ${DB_PASSWORD:} # 从环境变量获取冒号后为空表示没有默认值5.3 安全审计与监控日志审计记录所有与密码相关的重要操作但务必脱敏。需要记录的信息包括操作类型登录、修改密码、重置密码、操作结果成功/失败、时间戳、IP地址、用户代理User-Agent、关联的用户ID成功时。对于失败操作尤其是登录失败和重置密码失败要设置告警当短时间内同一IP或同一用户出现大量失败时通知管理员。定期扫描与渗透测试使用静态应用安全测试SAST工具扫描代码查找是否存在密码硬编码、不安全的哈希函数调用等漏洞。定期进行动态应用安全测试DAST或聘请专业团队进行渗透测试模拟攻击者的行为来发现重置流程、接口权限等方面的漏洞。依赖库检查定期使用OWASP Dependency-Check等工具检查项目依赖包括Spring Security、BCrypt的实现库等是否存在已知的安全漏洞并及时升级。6. 进阶考量与特定场景处理对于有更高安全要求的系统或者一些特殊场景我们还需要考虑更多。6.1 多因素认证MFA的集成对于管理员账户或高权限操作强烈建议启用多因素认证。Jeecg-Boot本身可能不直接提供但可以集成TOTP基于时间的一次性密码算法使用Google Authenticator或Authy等应用。基本思路是在用户启用MFA时后端生成一个密钥并生成一个二维码包含密钥和用户标识供用户扫描绑定。用户登录时在输入密码后还需输入Authenticator应用生成的6位动态码。后端使用相同的密钥和当前时间验证这个动态码。这需要在用户表增加MFA密钥字段并修改登录流程。虽然增加了复杂度但能极大提升账户安全性。6.2 密码的“加盐”前处理在将用户输入的密码交给BCryptPasswordEncoder之前有些场景下我们可能想先做一次“预哈希”或“规范化”。例如为了防止密码过长导致BCrypt计算时间过长可能成为拒绝服务攻击的向量可以先用SHA-256等快速哈希对超长密码进行一次哈希再将哈希值交给BCrypt。但这种做法需要极其谨慎因为它可能引入新的弱点并且破坏了BCrypt处理任意长度输入的能力。一般来说不推荐这样做更好的办法是在前端或网关层对输入长度进行合理的限制如最大128字符。6.3 第三方系统集成时的密码处理当你的Jeecg-Boot系统需要与另一个旧系统或第三方系统共享用户认证时即单点登录SSO的一部分密码处理会变得复杂。如果第三方系统也使用BCrypt且强度一致那么可以直接同步密码哈希值。这是最理想的情况。如果第三方系统使用其他哈希算法你无法将BCrypt哈希值转换回去。这时通常有两种方案密码同步在用户修改密码时同时用两套算法生成哈希值分别存入两个系统。这要求你拥有第三方系统的写权限且两个系统的密码修改必须原子性操作复杂度高。代理认证在你的Jeecg-Boot系统中存储BCrypt哈希用于自身认证。当需要登录第三方系统时由你的后端服务用用户输入的明文密码在安全的内存环境中去调用第三方系统的认证接口。这意味着明文密码需要在你的系统中短暂存在并用于网络请求风险较高需要确保通信信道如内部网络和第三方接口的安全性。最佳实践在新的架构中应优先考虑使用OAuth 2.0、OpenID Connect、SAML等标准的SSO协议彻底避免密码在系统间传递或共享。7. 实战排坑那些年我们踩过的“密码”坑理论说再多不如踩一次坑记得牢。分享几个在Jeecg-Boot项目中真实遇到过的、与密码相关的典型问题及其排查解决思路。7.1 坑一登录缓慢CPU飙升现象生产环境用户反馈登录偶尔特别慢监控发现应用服务器CPU在登录时偶尔会冲到100%。排查查看慢SQL日志登录相关的查询并无异常。查看应用日志发现登录请求的处理时间确实很长但日志点都打在业务代码里耗时不在数据库。使用Arthas或Async-Profiler对应用进行采样分析发现热点在BCryptPasswordEncoder.matches()方法上。根因与解决BCryptPasswordEncoder的强度work factor设置过高。在虚拟机或容器资源受限的环境下高强度如14以上的BCrypt计算会成为CPU密集型操作尤其在登录并发稍高时。虽然单个请求可能只多花100毫秒但并发起来就会导致线程池拥堵和CPU打满。临时方案适当降低BCrypt强度如从14降到12并扩容应用实例。根本方案对于高并发登录场景可以考虑将密码验证服务单独抽离甚至使用硬件加速但成本高。更务实的做法是确保测试环境的硬件配置与生产环境比例相当在性能测试中关注登录接口的响应时间和资源消耗。7.2 坑二“记住我”功能与密码修改后会话失效的冲突现象系统开启了“记住我”功能。用户A修改密码后理论上其他会话应该失效。但用户A发现用另一台电脑之前勾选了“记住我”仍然能自动登录。排查“记住我”功能通常基于持久化的remember-mecookie其包含用户名、过期时间和令牌。检查代码发现密码修改后只调用了使HTTP Session失效的逻辑session.invalidate()但没有清理“记住我”相关的令牌。根因与解决Spring Security的“记住我”实现如PersistentTokenBasedRememberMeServices会将令牌存储在数据库中。修改密码后必须同时清理该用户对应的所有“记住我”令牌。// 在修改密码成功的逻辑中 Autowired private PersistentTokenRepository persistentTokenRepository; // 需要配置 public void changePasswordSuccess(String username) { // ... 修改密码逻辑 ... // 1. 使当前session失效 (如果修改密码的是当前用户) // 2. 清除该用户的remember-me tokens persistentTokenRepository.removeUserTokens(username); }教训安全功能的联动要考虑周全。修改密码、账户锁定、管理员强制下线等操作都需要同时处理Session和Remember-Me Token。7.3 坑三导入用户数据时的密码初始化现象需要从旧系统批量导入用户数据到新的Jeecg-Boot系统。旧系统密码使用的是MD5哈希如何让用户能无缝登录排查与方案这是一个常见的迁移场景。粗暴地将MD5哈希值直接存入新系统的password字段是行不通的因为BCryptPasswordEncoder不认识MD5。方案A推荐强制首次登录重置密码。导入时将用户的密码字段设置为一个随机、复杂的临时密码或置空并标记该用户为“需重置密码”状态。用户首次登录时系统检查此状态强制跳转到密码重置页面。这种方式最安全但用户体验稍差。方案B兼容迁移实现一个过渡期的密码编码器。可以自定义一个PasswordEncoder在验证时先尝试用BCrypt匹配如果失败再尝试用MD5哈希用户输入的密码并与旧哈希值比对。如果MD5比对成功则用BCrypt重新哈希用户本次输入的密码更新数据库并返回认证成功。public class LegacyCompatiblePasswordEncoder implements PasswordEncoder { private final PasswordEncoder bcryptEncoder new BCryptPasswordEncoder(); private final PasswordEncoder md5Encoder new MessageDigestPasswordEncoder(MD5); Override public String encode(CharSequence rawPassword) { // 新密码一律用BCrypt编码 return bcryptEncoder.encode(rawPassword); } Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 1. 先用BCrypt尝试匹配新用户或已迁移用户 if (bcryptEncoder.matches(rawPassword, encodedPassword)) { return true; } // 2. 如果BCrypt不匹配尝试判断encodedPassword是否是MD5格式例如长度32的十六进制串 if (isMD5Hash(encodedPassword)) { // 用MD5比对 if (md5Encoder.matches(rawPassword, encodedPassword)) { // 匹配成功说明是旧用户首次登录。此时可以异步或同步地更新密码为BCrypt哈希。 // 注意更新数据库操作需要处理好事务和并发。 updatePasswordToBCrypt(rawPassword, encodedPassword); return true; } } return false; } private boolean isMD5Hash(String str) { ... } private void updatePasswordToBCrypt(CharSequence rawPassword, String oldMd5Hash) { ... } }注意方案B需要小心处理数据库更新时的并发问题两个请求同时用同一个旧密码登录并且要在所有用户都成功登录一次、密码被迁移后移除MD5的兼容逻辑。这是一个临时方案。密码安全无小事它在Jeecg-Boot项目里绝不是配置一个BCryptPasswordEncoder就万事大吉的活。它需要我们从威胁建模的角度出发审视密码流动的每一个环节从前端输入到网络传输从后端接收到加密存储从验证比对的算法选择到失败后的处理逻辑从重置流程的令牌安全到运维审计的方方面面。每个环节的疏忽都可能成为整个安全链条中最薄弱的一环。在实际开发中我最大的体会是“默认不安全”和“持续加固”两个原则。不要相信任何默认配置就是绝对安全的要带着审视的眼光去检查框架提供的每一个功能。同时安全措施也不是一蹴而就的随着业务发展、漏洞披露和攻击技术的演进我们需要定期回顾和加固这些措施比如调整BCrypt强度、增加新的密码策略、收紧重置令牌的时效等。最后再分享一个很实用但容易被忽略的小技巧在开发、测试环境可以使用一个固定的、已知的密码哈希值方便测试。例如配置两个PasswordEncoderBean根据环境变量切换。在生产环境用BCryptPasswordEncoder在测试环境用一个简单的NoOpPasswordEncoder仅用于测试绝对禁止用于生产或者一个已知的BCrypt哈希$2a$10$...。这样测试账号的密码可以统一写成test123而不用担心在不同环境需要不同的密码也避免了在测试代码中硬编码密码哈希的麻烦。当然这一切的前提是你能严格区分不同环境的配置并且确保这个“后门”永远不会被带到生产环境。