Spring Boot 中密码加密的两种方法
先说一句:密码是无法解密的。大家也不要再问松哥微人事项目中的密码怎么解密了!
密码无法解密,还是为了确保系统安全。今天松哥就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全。
1.为什么要加密
2011年12月21日,有人在网络上公开了一个包含600万个CSDN用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN在微博、官方网站等渠道发出了声明,解释说此数据库系2009年备份所用,因不明原因泄露,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于CSDN把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄露就会造成很大的安全隐患。由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。
这次泄密,也留下了一些有趣的事情,特别是对于广大程序员设置密码这一项。人们从CSDN泄密的文件中,发现了一些好玩的密码,例如如下这些:
- ppnn13%dkstFeb.1st这段密码的中文解析是:娉娉袅袅十三余,豆蔻梢头二月初。
- csbt34.ydhl12s这段密码的中文解析是:池上碧苔三四点,叶底黄鹂一两声
- ...
等等不一而足,你会发现很多程序员的人文素养还是非常高的,让人啧啧称奇。
2.加密方案
密码加密我们一般会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中创建数字“指纹”的方法。
散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,重新创建一个散列值。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。
我们常用的散列函数有MD5消息摘要算法、安全散列算法(SecureHashAlgorithm)。
但是仅仅使用散列函数还不够,单纯的只使用散列函数,如果两个用户密码明文相同,生成的密文也会相同,这样就增加的密码泄漏的风险。
为了增加密码的安全性,一般在密码加密过程中还需要加盐,所谓的盐可以是一个随机数也可以是用户名,加盐之后,即使密码明文相同的用户生成的密码密文也不相同,这可以极大的提高密码的安全性。
传统的加盐方式需要在数据库中有专门的字段来记录盐值,这个字段可能是用户名字段(因为用户名唯一),也可能是一个专门记录盐值的字段,这样的配置比较繁琐。
SpringSecurity提供了多种密码加密方案,官方推荐使用BCryptPasswordEncoder,BCryptPasswordEncoder使用BCrypt强哈希函数,开发者在使用时可以选择提供strength和SecureRandom实例。strength越大,密钥的迭代次数越多,密钥迭代次数为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-codec commons-codec 1.11
然后自定义一个PasswordEncoder:
@Component publicclassMyPasswordEncoderimplementsPasswordEncoder{ @Override publicStringencode(CharSequencerawPassword){ returnDigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()); } @Override publicbooleanmatches(CharSequencerawPassword,StringencodedPassword){ returnencodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes())); } }
在SpringSecurity中,PasswordEncoder专门用来处理密码的加密与比对工作,我们自定义MyPasswordEncoder并实现PasswordEncoder接口,还需要实现该接口中的两个方法:
- encode方法表示对密码进行加密,参数rawPassword就是你传入的明文密码,返回的则是加密之后的密文,这里的加密方案采用了MD5。
- matches方法表示对密码进行比对,参数rawPassword相当于是用户登录时传入的密码,encodedPassword则相当于是加密后的密码(从数据库中查询而来)。
最后记得将MyPasswordEncoder通过@Component注解标记为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,使得密码加密加盐变得非常容易。只需要提供BCryptPasswordEncoder这个Bean的实例即可,微人事就是采用了这种方案(https://github.com/lenve/vhr),如下:
@Bean PasswordEncoderpasswordEncoder(){ returnnewBCryptPasswordEncoder(10); }
创建BCryptPasswordEncoder时传入的参数10就是strength,即密钥的迭代次数(也可以不配置,默认为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就可以实现登录。
本案例使用了配置在内存中的用户,一般情况下,用户信息是存储在数据库中的,因此需要在用户注册时对密码进行加密处理,如下:
@Service publicclassRegService{ publicintreg(Stringusername,Stringpassword){ BCryptPasswordEncoderencoder=newBCryptPasswordEncoder(10); StringencodePasswod=encoder.encode(password); returnsaveToDb(username,encodePasswod); } }
用户将密码从前端传来之后,通过调用BCryptPasswordEncoder实例中的encode方法对密码进行加密处理,加密完成后将密文存入数据库。
4.源码浅析
最后我们再来稍微看一下PasswordEncoder。
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", "Badcredentials")); } StringpresentedPassword=authentication.getCredentials().toString(); if(!passwordEncoder.matches(presentedPassword,userDetails.getPassword())){ logger.debug("Authenticationfailed:passworddoesnotmatchstoredvalue"); thrownewBadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Badcredentials")); } }
可以看到,密码比对就是通过passwordEncoder.matches方法来进行的。
关于DaoAuthenticationProvider的调用流程,大家可以参考SpringSecurity自定义认证逻辑的两种方式(高级玩法)一文。
好了,今天就和小伙伴们简单聊一聊SpringSecurity加密问题,小伙伴们要是有收获记得点个在看鼓励下松哥哦~
以上就是SpringBoot中密码加密的两种方法的详细内容,更多关于SpringBoot密码加密的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。