SpringSecurity中通过PasswordEncoder接口定义了密码加密和比对的相关操作:
public interface PasswordEncoder {
// 对明文密码进行加密
String encode(CharSequence var1);
// 进行密码比对
boolean matches(CharSequence var1, String var2);
// 判断当前密码是否需要升级,默认返回false,表示不需要升级
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
针对密码的所有操作,PasswordEncoder都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。
使用自适应单向函数处理密码问题的常见实现类:
BCryptPasswordEncoder
Argon2PasswordEncoder
SCryptPasswordEncoder
Pbkdf2PasswordEncoder
SpringSecurity中默认的密码加密方式为DelegatingPasswordEncoder,主要用于代理上面介绍的不同的密码加密方案。
先来看下PasswordEncoderFactories类,因为正是由它里面的静态方法createDelegatingPasswordEncoder()提供了默认的DelegatingPasswordEncoder实例:
public class PasswordEncoderFactories {
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
private PasswordEncoderFactories() {
}
}
createDelegatingPasswordEncoder() 方法中,首先定义了encoders变量,encoders中存储了每一种密码加密方案的id和所对应的加密类,例如MD4对应Md4PasswordEncoder,noop对应NoOpPasswordEncoder,其中encodingId的默认值为bcrypt,相当于默认使用的加密方案是BCryptPasswordEncoder。
再来看下DelegatingPasswordEncoder类的源码,由于源码较长,先来看下属性和构造方法:
public class DelegatingPasswordEncoder implements PasswordEncoder {
}
// 定义前缀PREFIX和后缀SUFFIX用来包裹将来生成的加密方案的id
private static final String PREFIX = "{";
private static final String SUFFIX = "}";
// 默认的加密方案id
private final String idForEncode;
// 表示默认的加密方案(BCryptPasswordEncoder)
// 它的值是根据idForEncode从idToPasswordEncoder集合中提取出来的
private final PasswordEncoder passwordEncoderForEncode;
// 用来保存id和家吗方案之间的映射
private final Map<String, PasswordEncoder> idToPasswordEncoder;
// 默认的密码比对器,当根据密码加密方案的id无法找到对应的加密方案时,就会使用默认的密码比对器
// defaultPasswordEncoderForMatches的默认类型是UnmappedIdPasswordEncoder
// 在UnmappedIdPasswordEncoder的matches方法中不会做任何密码比对操作,直接抛出异常
private PasswordEncoder defaultPasswordEncoderForMatches = new DelegatingPasswordEncoder.UnmappedIdPasswordEncoder();
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
if (idForEncode == null) {
throw new IllegalArgumentException("idForEncode cannot be null");
} else if (!idToPasswordEncoder.containsKey(idForEncode)) {
throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
} else {
Iterator var3 = idToPasswordEncoder.keySet().iterator();
while(var3.hasNext()) {
String id = (String)var3.next();
if (id != null) {
if (id.contains("{")) {
throw new IllegalArgumentException("id " + id + " cannot contain " + "{");
}
if (id.contains("}")) {
throw new IllegalArgumentException("id " + id + " cannot contain " + "}");
}
}
}
this.idForEncode = idForEncode;
// 根据idForEncode获取passwordEncoderForEncode
this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
}
}
public void setDefaultPasswordEncoderForMatches(PasswordEncoder defaultPasswordEncoderForMatches) {
if (defaultPasswordEncoderForMatches == null) {
throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
} else {
this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
}
}
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
private UnmappedIdPasswordEncoder() {
}
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
String id = DelegatingPasswordEncoder.this.extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
encode()方法实现:
// 具体的加密工作仍然由加密类来完成
// 只不过在密码加密完成后,给加密后的字符串加上一个前缀{id},用来描述所采用的具体加密方案。
public String encode(CharSequence rawPassword) {
return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
}
不同的前缀代表了后面的字符串采用了不同的加密方案。
matches()方法实现:
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
} else {
// 从加密字符串中提取出具体的加密方案id,也就是{}中的字符
String id = this.extractId(prefixEncodedPassword);
// 到集合中获取具体的加密方案
PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
// 如果获取到的为null,说明不存在对应的加密实例
// 那么就采用默认的密码匹配器defaultPasswordEncoderForMatches
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
} else {
// 如果获取到了对应的加密实例,则调用matches()方法完成密码校验
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
}
}
// 从加密字符串中提取出具体的加密方案id,也就是{}中的字符,具体的提取方式就是字符串截取
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
} else {
int start = prefixEncodedPassword.indexOf("{");
if (start != 0) {
return null;
} else {
int end = prefixEncodedPassword.indexOf("}", start);
return end < 0 ? null : prefixEncodedPassword.substring(start + 1, end);
}
}
}
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf("}");
return prefixEncodedPassword.substring(start + 1);
}
upgradeEncoding() 方法实现:
public boolean upgradeEncoding(String prefixEncodedPassword) {
String id = this.extractId(prefixEncodedPassword);
// 如果当前加密字符串所采用的加密方案不是默认的加密方案(BCryptPasswordEncoder)
// 就会自动进行密码升级
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
} else {
// 调用默认加密方案的upgradeEncoding()方法怕暖密码是否需要升级
String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
return ((PasswordEncoder)this.idToPasswordEncoder.get(id)).upgradeEncoding(encodedPassword);
}
}
① 创建一个SpringBoot工程,并引入SpringSecurity依赖,创建一个测试接口:
@RestController
public class UserResource {
@RequestMapping("/hello")
public String test(){
return "hello";
}
}
② 在单元测试中生成一段加密字符串:
@SpringBootTest
public class UuaApplicationTest {
@Test
public void test(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// $2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS
System.out.println(encoder.encode("123"));
}
}
③ 自定义SecurityConfig类:
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 表单登录
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
// 自定义用户认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("zhangsan")
.password("$2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS")
.roles("admin");
}
// 将BCryptPasswordEncoder示例注册到Spring容器中,这将代替默认的DelegatingPasswordEncoder
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
配置完成后启动项目就可以使用zhangsan/123进行登录了。
由于默认使用的是DelegatingPasswordEncoder,所以页可以不配置PasswordEncode实例,只需要在密码前加上前缀{bcrypt}:
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("zhangsan")
.password("{bcrypt}$2a$10$.758pfnk63n7fUNPLovJS.ydgs0gpSqH0nFrlxVTj/vLozzIk01NS")
.roles("admin");
}
}
使用DelegatingPasswordEncoder的另一个好处是会自动进行密码加密升级。
① 创建一个数据库test,向数据库中添加一个user表,并添加一条数据,在用户数据中,用户密码是{noop}123:
create table user(
id int(11) not null auto_increment,
username varchar(32) default null,
password varchar(255) default null,
primary key(id)
)engine=innodb default charset=utf8
INSERT INTO `test`.`user` (`id`, `username`, `password`) VALUES (1, 'zhangsan', '{noop}123');
② 在项目中引入MyBatis和mysql依赖:
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
③ 在 application.properties 中添加数据库的连接信息:
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
④ 创建 User 实体类:
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
⑤ 创建MyUserDetailsService类:
@Service
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
// 实现了UserDetailsPasswordService接口中的updatePassword()方法
// 当系统判断密码加密方案需要升级的时候,就会自动调用updatePassword中的方法去修改数据库中的密码
// 当数据库中的密码修改成功后,修改User对象的password属性,并将User对象返回
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
Integer result = userMapper.updatePassword(user.getUsername(),newPassword);
if(result==1){
((User) user).setPassword(newPassword);
}
return user;
}
// 实现了UserDetailsService接口中的loadUserByUsername()方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if(Objects.isNull(user)){
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
Integer updatePassword(@Param("username") String username,
@Param("newPassword") String newPassword);
}
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.imooc.uua.dao.UserMapper">
<update id="updatePassword">
update user set password=#{newPassword} where username=#{username}
update>
<select id="loadUserByUsername" resultType="com.imooc.uua.entity.User">
select * from user where username=#{username};
select>
mapper>
⑥ 配置SecurityConfig:
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
启动项目登录成功后密码就变成了{bcrypt}$2a$10$VJeTeGpn8PhoQ6S3i4nmcuT3ho6QJpmVbtR/PFa4ffag.Dx0RFLVe
如果使用了DelegatingPasswordEncoder,只要数据库中存储的加密方案不是DelegatingPasswordEncoder中默认的BCryptPasswordEncoder,在登录成功后,都会自动升级为BCryptPasswordEncoder加密。这就是加密方案的升级。