密码加密
常见的几种密码加密的方式
- BCryptPasswordEncoder
BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptP asswordEncoder “为自己带盐”开发者不需要额外维护一个“盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。
- Argon2PasswordEncoder
Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
- Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2
算法也是一种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
- SCryptPasswordEncoder
SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,serypt 也是一种故意降低运算速度的算法,而且需要大量内存。
PasswordEncoder
源码:
package org.springframework.security.crypto.password; /** * 用于编码密码的服务接口。首选实现是 BCryptPasswordEncoder。 * 作者: * 基思·唐纳德 */ public interface PasswordEncoder { //对原始密码进行编码。通常,良好的编码算法会应用 SHA-1 或更大的哈希值以及 8 字节或更大的随机生成盐 String encode(CharSequence rawPassword); /* 验证从存储中获取的编码密码是否与提交的原始密码匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。存储的密码本身永远不会被解码。 形参: 原始密码 – 要编码和匹配的原始密码 编码密码 – 存储中要与之比较的编码密码 返回值: 如果编码后的原始密码与存储中的编码密码匹配,则为 true */ boolean matches(CharSequence rawPassword, String encodedPassword); /* 如果应再次对编码的密码以提高安全性,则返回 true,否则返回 false。默认实现始终返回 false。 形参: 编码密码 – 要检查的编码密码 返回值: 如果应再次对编码的密码进行编码以提高安全性,则为 true,否则为 false */ default boolean upgradeEncoding(String encodedPassword) { return false; } }
从中我们得出了他的三个实现方法
- encode 用来进行明文加密的
- matches 用来比较密码的方法
- upgradeEncoding 用来给密码进行升级的方法
DelegatingPasswordEncoder
根据上面 PasswordEncoder的介绍,可能会以为 Spring security 中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,
在 spring Security 5.0之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。
它继承了上面的PasswordEcoder,并且实现了更为强大的功能
同时它具有更好的兼容性 和 便捷性 以及 安全性
DelegatingPasswordEncoder源码文档
于前缀标识符委派给另一个密码编码器的密码编码器。 //构造实例 您可以使用 轻松 org.springframework.security.crypto.factory.PasswordEncoderFactories构造实例。或者,您可以创建自己的自定义实例。例如: String idForEncode = "bcrypt"; Map encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders); //密码存储格式 密码的一般格式为: {id}encodedPassword 这样,“id”是用于查找 PasswordEncoder 应使用的标识符,而“encodedPassword”是所选 PasswordEncoder的原始编码密码。“id”必须位于密码的开头,以“{”开头,以“}”结尾。如果找不到“id”,则“id”将为空。例如,以下内容可能是使用不同“id”编码的密码列表。所有原始密码都是“密码”。 {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 对于我们上面构建的委派密码编码器: 第一个密码的 PasswordEncoder ID为“bcrypt”,编码密码为“$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG”。匹配时,它将委托给 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 第二个密码的ID为“noop”,编码密码为 PasswordEncoder “password”。匹配时,它将委托给 NoOpPasswordEncoder 第三个密码的 PasswordEncoder ID为“pbkdf2”,编码密码为“5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc”。匹配时,它将委托给 Pbkdf2PasswordEncoder 第四个密码的 PasswordEncoder ID为“scrypt”,编码密码为“$e 0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec 05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=” 匹配时,它将委托给 org.springframework.security.crypto.scrypt.SCryptPasswordEncoder 最终密码的 ID 为 PasswordEncoder “sha256”,编码密码为“97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0”。匹配时,它将委托给 StandardPasswordEncoder //密码编码 传递给构造函数的内容 idForEncode 确定将 PasswordEncoder 用于编码密码。在我们上面构造的中 DelegatingPasswordEncoder ,这意味着编码“密码”的结果将被委托给 BCryptPasswordEncoder 并以“{bcrypt}”为前缀。最终结果如下所示: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG //密码匹配 匹配是基于“id”和“id” PasswordEncoder 到构造函数中提供的映射完成的。我们在“密码存储格式”中的示例提供了如何完成此操作的工作示例。默认情况下,使用未映射的“id”(包括空 id)的密码调用的结果 matches(CharSequence, String) 将导致 IllegalArgumentException.可以使用 自定义 setDefaultPasswordEncoderForMatches(PasswordEncoder)此行为。 请参阅: org.springframework.security.crypto.factory.PasswordEncoderFactories
DelegatingPasswordEncoder源码解读
public class DelegatingPasswordEncoder implements PasswordEncoder { private static final String PREFIX = "{"; private static final String SUFFIX = "}"; private final String idForEncode; private final PasswordEncoder passwordEncoderForEncode; private final Map<String, PasswordEncoder> idToPasswordEncoder; private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder(); public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) { if (idForEncode == null) { throw new IllegalArgumentException("idForEncode cannot be null"); } if (!idToPasswordEncoder.containsKey(idForEncode)) { throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder); } for (String id : idToPasswordEncoder.keySet()) { if (id == null) { continue; } if (id.contains(PREFIX)) { throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX); } if (id.contains(SUFFIX)) { throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX); } } this.idForEncode = idForEncode; this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); }
实现
自定义配置文件实现PasswordEncoder接口
三个方法前面有介绍
@Override public String encode(CharSequence rawPassword) { return null; } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return false; } @Override public boolean upgradeEncoding(String encodedPassword) { return PasswordEncoder.super.upgradeEncoding(encodedPassword); }
密码自动升级
推荐使用DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用。
- 准备库表
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `enabled` tinyint(1) DEFAULT NULL, `accountNonExpired` tinyint(1) DEFAULT NULL, `accountNonLocked` tinyint(1) DEFAULT NULL, `credentialsNonExpired` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- 角色表 CREATE TABLE `role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `name_zh` varchar(32) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- 用户角色关系表 CREATE TABLE `user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `uid` (`uid`), KEY `rid` (`rid`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
- 插入数据
-- 插入用户数据 BEGIN; INSERT INTO `user` VALUES (1, 'root', '{noop}123', 1, 1, 1, 1); INSERT INTO `user` VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1); INSERT INTO `user` VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1); COMMIT; -- 插入角色数据 BEGIN; INSERT INTO `role` VALUES (1, 'ROLE_product', '商品管理员'); INSERT INTO `role` VALUES (2, 'ROLE_admin', '系统管理员'); INSERT INTO `role` VALUES (3, 'ROLE_user', '用户管理员'); COMMIT; -- 插入用户角色数据 BEGIN; INSERT INTO `user_role` VALUES (1, 1, 1); INSERT INTO `user_role` VALUES (2, 1, 2); INSERT INTO `user_role` VALUES (3, 2, 2); INSERT INTO `user_role` VALUES (4, 3, 3); COMMIT;
- 整合 mybatis
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency>
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false spring.datasource.username=root spring.datasource.password=root mybatis.mapper-locations=classpath:/mapper/*.xml mybatis.type-aliases-package=com.baizhi.entity logging.level.com.baizhi.dao=debug
- 编写实体类
public class User implements UserDetails { private Integer id; private String username; private String password; private Boolean enabled; private Boolean accountNonExpired; private Boolean accountNonLocked; private Boolean credentialsNonExpired; private List roles = new ArrayList<>(); @Override public Collection getAuthorities() { List authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public boolean isAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } public void setRoles(List roles) { this.roles = roles; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } }
public class Role { private Integer id; private String name; private String nameZh; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; } }
- 创建dao
@Mapper public interface UserDao { List getRolesByUid(Integer uid); User loadUserByUsername(String username); Integer updatePassword(@Param("username") String username,@Param("password") String password); }
- 编写 mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> select id, username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired from `user` where username = #{username} select r.id, r.name, r.name_zh nameZh from `role` r, `user_role` ur where r.id = ur.rid and ur.uid = #{uid} update `user` set password=#{password} where username=#{username}
- 编写service 实现
@Service public class MyUserDetailService implements UserDetailsService,UserDetailsPasswordService { private final UserDao userDao; @Autowired public MyUserDetailService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) { throw new RuntimeException("用户不存在!"); } user.setRoles(userDao.getRolesByUid(user.getId())); return user; } @Override public UserDetails updatePassword(UserDetails user, String newPassword) { Integer result = userDao.updatePassword(user.getUsername(), newPassword); if (result == 1) { ((User) user).setPassword(newPassword); } return user; } }
- 配置securityconfig
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { private final MyUserDetailService myUserDetailService; @Autowired public SecurityConfig(MyUserDetailService myUserDetailService) { this.myUserDetailService = myUserDetailService; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //查询数据库 auth.userDetailsService(myUserDetailService); } }
- 启动项目测试
说明
部分的代码是由@编程不良人的学习教程中提供的,仅作为自己的学习参考使用!
作者教程链接 : Spring Security 最新实战教程