Loading

SpringSecurity系列学习(五):授权流程和源码分析

系列导航

SpringSecurity系列

SpringSecurityOauth2系列

授权

看到标题了吧?这一节咱们不上号哈,先从原理入手!减少踩坑的概率!

上一节我们完成了认证的流程,接下来我们来谈一谈授权

在正式开始之前,先给大家提个醒,授权这个东西,相比起认证,其实更偏业务一点,在技术上不难,关键是业务设计。这里面是个大学问,每一个项目的权限设计都不同,怎么设计好用户角色权限的关系(我剧透了?)其实才是最难的点。

什么是授权

根据用户的权限来控制用户使用资源的过程就是授权

用微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包。

发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

为什么要授权?

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的, 控制不同的用户能够访问不同的资源。

授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

SpringSecurity中的授权

AbstractAccessDecisionManager

根据相关信息,做出授权决定

这个类中有一个decide(Object)的方法,接收Object类型的参数,是一个安全对象。其安全对象具体是什么,SpringSecurity并没有去严格的限制它。其检查逻辑需要去自定义

基于投票的AccessDecisionManager实现

AccessDecisionManager对一组AccessDecisionVoter(投票器)实现进行轮询授权决定(和之前我们学习认证的时候,去轮询AuthenticationProvider是一样的)。然后AccessDecisionManager根据对投票的评估,决定是否抛出一个AccessDeniedException异常。

三种投票策略

  • ConsensusBased 多数服从少数
  • AffirmaticeBased 有一票就可以通过
  • UnanimousBased 需要全票才能通过(默认采取的策略)

AccessDecisionVoter的一种实现:RoleVoter

如果有任何的ConfigAttribute是以ROLE_开头的,它就会进行投票

如果GrantedAuthority(权限列表)中有一个或者多个以ROLE_开头的角色能够匹配上ConfigAttribute中设置的角色,这个投票器就投票授予访问权限

如果没有任何GrantedAuthority返回的字符串与角色字符串相匹配,它就会投票拒绝访问

如果没有ConfigAttributeROLE_开头的角色,那么就放弃投票

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")

这个类图可以表示:AccessDecisionManagerAccessDecisionVoter进行轮询。

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,即同意授权,则就授予权限。

posted @ 2021-09-27 16:43  硝酸铜  阅读(866)  评论(1编辑  收藏  举报