SpringSecurity-授权学习日志

授权

以我自己的学习习惯,我喜欢先学会怎么用再去学习深一些,所以我先借助示例来学会怎么用,然后再借助文档中的架构和更详细的内容来深入和补充

先是基本的概念

SpringSecurity的高级授权能力是其受欢迎的原因之一,它能通过简单的配置实现对不同role授予不同的访问权限,而授权的配置方式可以通过俩种,一种是通过uri来配置,这种需要再security的配置文件中配置,一种是方法上的配置,简单说就是加个注解,但是底层还是比使用要复杂很多的,我打算先学会用再去学原理

基于之前的认证的学习使用的配置的示例,该如何去使用SpringSecurity的授权功能呢

授权Authorization是在认证Authentication后进行的,授权的配置主要依赖SpringSecurity Filter Chain中的authorizeHttpRequests部分,以及认证过程中设置到Authentication对象中的权限消息Authorities,这个Authorities通常是角色或者更细粒度的表示

授权判断就是检查当前用户的Authentication对象中的Authorities是否满足访问某个资源所需要的权限需求

配置授权的方式有俩种,一种是基于请求URI的配置,需要在SpringSecurity的配置文件config上配置。

还有一种是基于方法也就是基于特定接口做的授权规则,先来看看这俩种分别如何配置,再去研究他们是如何工作的

基于请求URI配置

简单说如何配置的话就是直接在SecurityFilterChain上通过authorizeHttpRequests来配置

@Bean
 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
     http.csrf().disable()
         .authorizeHttpRequests(auth -> auth
             .requestMatchers("/login", "/register").permitAll() // 登录注册公开访问

             // 只有拥有 ADMIN 角色的用户才能访问 /admin/** 下的资源
             .requestMatchers("/admin/**").hasRole("ADMIN")

             // 拥有 ADMIN 或 EDITOR 任意一个角色的用户才能访问 /articles/edit/**
             .requestMatchers("/articles/edit/**").hasAnyRole("ADMIN", "EDITOR")

             // 拥有某个细粒度权限(Authority)的用户才能访问,例如需要 "read:users" 权限
             .requestMatchers("/api/users").hasAuthority("read:users")

             // 拥有 "write:articles" 权限或 ADMIN 角色的用户才能访问 /articles/new
             .requestMatchers("/articles/new").access("hasAuthority('write:articles') or hasRole('ADMIN')")

             .anyRequest().authenticated() // 其他所有请求必须已认证(即 SecurityContextHolder 中有 Authentication 对象,不管权限是什么)
         )
         .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
         .and()
         .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

     return http.build();
 }

还有更简单的例子

http
    .authorizeHttpRequests((authorize) -> authorize
        .anyRequest().authenticated()
    )

这个例子简单说就是直接对所有的http请求都要求进行认证

进一步地,讲一下匹配规则

http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/resource/**").hasAuthority("USER")
        .anyRequest().authenticated()
    )

还有就是带有路径参数的

http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/resource/{name}").access(new WebExpressionAuthorizationManager("#name == authentication.name"))
        .anyRequest().authenticated()
    )//路径参数name要等于context的Authentication实例的name

如何进行测试呢

@WithMockUser(authorities="USER")//授予了User的role权限进行测试
@Test
void endpointWhenUserAuthorityThenAuthorized() {
    this.mvc.perform(get("/endpoint/jon"))
        .andExpect(status().isOk());
}

@WithMockUser
@Test
void endpointWhenNotUserAuthorityThenForbidden() {
    this.mvc.perform(get("/endpoint/jon"))
        .andExpect(status().isForbidden());
}

@Test
void anyWhenUnauthenticatedThenUnauthorized() {
    this.mvc.perform(get("/any"))
        .andExpect(status().isUnauthorized())
}
http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers(RegexRequestMatcher.regexMatcher("/resource/[A-Za-z0-9]+")).hasAuthority("USER")
        .anyRequest().denyAll()
    )
http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers(HttpMethod.GET).hasAuthority("read")
        .requestMatchers(HttpMethod.POST).hasAuthority("write")
        .anyRequest().denyAll()
    )

根据Dispatcher Type来限定

先讲一下什么是Dispatcher Type

Dispatcher Type就是分发类型

例如在JavaWeb中,一个请求可以被不同方式分发到Servlet或Controller

例如

直接访问,被Servlet或者Controller访问,被错误页面机制分发

由于SpringSecurity默认只对用户直接发起的请求进行授权控制,所有要在dispatcherTypeMatchers来进行配置

http
    .authorizeHttpRequests((authorize) -> authorize
        .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
        .requestMatchers("/endpoint").permitAll()
        .anyRequest().denyAll()
    )

自定义Matcher

RequestMatcher printview = (request) -> request.getParameter("print") != null;
http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers(printview).hasAuthority("print")
        .anyRequest().authenticated()
    )

常见的授权表达式的字段和方法

permitAll - 该请求不需要授权即可调用;注意,在这种情况下,将不会从 session 中检索 Authentication。

denyAll - 该请求在任何情况下都是不允许的;注意在这种情况下,永远不会从会话中检索 Authentication。

hasAuthority - 请求要求 Authentication 的 GrantedAuthority 符合给定值。

hasRole - hasAuthority 的快捷方式,前缀为 ROLE_ 或任何配置为默认前缀的内容。

hasAnyAuthority - 请求要 Authentication 具有符合任何给定值的 GrantedAuthority。

hasAnyRole - hasAnyAuthority 的一个快捷方式,其前缀为 ROLE_ 或任何被配置为默认的前缀。

hasPermission - 用于对象级授权的 PermissionEvaluator 实例的 hook。

这里是对最常见字段的简要介绍:

authentication - 与此方法调用相关的 Authentication 实例。

principal - 与此方法调用相关的 Authentication#getPrincipal。

SecurtiyMather

@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")  // ← 只处理 /api/** 下的请求
        .authorizeHttpRequests(...)   // ← 配置授权规则
        .addFilterBefore(...)         // ← 添加自定义过滤器
        .formLogin(...);              // ← 登录方式配置
    return http.build();
}

@Bean
public SecurityFilterChain publicSecurityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/public/**") // ← 只处理 /public/** 下的请求
        .authorizeHttpRequests(...)
        .permitAll();                   // ← 所有人都可以访问
    return http.build();
}

Request授权组件时如何工作的

使用AuthorizationFilter构造一个Supplier,从SecurityContextHolder中检索一个Authentication

其中Supplier可以看作一个用来延迟加载Authentication的供应商,需要时再取出来

使用Supplier的原因是

  • 请求在过滤器链中可能会被转发(forward)、包含(include)多次
  • 有些情况可能还没有完全解析完身份(如异步请求)
  • 使用 Supplier 可以保证每次获取的是最新的身份信息

将Supplier<Authentication>和HttpServletRequest传递给AuthorizationManager,AuthorizationManager将请求与authorizationHttpRequests中的模式匹配

如果授权被拒绝,会发布一个 AuthorizationDeniedEvent,并抛出一个 AccessDeniedException。在这种情况下,ExceptionTranslationFilter 会处理 AccessDeniedException

如果访问被授权,就会 发布一个AuthorizationGrantedEventAuthorizationFilter 继续进行 FilterChain,允许应用程序正常处理

那么如何基于方法安全呢(Method Security)

要想启动方法级别首先需要在SpringSecurity的配置类中启动

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
// ... 其他导入

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用方法级别安全
// @EnableGlobalMethodSecurity // 旧版本可能使用这个注解
// ... 其他注解
public class SecurityConfig {
    // ... 其他 Bean 和配置 ...
}

启动之后有多种使用的注解

1.PreAuthorize

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')") // 只有拥有 ADMIN 角色的用户才能调用此方法
    public List<User> getAllUsers() {
        // ... 获取所有用户逻辑 ...
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')") // 拥有 ADMIN 或 EDITOR 任意一个角色即可
    public void createPost(Post post) {
        // ... 创建文章逻辑 ...
    }

    @PreAuthorize("hasAuthority('delete:user')") // 拥有 'delete:user' 权限的用户才能调用
    public void deleteUser(Long userId) {
        // ... 删除用户逻辑 ...
    }

    @PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')") // 用户只能查看自己的信息,或者 ADMIN 可以查看所有用户
    public User getUserById(Long userId) {
        // ... 获取指定ID用户逻辑 ...
    }
}

PreAuthorize是在方法执行之前进行授权

2.PostAuthorize

在方法执行之后进行授权检查,可以访问方法的返回值 (returnObject)。不太常用,因为它在方法执行后才检查,如果方法有副作用(比如删除了数据),即使授权失败也无法撤销。主要用于检查用户是否有权查看返回的结果。

@PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')") // 返回的文章只有作者本人或 ADMIN 可见
 public Post getPostById(Long postId) {
     // ... 获取文章逻辑 ...
     // 返回 Post 对象
 }

3.Secured,RolesAllowed

  • @Secured({"ROLE_ADMIN", "ROLE_USER"}):一个更简单的注解,只能检查指定的角色或 GrantedAuthority 名称。不如 @PreAuthorize 灵活,不能使用 SpEL 表达式。需要 @EnableGlobalMethodSecurity(securedEnabled = true)
  • @RolesAllowed({"ADMIN", "USER"}):JSR 250 标准的注解,功能类似 @Secured,也是检查角色。角色名不需要加 "ROLE_" 前缀(Spring Security 会自动处理)。需要 @EnableGlobalMethodSecurity(jsr250Enabled = true)

方法安全工作原理

Method Security是基于SpringAOP构建的,可以根据实际要求修改默认配置

PreAuthorize是检查入门资格,而PostAuthorize是检查返回值是否符合一定要求

例如

@Service
public class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Customer readCustomer(String id) { ... }
}

这里的PreAuthorize是判断role的要求

而PostAuthorize是判断return的Object的owner属性是否符合要求

内部的整体流程是

这个图描绘得非常清晰

首先是使用了SpringAOP为readCustomer实现了Pre和Post俩个Authrized?过程

在进行readCustomer之前,调用AuthorizationManagerBeforeMethodInterceptor,这个拦截器又会调用PreAuthorizeAuthorizationManager去检查,然后它又会使用MethodSecurityExpressionHandler去解析SpEL表达式, 并从包含 Supplier&lt;Authentication> 和 MethodInvocation 的 MethodSecurityExpressionRoot 构建相应的 EvaluationContext。"

根据评估,如果通过就调用方法,否者就会发布一个AuthorizationDeniedEvent,并抛出AccessDeniedException,然后ExceptionTranslationFilter会捕获并响应一个403状态码

Post的过程也是相似的,这里就不说了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值