本文讲一下spring security关于权限认证相关的内容
spring security 过滤器链
先来讲一下spring security的工作过程。它其实就是一系列的filter过滤器和拦截器。
我们最常用的一般是身份认证过滤器过滤器: usernamePassword Authentication Filter,以及今天要讲到的权限拦截器 FilterSecuity Interceptor。
以下是完整的过滤器链, 但是我们并不需要完全关心所有的。
Spring Security的核心逻辑都在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件我们就基本掌握了Spring Security,这个框架的使用方式就是对这些过滤器和组件进行扩展。
UsernamePasswordAuthenticationFilter
我们先来简单回顾一下用户认证,因为我们需要的权限信息,需要从 Authentication 中获取。
Authentication 是什么呢?这里简单介绍一下
里面最重要的有三项信息:
- Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
- Credentials:用户凭证,一般是密码
- Authorities:用户权限
而 Authentication 就代表 当前登录用户
首先 在 UsernamePasswordAuthenticationFilter
中,将用户名密码封装成UsernamePasswordAuthenticationToken。并调用authenticate方法认证
而 authenticate 方法由 AuthenticationManager 提供, 是Spring Security用于执行身份验证的组件,只需要调用它的authenticate 方法即可完成认证
authenticate方法的大概逻辑:
- this.getUserDetailsService().loadUserByUsername(username); 获取 UserDtails类
- 调用passwordEncoder.matches(password, userDetails.getPassword()判断用户名密码是否相同
- 返回的已认证Authentication,将整个UserDetails放进去充当Principal
所以我们的目的就很清楚了: 我们自己实现UserDetialsService、UserDetails、PasswordEncoder,这三个组件/类
UserDetialsService
自定义UserDetailService,重写 loadByUsername,获取用户信息。
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
user.getAuthorities();
return user;
}
}
UserDetials类
可以注意到的是 loadUserByUsername 返回类型是 UserDetails
,这是很重要的一个类。因为我们的权限就是通过该类的 getAuthorities()
方法获取的.
// UserDetails接口方法
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
}
所以我们的权限哪里来? 就是通过返回 UserDetails
类型的对象,spring security就可以调用 getAuthorities 来获取我们的权限。
如何返回这个 UserDetails
类型的对象?
第一种方案: 我们直接 new 一个 spring security 实现 UserDetails
接口的 类。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 设置用户角色
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 这里替换成你获取权限的方法
authorities.add(new SimpleGrantedAuthority("admin"));
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), authorities);
}
第二种方案: 我们自己的类,继承 UserDetails 类,并重写其中的方法
public class User implements UserDetails {
@Override
Collection<? extends GrantedAuthority> getAuthorities() {
// 返回权限
}
}
现在的一般都是基于RBAC(Role-Based Access Control)模型来进行权限控制,即:基于角色的权限控制。
所以上面的代码可以替换成此逻辑: 获取用户所有角色,获取对应角色对应的所有权限,返回所有权限。
至此,我们已经成功返回了一个 UserDetails
类型的对象,且其中有我们的权限信息。
PasswordEncoder
可用 自带的 BCryptPasswordEncoder
@Configuration
@EnableWebSecurity
public class MvcSecurityConfig extends WebSecurityConfigurerAdapter {
private final BCryptPasswordEncoder passwordEncoder;
public MvcSecurityConfig() {
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Bean
PasswordEncoder passwordEncoder() {
return this.passwordEncoder;
}
}
经过上述校验完后, 我们获得了一个 UsernamePasswordAuthenticationToken 类型的对象,其中有我们的用户名,密码,权限
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
authentication.getCredentials(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(result);
之后就会存到我们的 SecurityContextHolder 安全上下文中。
至此,我们已经走完了 UsernamePasswordAuthenticationFilter。当前登录用户已经存在 SecurityContextHolder 中, 注销或者session过期前我们都不需要重新认证。直接从上下文中获取就可以。
FilterSecuity Interceptor
启动权限认证
修改WebSecurityConfig类
配置类添加注解:
开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认
@EnableGlobalMethodSecurity(prePostEnabled = true)
至此我们就可以在controller层使用 @PreAuthorize 进行校验了。
@RestController
@RequestMapping("/V1.0/syllabus")
@PreAuthorize("hasAuthority('SCOPE_all')")
public class ApiSyllabusController {
}
表示访问该Controller下的所有方法,都需要当前登录用户有 SCOPE_all权限。
我们来简单看一下 hasAuthority 方法
调用了 hasAnyAuthorityName
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
从 getAuthoritySet()方法中获取所有权限, 然后判断 SCOPE_all 是否在 Set<String> 中, 如果是,则证明当前登录用户有 SCOPE_all 权限,允许访问。
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
获取权限方法:
private Set<String> getAuthoritySet() {
if (this.roles == null) {
Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
if (this.roleHierarchy != null) {
userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
}
this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return this.roles;
}
重点是这行, 看到了我们熟悉的东西 this.authentication.getAuthorities()
Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
所以原理很简单,我们之间将包含权限的 UserDetails 封装在 authentication中。直接调用 getAuthorities() 方法就能获取当前登录用户所有权限了。
@PreAuthorize("hasAuthority('SCOPE_all')")
至此 hasAuthority 返回了 true, 权限校验成功
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。