Day615.SpringSecurity常见错误 -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day615.SpringSecurity常见错误 -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
SpringSecurity常见错误
针对SpringSecurity
,Java程序员都会知道。他是一种Filter链+职责链设计模式
的安全框架。
那在使用它的时候也会出现一些常见的错误,如下就列举一些开发中可能会出现的常见错误。
一、PasswordEncoder匹配不到错误
第一次尝试使用 Spring Security 时,我们经常会忘记定义一个 PasswordEncoder。
因为这在 Spring Security 旧版本中是允许的。而一旦使用了新版本,则必须要提供一个 PasswordEncoder。
这里我们可以先写一个反例来感受下:首先我们在 Spring Boot 项目中直接开启 Spring Security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
添加完这段依赖后,Spring Security 就已经生效了。然后我们配置下安全策略,如下:
@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();
我们故意注掉了PasswordEncoder
注入容器,然后启动SpringBoot,发现项目启动成功。
但是当我们请求对应的http://localhost:8080/admin 地址时,就报了如下错误:
那这是为什么呢???
那为什么需要一个 PasswordEncoder。我们需要一个 PasswordEncoder 来满足加密
的功能。
它的两个关键方法 encode()
和 matches()
,相信你就能理解它们的作用了。
思考下,假设我们默认提供一个出来并集成到 Spring Security 里面去,那么很可能隐藏错误,所以还是强制要求起来比较合适。
我们再从源码上看下 “no PasswordEncoder
” 异常是如何被抛出的?当我们不指定 PasswordEncoder 去启动我们的案例程序时,我们实际指定了一个默认的 PasswordEncoder,这点我们可以从构造器 DaoAuthenticationProvider 看出来:
public DaoAuthenticationProvider()
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
我们可以看下 PasswordEncoderFactories.createDelegatingPasswordEncoder() 的实现:
public static PasswordEncoder createDelegatingPasswordEncoder()
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
我们可以换一个视角来看下这个 DelegatingPasswordEncoder 长什么样:
其实它是多个内置的 PasswordEncoder 集成
在了一起。
当我们校验用户时,我们会通过下面的代码来匹配,参考 DelegatingPasswordEncoder#matches:
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 delegate = this.idToPasswordEncoder.get(id);
if (delegate == null)
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
private String extractId(String prefixEncodedPassword)
if (prefixEncodedPassword == null)
return null;
//
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0)
return null;
//
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0)
return null;
return prefixEncodedPassword.substring(start + 1, end);
假设我们的 prefixEncodedPassword 中含有 id,则根据 id 到 DelegatingPasswordEncoder 的 idToPasswordEncoder 找出合适的 Encoder;假设没有 id,则使用默认的 UnmappedIdPasswordEncoder。我们来看下它的实现:
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 + "\\"");
从上述代码可以看出,no PasswordEncoder for the id “null” 异常就是这样被 UnmappedIdPasswordEncoder 抛出的。
那么这个可能含有 id 的 prefixEncodedPassword 是什么?其实它就是存储的密码,在我们的案例中由下面代码行中的 password() 指定:
auth.inMemoryAuthentication().withUser("admin").password("pass").roles("ADMIN");
这里我们不妨测试下,修改下上述代码行,给密码指定一个加密方式,看看之前的异常还存在与否:
auth.inMemoryAuthentication().withUser("admin").password("MD5pass").roles("ADMIN");
此时,以调试方式运行程序,你会发现,这个时候已经有了 id,且取出了合适的 PasswordEncoder。
那知道了如上,对的解决方案如下:
- 给Ioc容器中注入一个
PasswordEncoder
- 在密码中
加个前缀
二、ROLE_ 前缀是否添加错误
我们再来看一个 Spring Security 中关于权限角色的案例,ROLE_ 前缀
加还是不加?不过这里我们需要提供稍微复杂一些的功能,即模拟授权时的角色相关控制。
所以我们需要完善下案例,这里我先提供一个接口,这个接口需要管理的操作权限:
@RestController
public class HelloWorldController
@RequestMapping(path = "admin", method = RequestMethod.GET)
public String admin()
return "admin operation";
;
然后我们使用 Spring Security 默认的内置授权来创建一个授权配置类:
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter
@Bean
public PasswordEncoder passwordEncoder()
//同案例1,这里省略掉
@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();
通过上述代码,我们添加了 3 个用户:
- 用户 fujian:角色为 USER
- 用户 admin1:角色为 ADMIN
- 用户 admin2:角色为 ADMIN
然后我们从浏览器访问我们的接口 ·http://localhost:8080/admin·,使用上述 3 个用户登录,你会发现用户 admin1 可以登录,而 admin2 设置了同样的角色却不可以登陆,并且提示下面的错误:
那为什么呢?
主要的以两种添加方式,如下:
//admin1 的添加
.withUser("admin").password("pass").roles("ADMIN")
//admin2 的添加
.withUser(new UserDetails()
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));
@Override
public String getUsername()
return "admin2";
//省略其他非关键代码
);
查看上面这两种添加方式,你会发现它们真的仅仅是两种风格而已,所以最终构建出用户的代码肯定是相同的。
我们先来查看下 admin1 的添加最后对 Role 的处理(参考 User.UserBuilder#roles):
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;
可以看出,当 admin1 添加 ADMIN 角色时,实际添加进去的是 ROLE_ADMIN
。
但是我们再来看下 admin2 的角色设置,最终设置的方法其实就是 User#withUserDetails:
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;
所以,admin2 的添加,最终设置进的 Role 就是 ADMIN。
此时我们可以得出一个结论:通过上述两种方式设置的相同 Role(即 ADMIN),最后存储的 Role 却不相同,分别为 ROLE_ADMIN 和 ADMIN。
那么为什么只有 ROLE_ADMIN 这种用户才能通过授权呢?这里我们不妨通过调试视图看下授权的调用栈,截图如下:
对于案例的代码,最终是通过 “UsernamePasswordAuthenticationFilter” 来完成授权的。
而且从调用栈信息可以大致看出,授权的关键其实就是查找用户,然后校验权限。查找用户的方法可参考 InMemoryUserDetailsManager#loadUserByUsername,即根据用户名查找已添加的用户:
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException
UserDetails user = users.get(username.toLowerCase());
if (user == null)
throw new UsernameNotFoundException(username);
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
完成账号是否过期、是否锁定等检查后,我们会把这个用户转化为下面的 Token(即 UsernamePasswordAuthenticationToken)供后续使用,关键信息如下:
最终在判断角色时,我们会通过 UsernamePasswordAuthenticationToken 的父类方法 AbstractAuthenticationToken#getAuthorities 来取到上述截图中的 ADMIN。
而判断是否具备某个角色时,使用的关键方法是 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;
在上述代码中,prefix 是 ROLE_(默认值,即 SecurityExpressionRoot#defaultRolePrefix),Roles 是待匹配的角色 ROLE_ADMIN,产生的 defaultedRole 是 ROLE_ADMIN,而我们的 role-set 是从 UsernamePasswordAuthenticationToken 中获取到 ADMIN,所以最终判断的结果是 false。
最终这个结果反映给上层来决定是否通过授权,可参考 WebExpressionVoter#vote:
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes)
//省略非关键代码
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
以上是关于Day615.SpringSecurity常见错误 -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章
Day610.SpringWebHeader解析常见错误 -Spring编程常见错误
Day616.SpringException常见错误 -Spring常见编程错误
Day609.SpringWebURL解析常见错误 -Spring编程常见错误
Day621.Spring Test 常见错误 -Spring编程常见错误