先说一句:密码是没法解密的。你们也不要再问松哥微人事项目中的密码怎么解密了!前端
密码没法解密,仍是为了确保系统安全。今天松哥就来和你们聊一聊,密码要如何处理,才能在最大程度上确保咱们的系统安全。vue
本文是 Spring Security 系列的第 20 篇,阅读本系列前面的文章有助于更好的理解本文:java
2011 年 12 月 21 日,有人在网络上公开了一个包含 600 万个 CSDN 用户资料的数据库,数据所有为明文储存,包含用户名、密码以及注册邮箱。事件发生后 CSDN 在微博、官方网站等渠道发出了声明,解释说此数据库系 2009 年备份所用,因不明缘由泄露,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到此次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,因为不少用户是多个网站共用一个密码,所以一个网站密码泄露就会形成很大的安全隐患。因为有了这么多前车可鉴,咱们如今作系统时,密码都要加密处理。git
此次泄密,也留下了一些有趣的事情,特别是对于广大程序员设置密码这一项。人们从 CSDN 泄密的文件中,发现了一些好玩的密码,例如以下这些:程序员
ppnn13%dkstFeb.1st
这段密码的中文解析是:娉娉袅袅十三余,豆蔻梢头二月初。csbt34.ydhl12s
这段密码的中文解析是:池上碧苔三四点,叶底黄鹂一两声等等不一而足,你会发现不少程序员的人文素养仍是很是高的,让人啧啧称奇。github
密码加密咱们通常会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中建立数字“指纹”的方法。算法
散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,而后将数据打乱混合,从新建立一个散列值。散列值一般用一个短的随机字母和数字组成的字符串来表明。好的散列函数在输入域中不多出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。数据库
咱们经常使用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。后端
可是仅仅使用散列函数还不够,单纯的只使用散列函数,若是两个用户密码明文相同,生成的密文也会相同,这样就增长的密码泄漏的风险。安全
为了增长密码的安全性,通常在密码加密过程当中还须要加盐,所谓的盐能够是一个随机数也能够是用户名,加盐以后,即便密码明文相同的用户生成的密码密文也不相同,这能够极大的提升密码的安全性。
传统的加盐方式须要在数据库中有专门的字段来记录盐值,这个字段多是用户名字段(由于用户名惟一),也多是一个专门记录盐值的字段,这样的配置比较繁琐。
Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时能够选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。
不一样于 Shiro 中须要本身处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来很是方便。
commons-codec 是一个 Apache 上的开源项目,用它能够方便的实现密码加密。松哥在 V 部落
项目中就是采用的这种方案(https://github.com/lenve/VBlog)。在 Spring Security 还未推出 BCryptPasswordEncoder 的时候,commons-codec 仍是一个比较常见的解决方案。
因此,这里我先来给你们介绍下 commons-codec 的用法。
首先咱们须要引入 commons-codec 的依赖:
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency>
而后自定义一个 PasswordEncoder:
@Component public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes())); } }
在 Spring Security 中,PasswordEncoder 专门用来处理密码的加密与比对工做,咱们自定义 MyPasswordEncoder 并实现 PasswordEncoder 接口,还须要实现该接口中的两个方法:
最后记得将 MyPasswordEncoder 经过 @Component 注解标记为 Spring 容器中的一个组件。
这样用户在登陆时,就会自动调用 matches 方法进行密码比对。
固然,使用了 MyPasswordEncoder 以后,在用户注册时,就须要将密码加密以后存入数据库中,方式以下:
public int reg(User user) { ... //插入用户,插入以前先对密码进行加密 user.setPassword(passwordEncoder.encode(user.getPassword())); result = userMapper.reg(user); ... }
其实很简单,就是调用 encode 方法对密码进行加密。完整代码你们能够参考 V 部落(https://github.com/lenve/VBlog),我这里就不赘述了。
可是本身定义 PasswordEncoder 仍是有些麻烦,特别是处理密码加盐问题的时候。
因此在 Spring Security 中提供了 BCryptPasswordEncoder,使得密码加密加盐变得很是容易。只须要提供 BCryptPasswordEncoder 这个 Bean 的实例便可,微人事就是采用了这种方案(https://github.com/lenve/vhr),以下:
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(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 public class RegService { public int reg(String username, String password) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); String encodePasswod = encoder.encode(password); return saveToDb(username, encodePasswod); } }
用户将密码从前端传来以后,经过调用 BCryptPasswordEncoder 实例中的 encode 方法对密码进行加密处理,加密完成后将密文存入数据库。
最后咱们再来稍微看一下 PasswordEncoder。
PasswordEncoder 是一个接口,里边只有三个方法:
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
Spring Security 为 PasswordEncoder 提供了不少实现:
可是老实说,自从有了 BCryptPasswordEncoder,咱们不多关注其余实现类了。
PasswordEncoder 中的 encode 方法,是咱们在用户注册的时候手动调用。
matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
能够看到,密码比对就是经过 passwordEncoder.matches 方法来进行的。
关于 DaoAuthenticationProvider 的调用流程,你们能够参考 SpringSecurity 自定义认证逻辑的两种方式(高级玩法)一文。
好了,今天就和小伙伴们简单聊一聊 Spring Security 加密问题,小伙伴们要是有收获记得点个在看鼓励下松哥哦~