SpringSecurity系列学习(五):授权流程和源码分析
系列导航
SpringSecurity系列
- SpringSecurity系列学习(一):初识SpringSecurity
- SpringSecurity系列学习(二):密码验证
- SpringSecurity系列学习(三):认证流程和源码解析
- SpringSecurity系列学习(四):基于JWT的认证
- SpringSecurity系列学习(四-番外):多因子验证和TOTP
- SpringSecurity系列学习(五):授权流程和源码分析
- SpringSecurity系列学习(六):基于RBAC的授权
SpringSecurityOauth2系列
- SpringSecurityOauth2系列学习(一):初认Oauth2
- SpringSecurityOauth2系列学习(二):授权服务
- SpringSecurityOauth2系列学习(三):资源服务
- SpringSecurityOauth2系列学习(四):自定义登陆登出接口
- SpringSecurityOauth2系列学习(五):授权服务自定义异常处理
授权
看到标题了吧?这一节咱们不上号哈,先从原理入手!减少踩坑的概率!
上一节我们完成了认证的流程,接下来我们来谈一谈授权
在正式开始之前,先给大家提个醒,授权这个东西,相比起认证,其实更偏业务一点,在技术上不难,关键是业务设计。这里面是个大学问,每一个项目的权限设计都不同,怎么设计好用户角色权限的关系(我剧透了?)其实才是最难的点。
什么是授权
根据用户的权限来控制用户使用资源的过程就是授权
用微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包。
发红包功能、发朋友圈功能都是微信的资源即功能资源
,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。
为什么要授权?
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的, 控制不同的用户能够访问不同的资源。
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
SpringSecurity中的授权
AbstractAccessDecisionManager
根据相关信息,做出授权决定
这个类中有一个decide(Object)
的方法,接收Object
类型的参数,是一个安全对象。其安全对象具体是什么,SpringSecurity并没有去严格的限制它。其检查逻辑需要去自定义
基于投票的AccessDecisionManager实现
AccessDecisionManager
对一组AccessDecisionVoter
(投票器)实现进行轮询授权决定(和之前我们学习认证的时候,去轮询AuthenticationProvider
是一样的)。然后AccessDecisionManager
根据对投票的评估,决定是否抛出一个AccessDeniedException
异常。
三种投票策略
ConsensusBased
多数服从少数AffirmaticeBased
有一票就可以通过UnanimousBased
需要全票才能通过(默认采取的策略)
AccessDecisionVoter的一种实现:RoleVoter
如果有任何的ConfigAttribute
是以ROLE_
开头的,它就会进行投票
如果GrantedAuthority
(权限列表)中有一个或者多个以ROLE_
开头的角色能够匹配上ConfigAttribute
中设置的角色,这个投票器就投票授予访问权限
如果没有任何GrantedAuthority
返回的字符串与角色字符串相匹配,它就会投票拒绝访问
如果没有ConfigAttribute
以ROLE_
开头的角色,那么就放弃投票
ConfigAttribute
是指配置的访问资源需要的角色配置,比如:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(req -> req
//访问 /admin路径下的请求 要有管理员权限
.antMatchers("/admin/**").hasRole("ADMIN")
...
}
这里的hasRole("ADMIN")
就是一个ConfigAttribute
,并且通过hasRole("ADMIN")
设置的ConfigAttribute
,都会自动加上前缀ROLE_
hasRole("ADMIN") = hasAuthority("ROLE_ADMIN")
这个类图可以表示:AccessDecisionManager
对AccessDecisionVoter
进行轮询。
RoleVoter
就是一种AccessDecisionVoter
AuthenticatedVoter
是另一种AccessDecisionVoter
,当你认证成功,它就会投票授予访问权限
安全表达式
安全表达式是SpringSecurity中非常重要,并且非常受欢迎的功能,能够自定义安全策略并且将其独立于业务代码之外。
之前我们只是粗略地了解了一下安全表达式,现在咱们深入学习一下
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(req -> req
.antMatchers("/authorize/**").permitAll()
//访问 /admin路径下的请求 要有管理员权限
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/**").access("hasRole('ADMIN') or hasRole('USER')")
...
}
安全表达式的顺序很重要,作用越广泛的规则要放在最后,避免其他规则失效
类似permitAll
的函数:
denyAll
:拒绝用户访问anonymous
: 匿名用户rememberMe
: 记住我用户authenticated
: 已认证用户fullyAuthenticated
: 既不是匿名用户也不是记住我用户
类似hasRole
的函数:
hasAnyRole
: 有其中一个角色即可haeAuthority
:hasRole("ADMIN")
等价于haeAuthority("ROLE_ADMIN")
haeAnyAuthority
:hasAnyRole("ADMIN","USER")
等价于haeAnyAuthority("ROLE_ADMIN","ROLE_USER")
复杂表达式应用
access
:更复杂的表达式,支持SpEL
表达式,可以引用HttpServletRequest
中的属性,也可以引用@Bean
现在这里我们有一个接口:
@GetMapping("/users/{username}")
public String getCurrentUserName(){
...
}
我们希望的是,只有用户本人或者管理员才可以访问:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(req -> req
.antMatchers("/users/{username}")
//只有当前认证的用户和管理员才能访问
.access("hasRole('ADMIN') or authentication.name.equals(#username)")
.anyRequest.denyAll()
...
}
这里我们直接引用了认证对象authentication
和路径参数username
如何去使用一个bean
?
存在这样一个bean
@Service
public class UserService{
public boolean checkUsername(Authentication authentication,String username){
...
}
}
使用@
引用bean
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(req -> req
.antMatchers("/users/{username}")
//只有当前认证的用户和管理员才能访问
.access("@userService.checkUsername(authentication,#username)")
.anyRequest.denyAll()
...
}
方法级的安全表达式
安全表达式主要是作用于url的,当客户端请求url的时候,去进行一个授权。
但是在更复杂的场景里面,我们还需要对方法进行一些限制
ps:方法级的授权和url的授权是不一样的,url和方法级的授权是相互独立的,都需要进行投票器的投票,即如果一个接口即设定了url级别的授权和方法级别的授权,那么会进行两次授权投票,一次是url,一次是方法,并且如果有一个授权不通过,则拒绝访问
配置
/**
* `@EnableWebSecurity` 注解 deug参数为true时,开启调试模式,会有更多的debug输出
* 启用`@EnableGlobalMethodSecurity(prePostEnabled = true)`注解后即可使用方法级安全注解
* 方法级安全注解:
* pre : @PreAuthorize(执行方法之前授权) @PreFilter(执行方法之前过滤)
* post : @PostAuthorize (执行方法之后授权) @PostFilter(执行方法之后过滤)
*
* @author 硝酸铜
* @date 2021/6/2
*/
@EnableWebSecurity(debug = true)
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
}
ps:@PreAuthorize
,@PreFilter
,@PostAuthorize
,@PostFilter
都是通过注解形式实现的,也就是通过AOP来实现的,那么由于动态代理的原因,必须是从外部调用该方法才能生效,一个类调用自己的方法是没用的,比如
/**
* 匿名用户
* @author 硝酸铜
* @date 2021/6/7
*/
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {
...
@GetMapping(value = "/test")
public String test(){
return findAll();
}
@PreAuthorize(value = "hasAuthority('ADMIN')")
public String findAll(){
return "All";
}
}
test接口中调用同一个类中的方法,@PreAuthorize
不会生效
pre
@PreAuthorize(value = "hasAuthority('ADMIN')")
@GetMapping(value = "/hello")
public String getHello(){
return "Hello ";
}
在方法上加上注解即可,其用法和access
相同
我们用只有USER
角色的test账号去访问:
当然403被拒绝
只有有ADMIN
角色的账号才能访问成功
post
方法调用后的安全注解,先执行方法,利用对返回的对象进行某种判断,决定是否授权
@PostAuthorize("returnObject.username == authentication.name")
@GetMapping("/users/email/{email}")
public User getUserByEmail(@PathVariable String email){
return userService.findByEmail(email);
}
这里的例子就是一种场景,根据email查询用户数据,并且只能返回当前认证的用户的数据
因为authentication
中是没有email的,通过查询得到用户之后,才回去判断查询到的用户是不是用户本身,然后进行授权。
这种POST方式尽量少用,这种查询还好,如果是存在数据变更操作的方法,那么就不推荐去使用了。因为这种授权方式是执行完方法之后才进行授权,即使没有通过授权,方法里面的代码已经被执行了,如果涉及数据变更,那么数据已经被更改了,有很大的安全隐患。
授权流程分析
我们来分析一下流程,为了区分url和方法级注解,我将@pre
注解放在了Service里面
现在有这样一个接口:
/**
* 匿名用户
* @author 硝酸铜
* @date 2021/6/7
*/
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {
...
@Resource
private UserDetailsServiceImpl userDetailsService;
@GetMapping(value = "/test")
public String test(){
return userDetailsService.findAll();
}
}
@Service
public class UserDetailsServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService {
...
@PreAuthorize(value = "hasAuthority('ADMIN')")
public String findAll(){
return "All";
}
}
这个接口没用限制,并且在SecurityConfig
中,我们设置/authorize/**
的安全表达式为permitAll()
,它调用了UserDetailsServiceImpl
中一个只能ADMIN
角色访问的方法
不去登陆访问这个接口:
/authorize/test
接口授权过程:
url支持匿名访问,在web url这一层WebExpressionVoter
投票器已经给你投票通过了
并且这里用的策略是AffirmativeBased
,表示只要有一票通过即可。
也就是说现在你可以访问/authorize/test
接口了,也就是说匿名用户可以进入test()
方法中
findAll()
方法授权过程:
最上面显示使用了方法级的拦截器
我们这里因为没有登陆,所以是匿名用户
然后PreInvocationAuthorizationAdviceVoter
(注意,这里名字中有带Advice
说明是面向切面的方式进行拦截的)投票器一看,不是ADMIN
角色,不满足要求,投了拒绝票
RoleVoter
一看,没有设定ROLE_
开头的ConfigAttribute
,直接投了弃权票
AuthenticatedVoter
一看,匿名用户,我直接弃权
并且这里用的策略也是AffirmativeBased
,表示只要有一票通过即通过。
但是这里的投票器都没有通过的票,所以最后拒绝访问
授权源码解析
流程分析清楚了,我们来看看授权的源码
来分析一下,SpringSecurity是如何将安全表达式转化为这么灵活的一个机制的?其内部的检查流程是怎么样的?
方法级安全表达式,pre
前置安全表达式的投票器是PreInvocationAuthorizationAdviceVoter
这个类,来看看其内部的部分核心代码
public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> {
...
public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) {
//取出安全表达式
PreInvocationAttribute preAttr = this.findPreInvocationAttribute(attributes);
if (preAttr == null) {
return 0;
} else {
//检查是否允许授权,是则返回1
return this.preAdvice.before(authentication, method, preAttr) ? 1 : -1;
}
}
...
}
这里的投票方法vote()
逻辑为:首先取出安全表达式,然后判断是否允许授权,允许则投赞成票
进入before()
方法,其进入了一个叫做ExpressionBasedPreInvocationAdvice
的类中,逻辑如下
public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice {
...
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) {
//同样首先取出安全表达式
PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute)attr;
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi);
//preFilter 设定的前置过滤器
Expression preFilter = preAttr.getFilterExpression();
//preAuthorize ,也就是设定的类似 hasRole('Admin')这样的安全表达式
Expression preAuthorize = preAttr.getAuthorizeExpression();
if (preFilter != null) {
Object filterTarget = this.findFilterTarget(preAttr.getFilterTarget(), ctx, mi);
this.expressionHandler.filter(filterTarget, preFilter, ctx);
}
//判断当前认证的用户是否能够通过安全表达式
return preAuthorize != null ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true;
}
...
}
关于安全表达式的解析和判断,这里就不多做赘述了,有兴趣的小伙伴们可以自行看看源码哈
这里一个投票器的投票逻辑就结束了
之前说过,AccessDecisionManager
对一组AccessDecisionVoter
(投票器)实现进行轮询授权决定,那么是怎么样轮询的呢?
如果你在上面投票的源代码中打一个断点,你就会发现,PreInvocationAuthorizationAdviceVoter.vote()
方法结束后,来到了AffirmativeBased
类的decide()
方法中。
AffirmativeBased
类就是AbstractAccessDecisionManager
的一个具体实现,其源码逻辑如下:
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
Iterator var5 = this.getDecisionVoters().iterator();
//轮询投票器
while(var5.hasNext()) {
AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
//投票
int result = voter.vote(authentication, object, configAttributes);
switch(result) {
case -1:
++deny;
break;
// 授予权限,投票1
case 1:
return;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
} else {
this.checkAllowIfAllAbstainDecisions();
}
}
}
三种投票策略
ConsensusBased
多数服从少数AffirmaticeBased
有一票就可以通过UnanimousBased
需要全票才能通过(默认采取的策略)
这里的AffirmaticeBased
是指有一票就可以通过,所以当一个投票器投票1,即同意授权,则就授予权限。