17、Spring Security 实战 - SpringSecurity 授权管理

1. 权限管理

1. 授权核心概念

身份认证 ,就是判断一个用户是否为合法用户的处理过程。Spring Security 中支持多种不同方式的认证,但是无论开发者使用那种方式认证,都不会影响授权功能使用。因为Spring Security 很好做到了认证和授权解耦。

授权,即访问控制,控制谁能访问哪些资源。简单的理解授权就是根据系统提前设置好的规则,给用户分配可以访问某一个资源的权限,用户根据自己所具有权限,去执行相应操作。

认证成功之后会将当前登录用户信息保存到Authentication 对象中,Authentication 对象中有一个 getAuthorities() 方法,用来返回当前登录用户具备的权限信息,也就是当前用户具有权限信息。该方法的返回
值为Collection<? extends GrantedAuthority>,当需要进行权限判断时,就回根据集合返回权限信息调用相应方法进行判断。

当客户端发起请求时,身份认证过滤器会对用户进行身份认证,身份认证成功后,身份认证过滤器会将用户详情信息存储在安全上下文中,并将请求转发给授权过滤器,授权过滤器决定是否允许调用。

public interface Authentication extends Principal, Serializable {
   
     

    // 由 AuthenticationManager 设置以指示已授予主体的权限。
    Collection<? extends GrantedAuthority> getAuthorities();

    // 返回身份认证过程中返回的密码或者任何秘钥
    Object getCredentials();

    // 存储有关身份验证请求的其他详细信息。 这些可能是 IP 地址、证书序列号等。
    Object getDetails();

    // 被认证的主体的身份。
    // 被认证的主体的身份。 在使用用户名和密码的身份验证请求的情况下,这将是用户名。 调用者应填充身份验证请求的主体。
	// AuthenticationManager 实现通常会返回一个包含更丰富信息的 Authentication 作为应用程序使用的主体。 许多身份验证提供程序将创建一个 UserDetails 对象作为主体。
    Object getPrincipal();

    // 如果身份认证程序结束则返回true,如果正在进行则返回false
    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;

}

public interface GrantedAuthority extends Serializable {
   
     
    // 返回权限的名称
    String getAuthority();
}

那么问题来了,针对于这个返回值 GrantedAuthority 应该如何理解呢? 是⻆色还是权限?

我们针对于授权可以是 基于⻆色权限管理 和 基于资源权限管理 ,从设计层面上来说,⻆色和权限是两个完全不同的东⻄:权限是一些具体操作,⻆色则是某些权限集合。如:READ_BOOK 和 ROLE_ADMIN 是完全不同的。因此至于返回值是什么取决于你的业务设计情况:

基于⻆色权限设计就是: 用户–》⻆色–》资源,返回就是用户的 ⻆色
基于资源权限设计就是: 用户–》权限–》资源,返回就是用户的 权限
基于⻆色和资源权限设计就是: 用户–》⻆色–》权限–》资源,返回统称为用户的权限

为什么可以统称为权限,因为从代码层面⻆色和权限没有太大不同都是权限,特别是在Spring Security 中,⻆色和权限处理方式基本上都是一样的。唯一区别SpringSecurity 在很多时候会自动给⻆色添加一个 ROLE_ 前缀,而权限则不会自动添加。

2. 权限管理策略

Spring Security 中提供的权限管理策略主要有两种类型:

基于过滤器(URL)的权限管理 (FilterSecurityInterceptor) :基于过滤器的权限管理主要是用来拦截 HTTP 请求,拦截下来之后,根据 HTTP请求地址进行权限校验。

基于AOP (方法)的权限管理 (MethodSecurityInterceptor):基于 AOP 权限管理主要是用来处理方法级别的权限问题。当需要调用某一个方法时,通过 AOP 将操作拦截下来,然后判断用户是否具备相关的权限。

2. 基于 URL 权限管理

@RestController
public class DemoController {
   
     
    @GetMapping("/admin")  //ADMIN
    public String admin() {
   
     
        return "admin ok";
    }

    @GetMapping("/user")  //USER
    public String user() {
   
     
        return "user ok";
    }

    @GetMapping("/getInfo")  //READ_INFO
    public String getInfo() {
   
     
        return "info ok";
    }
}

/**
 * 自定义 spring security 配置类
 */
@Configuration
public class SecurityConfig  extends WebSecurityConfigurerAdapter {
   
     

    //创建内存数据源
    @Bean
    public UserDetailsService userDetailsService() {
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        http.authorizeRequests()
                //具有 admin 角色   通用: /admin /admin/  /admin.html
                .mvcMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN")
                //具有 user 角色
                .mvcMatchers("/user").hasRole("USER")
                //READ_INFO 权限
                .mvcMatchers("/getInfo").hasAuthority("READ_INFO")
                .antMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}

访问:http://localhost:8080/user,使用root/123登录后可访问,再访问http://localhost:8080/getInfo 则会报错403异常。

 

3. 基于 方法 权限管理

基于方法的权限管理主要是通过 A0P 来实现的,Spring Security 中通过MethodSecurityInterceptor 来提供相关的实现。不同在于FilterSecurityInterceptor 只是在请求之前进行前置处理,MethodSecurityInterceptor 除了前置处理之外还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不同的实现类。

@EnableGlobalMethodSecurity 该注解是用来开启权限注解,用法如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true, jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{
   
     }

perPostEnabled: 开启 Spring Security 提供的四个权限注解,@PostAuthorize、@PostFilter、@PreAuthorize 以及@PreFilter;

securedEnabled: 开启 Spring Security 提供的 @Secured 注解支持,该注解不支持权限表达式;

jsr250Enabled: 开启 JSR-250 提供的注解,主要是@DenyAll、@PermitAll、@RolesAll 同样这些注解也不支持权限表达式;

@PostAuthorize: 在目前标方法执行之后进行权限校验。

@PostFiter: 在目标方法执行之后对方法的返回结果进行过滤。

@PreAuthorize:在目标方法执行之前进行权限校验。

@PreFiter:在目前标方法执行之前对方法参数进行过滤。

@Secured:访问目标方法必须具各相应的⻆色。

@DenyAll:拒绝所有访问。

@PermitAll:允许所有访问。

@RolesAllowed:访问目标方法必须具备相应的⻆色。

@RestController
@RequestMapping("/hello")
public class AuthorizeMethodController {
   
     

    // @PreAuthorize("hasRole('ADMIN')  and authentication.name =='win7'")
    @PreAuthorize("hasAuthority('READ_INFO')")
    @GetMapping
    public String hello() {
   
     
        return "hello";
    }

    @PreAuthorize("authentication.name==#name")
    @GetMapping("/name")
    public String hello(String name) {
   
     
        return "hello:" + name;
    }

    // filterTarget 必须是数组集合类型
    @PreFilter(value = "filterObject.id%2!=0",filterTarget = "users") 
    @PostMapping("/users")
    public void addUsers(@RequestBody List<User> users) {
   
     
        System.out.println("users = " + users);
    }

    @PostAuthorize("returnObject.id==1")
    @GetMapping("/userId")
    public User getUserById(Integer id) {
   
     
        return new User(id, "blr");
    }

    // 用来对方法返回值进行过滤
    @PostFilter("filterObject.id%2==0")
    @GetMapping("/lists")
    public List<User> getAll() {
   
     
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
   
     
            users.add(new User(i, "blr:" + i));
        }
        return users;
    }

    // 只能判断角色
    @Secured({
   
     "ROLE_USER"}) 
    @GetMapping("/secured")
    public User getUserByUsername() {
   
     
        return new User(99, "secured");
    }

    // 具有其中一个即可
    @Secured({
   
     "ROLE_ADMIN","ROLE_USER"}) 
    @GetMapping("/username")
    public User getUserByUsername2(String username) {
   
     
        return new User(99, username);
    }
    @PermitAll
    @GetMapping("/permitAll")
    public String permitAll() {
   
     
        return "PermitAll";
    }

    @DenyAll
    @GetMapping("/denyAll")
    public String denyAll() {
   
     
        return "DenyAll";
    }

    // 具有其中一个角色即可
    @RolesAllowed({
   
     "ROLE_ADMIN","ROLE_USER"}) 
    @GetMapping("/rolesAllowed")
    public String rolesAllowed() {
   
     
        return "RolesAllowed";
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
   
     
    private Integer id;
    private String name;
}

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    //创建内存数据源
    @Bean
    public UserDetailsService userDetailsService() {
   
     
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
        inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
        return inMemoryUserDetailsManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
     
        auth.userDetailsService(userDetailsService());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        http.authorizeRequests()
                //具有 admin 角色   通用: /admin /admin/  /admin.html
                .mvcMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN")
                //具有 user 角色
                .mvcMatchers("/user").hasRole("USER")
                //READ_INFO 权限
                .mvcMatchers("/getInfo").hasAuthority("READ_INFO")
                .antMatchers(HttpMethod.GET,"/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable();
    }
}

4. 授权流程

 

SpringSecurity存在一个称作 FilterSecurityInterceptor 的拦截器,该拦截器位于整个过滤器链的末端,核心功能是对权限控制过程进行拦截,即用来判断该请求能否访问HTTP端点。当对请求进行拦截后,下一步是获取请求的访问资源,以及访问这些资源所需要的权限信息,这一步称为权限配置。

 

ConfigAttribute 在 Spring Security 中,用户请求一个资源(通常是一个接口或者一个 Java 方法)需要的⻆色会被封装成一个 ConfigAttribute 对象,在ConfigAttribute 中只有一个 getAttribute方法,该方法返回一个 String 字符
串,就是⻆色的名称。一般来说,⻆色名称都带有一个 ROLE_ 前缀,投票器AccessDecisionVoter 所做的事情,其实就是比较用户所具各的⻆色和请求某个资源所需的 ConfigAtuibute 之间的关系。

AccesDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在AccessDecisionManager 中会换个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AaccesDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。

1. FilterSecurityInterceptor#invoke

// 实现了Servlet的Filter接口
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
   
     

    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
   
     
        this.securityMetadataSource = newSource;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
   
     
        invoke(new FilterInvocation(request, response, chain));
    }

    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
   
     
        // ...
        // 调用父类的beforeInvocation方法
        InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
        // ...
        super.afterInvocation(token, null);
    }
}

2. AbstractSecurityInterceptor#beforeInvocation

public abstract class AbstractSecurityInterceptor
    implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    public abstract SecurityMetadataSource obtainSecurityMetadataSource();
    
    protected InterceptorStatusToken beforeInvocation(Object object) {
   
     
        // ... 

        // 从SecurityMetadataSource中获取请求对应的ConfigAttribute集合(权限信息)
        Collection<ConfigAttribute> attributes 
            = this.obtainSecurityMetadataSource().getAttributes(object);
        // ...

        // 是否需要认证,获取认证信息
        Authentication authenticated = authenticateIfRequired();

        // 执行授权
        attemptAuthorization(object, attributes, authenticated);

        // ...
    }
}

步骤1:DefaultFil#terInvocationSecurityMetadataSource#getAttributes
public class DefaultFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
   
     

   protected final Log logger = LogFactory.getLog(getClass());

   private final Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
    
   @Override
   public Collection<ConfigAttribute> getAttributes(Object object) {
   
     
      final HttpServletRequest request = ((FilterInvocation) object).getRequest();
      int count = 0;
      for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : this.requestMap.entrySet()) {
   
     
         if (entry.getKey().matches(request)) {
   
     
            return entry.getValue();
         }
         else {
   
     
            if (this.logger.isTraceEnabled()) {
   
     
               this.logger.trace(LogMessage.format("Did not match request to %s - %s (%d/%d)", entry.getKey(),
                     entry.getValue(), ++count, this.requestMap.size()));
            }
         }
      }
      return null;
   }

   @Override
   public boolean supports(Class<?> clazz) {
   
     
      return FilterInvocation.class.isAssignableFrom(clazz);
   }

}

步骤2:AbstractSecurityInterceptor#authenticateIfRequired
public abstract class AbstractSecurityInterceptor
    implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    private Authentication authenticateIfRequired() {
   
     
        Authentication authentication 
            		= SecurityContextHolder.getContext().getAuthentication();
        if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
   
     
            if (this.logger.isTraceEnabled()) {
   
     
                this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
            }
            return authentication;
        }
        authentication = this.authenticationManager.authenticate(authentication);
        // Don't authenticated.setAuthentication(true) because each provider does that
        if (this.logger.isDebugEnabled()) {
   
     
            this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
        }
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        return authentication;
    }

}

根据上下文对象中的 Authentication 对象判断用户是否通过身份认证,如果尚未通过身份认证,则调用AuthenticationManager 进行认证,并把 Authentication 对象存储在上下文对象中。

步骤3:AbstractSecurityInterceptor#attemptAuthorization
public abstract class AbstractSecurityInterceptor
    implements InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
   
     
    
    private AccessDecisionManager accessDecisionManager;

    private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) {
   
     
        try {
   
     
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        // ...
    }
}

public class AffirmativeBased extends AbstractAccessDecisionManager {
   
     
    
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
   
     
        int deny = 0;
        for (AccessDecisionVoter voter : getDecisionVoters()) {
   
     
            int result = voter.vote(authentication, object, configAttributes);
            switch (result) {
   
     
                case AccessDecisionVoter.ACCESS_GRANTED:
                    return;
                case AccessDecisionVoter.ACCESS_DENIED:
                    deny++;
                    break;
                default:
                    break;
            }
        }
        if (deny > 0) {
   
     
            throw new AccessDeniedException(
                this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        }
        // To get this far, every AccessDecisionVoter abstained
        checkAllowIfAllAbstainDecisions();
    }
}