当前位置: 首页 > 科技观察

SpringBoot密码加密的两种姿势!

时间:2023-03-12 17:58:32 科技观察

首先要注意的是:密码无法解密。大家别问宋格伟人事项目里的密码怎么解密啊!密码不可解密,或保证系统安全。宋哥今天就来和大家聊一聊如何处理密码,才能最大程度的保证我们系统的安全。1、为什么要加密?2011年12月21日,有人在网上公布了一个包含600万CSDN用户资料的数据库。数据全部以纯文本形式存储,包括用户名、密码和注册的电子邮件地址。事发后,CSDN在微博、官网等渠道发表声明,解释称数据库于2009年备份,因不明原因泄露。目前已报警,并在官网公开致歉。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司都卷入了这起事件。整个事件中最令人震惊的是CSDN以明文形式存储用户密码。由于很多用户在多个网站使用同一个密码,网站密码的泄露会带来很大的安全隐患。因为前车之鉴,我们现在在做系统的时候,一定要对密码进行加密。这次泄露也留下了一些有趣的东西,特别是对于广大程序员来说设置密码。从CSDN泄露的文件中,人们发现了一些有趣的密码,比如:ppnn13%dkstFeb.1st这个密码的中文解析是:田玉默少十三余,二月初的豆蔻果头。csbt34.ydhl12s这个密码的中文解析是:三四点在水池上,还有一两只猫头鹰在叶底……等等,你会发现很多程序员的人文素养是还是很高的,让人惊叹。2、加密方案密码加密我们一般使用散列函数,也称散列算法、散列函数,它是一种从任何数据中创建数字“指纹”的方法。哈希函数将消息或数据压缩成一个摘要,使数据量变小,固定数据的格式,然后对数据进行打乱和混合,重新创建一个哈希值。哈希值通常由一小串随机字母和数字表示。一个好的散列函数在输入字段中的散列冲突很少。在哈希表和数据处理中,不抑制冲突来区分数据使得数据库记录更难查找。我们常用的哈希函数有MD5消息摘要算法和安全哈希算法(SecureHashAlgorithm)。然而,仅仅使用散列函数是不够的。单纯使用哈希函数,如果两个用户密码的明文相同,生成的密文也相同,增加了密码泄露的风险。为了增加密码的安全性,一般需要在密码加密过程中加盐。所谓盐可以是一个随机数,也可以是一个用户名。加盐后,相同密码明文的用户生成的密码密文是不同的,可以大大提高密码的安全性。传统的加盐方式需要数据库中有一个专门的字段来记录加盐值。这个字段可能是用户名字段(因为用户名是唯一的),也可能是专门用来记录salt值的字段。这样的配置很麻烦。SpringSecurity提供了多种密码加密方案。官方推荐使用BCryptPasswordEncoder。BCryptPasswordEncoder使用BCrypt强哈希函数。开发者在使用时可以选择提供strength和SecureRandom实例。strength越大,key的迭代次数越多,key的迭代次数为2^strength。strength的值在4~31之间,默认为10。与Shiro需要自己处理密码加盐不同,在SpringSecurity中,BCryptPasswordEncoder自带加盐,处理起来非常方便。3.实践3.1codec加密commons-codec是Apache上的一个开源项目,可以用来方便的实现密码加密。松哥在V部落项目(https://github.com/lenve/VBlog)中采用了这种方案。在SpringSecurity推出BCryptPasswordEncoder之前,commons-codec还是比较常见的方案。所以,这里先给大家介绍下commons-codec的使用方法。首先需要引入commons-codec的依赖:commons-codeccommons-codec1.11然后customizeaPasswordEncoder:@ComponentpublicclassMyPasswordEncoderimplementsPasswordEncoder{@OverridepublicStringencode(CharSequencerawPassword){returnDigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());}@Overridepublicbooleanmatches(CharSequencerawPassword,StringencodedPassword){returnencodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));}}在SpringSecurity中,PasswordEncoder专门用来处理密码的加密和比较。我们自定义MyPasswordEncoder,实现PasswordEncoder接口。我们还需要在这个接口中实现两个方法:encode方法是对密码进行加密Encryption,参数rawPassword是你传入的明文密码,返回是加密后的密文。这里的加密方案使用MD5。matches方法意味着比较密码。参数rawPassword相当于用户登录时传入的密码,encodedPassword相当于加密后的密码(从数据库中查询)。最后记得通过@Component注解将MyPasswordEncoder标记为Spring容器中的一个组件。这样,当用户登录时,会自动调用matches方法进行密码比对。当然,使用MyPasswordEncoder后,用户注册时,密码需要加密保存到数据库中,如下:publicintreg(Useruser){...//插入用户,插入前加密密码user.setPassword(passwordEncoder.encode(user.getPassword()));result=userMapper.reg(user);...}其实很简单,调用encode方法加密密码即可。完整代码可以参考V部落(https://github.com/lenve/VBlog),这里不再赘述。3.2BCryptPasswordEncoder加密但是自己定义PasswordEncoder还是有点麻烦,尤其是在处理密码加盐的时候。所以SpringSecurity中提供了BCryptPasswordEncoder,使得密码加密加盐变得非常简单。只需要提供一个BeanBCryptPasswordEncoder的实例,这是微人氏(https://github.com/lenve/vhr)采用的,如下:@BeanPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder(10);}创建BCryptPasswordEncoder时通过输入参数10为strength,即key的迭代次数(也可以不配置,默认为10)。同时配置的内存用户密码不再是123,如下:auth.inMemoryAuthentication().withUser("admin").password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq").roles("ADMIN","USER").and().withUser("sang").password("$2a$10$eUHbAOMq4bpxTvOVz33LIehLe3fu6NwqC9tdOcxJXEhyZ4simqXTC").roles("USER");这里的密码是通过BCryptPasswordEncoder加密的,虽然admin和sang加密后的密码不同,但是明文都是123,配置完成后,使用admin/123或者sang/123登录,本例使用内存中配置的用户.一般用户信息保存在数据库中,所以用户注册时需要对密码进行加密,如下:@ServicepublicclassRegService{publicintreg(Stringusername,Stringpassword){BCryptPasswordEncoderencoder=newBCryptPasswordEncoder(10);StringencodePasswod=encoder.encode(password);returnsaveToDb(username,encodePasswod);}}用户从前端传来密码后,调用BCryptPasswordEncoder实例中的encode方法对密码进行加密。密文存储在数据库中。4.源码分析最后,我们再来看一下PasswordEncoder。publicinterfacePasswordEncoder{Stringencode(CharSequencerawPassword);booleanmatches(CharSequencerawPassword,StringencodedPassword);defaultbooleanupgradeEncoding(StringencodedPassword){returnfalse;}}encode方法用于对密码进行加密。matches方法用于比较密码。upgradeEncoding表示密码是否需要再次加密,使密码更安全,默认为false。SpringSecurity为PasswordEncoder提供了很多实现:但是说实话,自从有了BCryptPasswordEncoder之后,我们很少去关注其他的实现类。PasswordEncoder中的encode方法是在用户注册时手动调用的。matches方法由系统调用,默认在DaoAuthenticationProvider#additionalAuthenticationChecks方法中调用。protectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,UsernamePasswordAuthenticationTokenauthentication)throwsAuthenticationException{if(authentication.getCredentials()==null){logger.debug("Authenticationfailed:nocredentialsprovided");thrownewBadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider}");"badCredentials")StringpresentedPassword=authentication.getCredentials().toString();if(!passwordEncoder.matches(presentedPassword,userDetails.getPassword())){logger.debug("认证失败:密码不匹配存储值");thrownewBadCredentialsException(messages.getCredentialsUserDateMessage("AbstractAbstract","Badcredentials"));}}可以看到密码比对是通过passwordEncoder.matches方法完成的。本文转载自微信公众号《江南的一场小雨》,可通过以下二维码关注。转载本文请联系江南一点鱼公众号。