文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间
案例33:使用 Spring Security 没有自定义密码解析器
在使用 Spring Security 时,忘记定义一个 PasswordEncoder,虽然程序能够启动成功,但在发送 http://localhost:8080/admin 请求,就会报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
//
// @Bean
// public PasswordEncoder passwordEncoder() {
// return new PasswordEncoder() {
// @Override
// public String encode(CharSequence charSequence) {
// return charSequence.toString();
// }
//
// @Override
// public boolean matches(CharSequence charSequence, String // s) {
// return Objects.equals(charSequence.toString(), s);
// }
// };
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("pass").roles("ADMIN");
}
// 配置 URL 对应的访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login").permitAll()
.and().csrf().disable();
}
}
解析:系统会默认创建 DelegatingPasswordEncoder() 里面虽然包含了多个 PasswordEncoder ,而在 DelegatingPasswordEncoder 执行 matches() 方法中会根据 config 里所设置的密码的前缀提取对应的 PasswordEncoder ,但由于所设置的密码没有前缀,因此采用默认的 UnmappedIdPasswordEncoder 进行密码的匹配,而其匹配必会抛出异常
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
// 提取所设置密码的 前缀
String id = extractId(prefixEncodedPassword);
// 根据前缀找到对应 PasswordEncoder
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
// 没有前缀 则使用默认的 PasswordEncoder
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
---------------------------------------------------------
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword,
String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
解决:1. 在 config 中设置密码时给定前缀,使用指定的 PasswordEncoder ,
auth.inMemoryAuthentication().withUser("admin").password("{noop}pass").roles("ADMIN");
2. 自定义一个 PasswordEncoder Bean
案例34:ROLE_ 前缀与角色
定义多个角色权限,但用户 admin1 可以登录,而 admin2 设置了同样的角色却不可以登陆
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
//同上案例,这里省略掉
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("fujian").password("pass").roles("USER")
.and()
.withUser("admin1").password("pass").roles("ADMIN")
.and()
.withUser(new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));
}
//省略其他非关键“实现”方法
public String getUsername() {
return "admin2";
}
});
}
// 配置 URL 对应的访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login").permitAll()
.and().csrf().disable();
}
}
解析:admin1 的添加最后对 Role 的处理,有前缀 ROLE_
public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(
roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"), () -> role
+ " cannot start with ROLE_ (it is automatically added)");
//添加“ROLE_”前缀
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities(authorities);
}
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList<>(authorities);
return this;
}
admin2 的角色设置,最终设置的方法其实就是 User#withUserDetails,没有前缀 ROLE_
public static UserBuilder withUserDetails(UserDetails userDetails) {
return withUsername(userDetails.getUsername())
//省略非关键代码
.authorities(userDetails.getAuthorities())
.credentialsExpired(!userDetails.isCredentialsNonExpired())
.disabled(!userDetails.isEnabled());
}
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList<>(authorities);
return this;
}
在校验的时候是通过 UsernamePasswordAuthenticationFilter 进行的,在判断角色时,则通过 UsernamePasswordAuthenticationToken 的父类方法 AbstractAuthenticationToken#getAuthorities 来获取用户角色。而判断是否具备某个角色时,使用的关键方法是 SecurityExpressionRoot#hasAnyAuthorityName
private boolean hasAnyAuthorityName(String prefix, String... roles) {
//通过 AbstractAuthenticationToken#getAuthorities 获取“role”
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
//尝试添加“prefix”,即“ROLE_”
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
}
if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(defaultRolePrefix)) {
return role;
}
return defaultRolePrefix + role;
}
解决:在添加 admin2 时,给角色添加上 ROLE_ 前缀即可
//admin2 的添加
.withUser(new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
@Override
public String getUsername() {
return "admin2";
}
//省略其他非关键代码
})