文章目录
概述
SpringSecurity原理(一)——初探
SpringSecurity原理(二)——认证
SpringSecurity原理(三)——授权
SpringSecurity原理(四)——过滤器
SpringSecurity原理(五)——扩展与配置
前面,我们已经简单的介绍了一下校验用户名和密码的认证过程。
这里,我们来了解一下Spring Security的权限校验过程。
首先,我们还是先通过一个简单的示例,来大致了解一下Spring Security的授权是个什么操作。
示例
首先,我们还是尽量保持简单,在之前的项目之上稍作修改,项目结构基本没有变化,只是修改一下我们的UserDetailsService,添加上用户的权限,然后修改我们的测试接口,给它添加上权限限制。
UserDetailsService
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String authority = "ROLE_admin,ROLE_teacher,read,write";
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
return User.builder()
.username("bob")
.password("$2a$10$344aKAgXr17q7u.8l5i7Cu8wUJr/cxBIniLsVtf/WwFrPx0khY62K")
.authorities(grantedAuthorities)
.build();
}
}
如果我们没有做特别的修改配置,Spring Security默认使用的GrantedAuthority是SimpleGrantedAuthority。
SimpleGrantedAuthority权限很简单,对得起它Simple的前缀,就只有一个字符串变量role表示角色。
权限类怎么设计是一回事,怎样校验权限又是另一回事,并且是更重要的一回事。
例如,看SimpleGrantedAuthority源码设计的目的肯定是用来表示一个用户有没有admin、editor、tech这类角色属性的。
但是和@PreAuthorize、@PostAuthorize、@Secured一起就被摆弄了十八般模样,变成:
可以校验:权限和角色
但是SimpleGrantedAuthority要守规矩:标识角色的SimpleGrantedAuthority的role必须有:ROLE_前缀,当然这也是可以反抗的,配置为其他前缀也行,但SimpleGrantedAuthority本身没啥发言权,自主权不在身上只能任人宰割。
Spring Security的User很给力,接收SimpleGrantedAuthority参数,也可以接收代表权限的字符串list,底层还是转为了SimpleGrantedAuthority列表。当然更给力的是User提供了一个roles接口,可以直接设置角色role,而不用自己去加ROLE_前缀了。
例如,向我们之前配置测试用户就用过的:
auth.inMemoryAuthentication()
.withUser("tim")
.password("111111")
.roles("admin")
.and()
.withUser("allen")
.password("222222")
.roles("user");
当然,也可以像上面的示例代码一样,使用写成一个字符串,用逗号分割,然后通过工具类方法AuthorityUtils.commaSeparatedStringToAuthorityList转换。
IndexController
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/hello")
public String hello(){
return "Hello World";
}
@PreAuthorize("hasRole('admin')")
@RequestMapping("/admin")
public String admin() {
return "admin";
}
@PreAuthorize("hasRole('admin') && hasAuthority('update')")
@RequestMapping("/admin-update")
public String adminAndUpdate() {
return "adminAndUpdate";
}
@PreAuthorize("hasAuthority('read')")
@RequestMapping("/read")
public String read() {
return "read";
}
@PreAuthorize("hasAuthority('update')")
@RequestMapping("/update")
public String update() {
return "update";
}
@Secured({"ROLE_user"})
@RequestMapping("/user")
public String user() {
return "user";
}
}
如果不去深究@PreAuthorize、@PostAuthorize、@Secured原理,只是使用非常简单,只需要添加上下面的注解,然后就可以放心使用了。
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
- @PreAuthorize是接口执行之前调用
- @PostAuthorize是接口执行之后调用
- @Secured校验角色的时候必须加上ROLE_前缀
其他添加权限控制方式
除了@PreAuthorize、@PostAuthorize、@Secured控制权限外,我们还可以通过HttpSecurity来配置权限。
在SecurityConfig(继承了WebSecurityConfigurerAdapter)中添加:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/home", "/js/*","/img/*").permitAll()
.antMatchers("/admin/**","/upload/**").hasAnyRole("admin")
.antMatchers("/order/**").hasAnyRole("user","admin")
.antMatchers("/tech/**").hasAnyRole("tech","admin")
.anyRequest().authenticated();
http.formLogin();
}
HttpSecurity还可以配置很多东西,我们后面再一点一点介绍。
上面我们简单的了解了一下Spring Security为我们提供的权限控制与校验,一般也就够用了。
但是,如果我们要:
- 设计自己的权限体系
- 修改权限校验的规则
- 自定义权限和自定义校验
那又该如何?
下面,我们就来简单了解一下Spring Security的权限与鉴权的流程。
GrantedAuthority
首先还是GrantedAuthority,它是用户权限的抽象,例如拥有什么角色role,有什么标识principal之类。
Spring Security为我们提供了3个GrantedAuthority实现类:
- SimpleGrantedAuthority:最重要的权限是角色
- SwitchUserGrantedAuthority:存储用户切换原用户的Authentication,像Windows用户切换用户操作
- JaasGrantedAuthority:除了角色,还多了Principal
JaasGrantedAuthority中的JASS(Java Authentication and Authorization Service),Java认证与授权服务,这个类更多是设计为JaasAuthenticationProvider与DefaultJaasAuthenticationProvider服务的,但是需要自己实现AuthorityGranter。
怎么设计GrantedAuthority很灵活,根据实际情况设计即可,问题的关键在于怎样校验。
在Spring Security授权体系中有一个非常重要的类FilterSecurityInterceptor,它虽然是以Interceptor结尾,但是它是一个拦截器。它控制了授权流程,为了过于算乱,跑遍了,我们在后面文章的过滤器中再介绍。
这里,我们先介绍一下和权限具体判定有关的投票者(AccessDecisionVoter)和投票管理器(AccessDecisionManager)。
AccessDecisionVoter
投票者有3个选项,已经固化在了它们的身体中:
// 同意
int ACCESS_GRANTED = 1;
// 弃权
int ACCESS_ABSTAIN = 0;
// 拒绝
int ACCESS_DENIED = -1;
Spring Security已经为我们提供了很多默认的投票者:
RoleVoter的投票方式就是,如果权限authority为空就拒绝,如果权限中有一个角色role和检查权限equals就同意。
RoleHierarchy表示角色继承,例如角色admin可以访问角色tech的所有权限,就可以表示为:
@Bean
protected RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_tech");
return hierarchy;
}
WebExpressionVoter就是根据SpEL表达式的计算值是否为true来决定同意还是拒绝。
我们前面介绍的@PreAuthorize、@PostAuthorize、@Secured就是使用的WebExpressionVoter。
如果对Spring EL不熟悉,可以看一下:
AccessDecisionManager
AccessDecisionManager是投票管理器,一次投票一般不会只有一个投票者,投票管理器的作用就是持有投票者的list,然后根据投票者的投票,最终统计出一个结果。
这里就涉及不同投票统计的策略了,所以Spring Security为我们提供了3个具体实现类:
- AffirmativeBased,霸权模式,表示一票通过
- ConsensusBased,民主模式,表示少数服从多数
- UnanimousBased,投资人模式,表示一票否决
在使用@PreAuthorize、@PostAuthorize、@Secured的时候,默认就是AffirmativeBased,只要有一票就通过。
TIP:和AccessDecisionManager比较像的是AfterInvocationManager,它管理一些AfterInvocationProvider,用于决定调用返回值。
权限校验SpEL
最后,在说一点,我们使用的的SpEL的校验:
@PreAuthorize(“hasRole(‘admin’) && hasAuthority(‘update’)”)
实际使用了MethodSecurityExpressionRoot,它继承了SecurityExpressionRoot,最顶层接口是SecurityExpressionOperations,整个继承体系是:
正真执行是在SecurityExpressionRoot的hasAnyAuthorityName方法:
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;
}