zoukankan      html  css  js  c++  java
  • Spring Security方法级别授权使用介绍

    1.简介

    简而言之,Spring Security支持方法级别的授权语义

    通常,我们可以通过限制哪些角色能够执行特定方法来保护我们的服务层 - 并使用专用的方法级安全测试支持对其进行测试

    在本文中,我们将首先回顾一些安全注释的使用。然后,我们将专注于使用不同的策略测试我们的方法安全性。

    2.启用方法级别的安全授权配置

    首先,要使用Spring Method Security,我们需要添加spring-security-config依赖项

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
    

    如果我们想使用Spring Boot,我们可以使用包含spring-security-config的spring-boot-starter-security依赖项

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

    接下来,我们需要启用全局方法级别授权安全性

    @Configuration
    @EnableGlobalMethodSecurity(
      prePostEnabled = true, 
      securedEnabled = true, 
      jsr250Enabled = true)
    public class MethodSecurityConfig 
      extends GlobalMethodSecurityConfiguration {
    }
    
    • prePostEnabled属性启用Spring Security前/后注释
    • securedEnabled属性确定是否应启用@Secured注释
    • jsr250Enabled属性允许我们使用@RoleAllowed注释

    我们将在下一节中详细探讨这些注释。

    3.应用方法级别安全性

    3.1。使用@Secured Annotation

    @Secured注释用于指定方法上的角色列表。因此,如果用户至少具有一个指定的角色,则用户能访问该方法

    我们定义一个getUsername方法:

    @Secured("ROLE_VIEWER")
    public String getUsername() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        return securityContext.getAuthentication().getName();
    }
    

    里,@ Secure(“ROLE_VIEWER”)注释定义只有具有ROLE_VIEWER角色的用户才能执行getUsername方法

    此外,我们可以在@Secured注释中定义角色列表:

    @Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
    public boolean isValidUsername(String username) {
        return userRoleRepository.isValidUsername(username);
    }
    

    在这种情况下,配置指出如果用户具有ROLE_VIEWER或ROLE_EDITOR,则该用户可以调用isValidUsername方法

    @Secured注释不支持Spring Expression Language(SpEL)

    3.2。使用@RoleAllowed注释

    @RoleAllowed注释是JSR-250对@Secured注释的等效注释

    基本上,我们可以像@Secured一样使用@RoleAllowed注释。因此,我们可以重新定义getUsername和isValidUsername方法

    @RolesAllowed("ROLE_VIEWER")
    public String getUsername2() {
        //...
    }
         
    @RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
    public boolean isValidUsername2(String username) {
        //...
    }
    

    同样,只有具有角色ROLE_VIEWER的用户才能执行getUsername2

    同样,只有当用户至少具有ROLE_VIEWER或ROLER_EDITOR角色之一时,用户才能调用isValidUsername2

    3.3。使用@PreAuthorize和@PostAuthorize注释

    @PreAuthorize和@PostAuthorize注释都提供基于表达式的访问控制。因此,可以使用SpEL(Spring Expression Language)编写

    @PreAuthorize注释在进入方法之前检查给定的表达式,而@PostAuthorize注释在执行方法后验证它并且可能改变结果

    现在,让我们声明一个getUsernameInUpperCase方法,如下所示:

    @PreAuthorize("hasRole('ROLE_VIEWER')")
    public String getUsernameInUpperCase() {
        return getUsername().toUpperCase();
    }
    

    @PreAuthorize(“hasRole('ROLE_VIEWER')”)与我们在上一节中使用的@Secured(“ROLE_VIEWER”)具有相同的含义。您可以在以前的文章中发现更多安全表达式详细信息。

    因此,注释@Secured({“ROLE_VIEWER”,“ROLE_EDITOR”})可以替换为@PreAuthorize(“hasRole('ROLE_VIEWER')或hasRole('ROLE_EDITOR')”)

    @PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
    public boolean isValidUsername3(String username) {
        //...
    }
    

    而且,我们实际上可以使用method参数作为表达式的一部分

    @PreAuthorize("#username == authentication.principal.username")
    public String getMyRoles(String username) {
        //...
    }
    

    这里,只有当参数username的值与当前主体的用户名相同时,用户才能调用getMyRoles方法

    值得注意的是,@ PreAuthorize表达式可以替换为@PostAuthorize表达式

    让我们重写getMyRoles

    @PostAuthorize("#username == authentication.principal.username")
    public String getMyRoles2(String username) {
        //...
    }
    

    但是,在上一个示例中,授权将在执行目标方法后延迟。

    此外,@ PostAuthorize注释提供了访问方法结果的能力

    @PostAuthorize
      ("returnObject.username == authentication.principal.nickName")
    public CustomUser loadUserDetail(String username) {
        return userRoleRepository.loadUserByUserName(username);
    }
    

    在此示例中,如果返回的CustomUser的用户名等于当前身份验证主体的昵称,则loadUserDetail方法会成功执行

    3.4。使用@PreFilter和@PostFilter注释

    Spring Security提供了@PreFilter注释来在执行方法之前过滤集合参数

    @PreFilter("filterObject != authentication.principal.username")
    public String joinUsernames(List<String> usernames) {
        return usernames.stream().collect(Collectors.joining(";"));
    }
    

    在此示例中,我们将过滤除经过身份验证的用户名以外的所有用户名

    这里,我们的表达式使用名称filterObject来表示集合中的当前对象

    但是,如果该方法有多个参数是集合类型,我们需要使用filterTarget属性来指定我们要过滤的参数

    @PreFilter
      (value = "filterObject != authentication.principal.username",
      filterTarget = "usernames")
    public String joinUsernamesAndRoles(
      List<String> usernames, List<String> roles) {
      
        return usernames.stream().collect(Collectors.joining(";")) 
          + ":" + roles.stream().collect(Collectors.joining(";"));
    }
    

    此外,我们还可以使用@PostFilter注释过滤返回的方法集合:

    @PostFilter("filterObject != authentication.principal.username")
    public List<String> getAllUsernamesExceptCurrent() {
        return userRoleRepository.getAllUsernames();
    }
    

    在这种情况下,名称filterObject引用返回集合中的当前对象

    使用该配置,Spring Security将遍历返回的列表并删除与主体用户名匹配的任何值

    3.5。Method Security元注释

    我们发现经常有使用相同安全配置保护不同方法的情况

    在这种情况下,我们可以定义一个Security元注释:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @PreAuthorize("hasRole('VIEWER')")
    public @interface IsViewer {
    }
    

    接下来,我们可以直接使用@IsViewer注释来保护我们的方法

    Security元注释是一个好主意,因为它们添加了更多语义并将我们的业务逻辑与安全框架分离。

    3.6。类级别Security注释

    如果我们发现对一个类中的每个方法使用相同的Security注释,我们可以考虑将该注释放在类级别

    @Service
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public class SystemService {
     
        public String getSystemYear(){
            //...
        }
      
        public String getSystemDate(){
            //...
        }
    }
    

    上面的示例中,安全规则hasRole('ROLE_ADMIN')将应用于getSystemYear和getSystemDate方法

    3.7。方法上有的多重Security注释

    我们还可以在一个方法上使用多个Security注释:

    @PreAuthorize("#username == authentication.principal.username")
    @PostAuthorize("returnObject.username == authentication.principal.nickName")
    public CustomUser securedLoadUserDetail(String username) {
        return userRoleRepository.loadUserByUserName(username);
    }
    

    因此,Spring将在执行securedLoadUserDetail方法之前和之后验证授权

    4.重要考虑因素

    我们想提醒两点方法Security:

    • 默认情况下,Spring AOP代理用于应用方法安全性 - 如果安全方法A由同一类中的另一个方法调用,则A中的安全性将被完全忽略这意味着方法A将在没有任何安全检查的情况下执行,这同样适用于私有方
    • Spring SecurityContext是线程绑定的 - 默认情况下,安全上下文不会传播到子线程

    5.测试方法Security

    5.1。配置

    要使用JUnit测试Spring Security,我们需要spring-security-test依赖项:

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
    </dependency>
    

    我们不需要指定依赖版本,因为我们使用的是Spring Boot插件。

    接下来,让我们通过指定runner和ApplicationContext配置来配置一个简单的Spring Integration测试

    @RunWith(SpringRunner.class)
    @ContextConfiguration
    public class TestMethodSecurity {
        // ...
    }
    

    5.2。测试用户名和角色

    现在我们的配置准备好了,让我们尝试测试我的getUsername方法,该方法由注释@Secured(“ROLE_VIEWER”)保护

    @Secured("ROLE_VIEWER")
    public String getUsername() {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        return securityContext.getAuthentication().getName();
    }
    

    由于我们在这里使用@Secured注释,因此需要对用户进行身份验证以调用该方法。否则,我们将获得AuthenticationCredentialsNotFoundException

    因此,我们需要为用户提供测试我们的安全方法。为此,我们使用@WithMockUser修饰测试方法并提供用户和角色

    @Test
    @WithMockUser(username = "john", roles = { "VIEWER" })
    public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
        String userName = userRoleService.getUsername();
         
        assertEquals("john", userName);
    }
    

    我们提供了一个经过身份验证的用户,其用户名是john,其角色是ROLE_VIEWER。如果我们不指定用户名或角色,则默认用户名为user,默认角色为ROLE_USER

    请注意,此处不必添加ROLE_前缀,Spring Security将自动添加该前缀

    如果我们不想拥有该前缀,我们可以考虑使用权限而不是角色

    例如,让我们声明一个getUsernameInLowerCase方法:

    @PreAuthorize("hasAuthority('SYS_ADMIN')")
    public String getUsernameLC(){
        return getUsername().toLowerCase();
    }
    

    我们可以使用权限测试:

    @Test
    @WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
    public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
        String username = userRoleService.getUsernameInLowerCase();
     
        assertEquals("john", username);
    }
    

    如果我们想在许多测试用例中使用相同的用户,我们可以在测试类中声明@WithMockUser注释:

    @RunWith(SpringRunner.class)
    @ContextConfiguration
    @WithMockUser(username = "john", roles = { "VIEWER" })
    public class TestWithMockUserAtClassLevel {
        //...
    }
    

    如果我们想以匿名用户身份运行我们的测试,我们可以使用@WithAnonymousUser注释:

    @Test(expected = AccessDeniedException.class)
    @WithAnonymousUser
    public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
        userRoleService.getUsername();
    }
    

    在上面的示例中,我们期望AccessDeniedException,因为匿名用户未被授予角色ROLE_VIEWER或权限SYS_ADMIN

    5.3。使用Custom UserDetailsService进行测试

    对于大多数应用程序,通常使用自定义类作为身份验证主体。在这种情况下,自定义类需要实现org.springframework.security.core.userdetails.UserDetails接口。

    在本文中,我们声明了一个CustomUser类,它扩展了UserDetails的现有实现,即org.springframework.security.core.userdetails.User

    public class CustomUser extends User {
        private String nickName;
        // getter and setter
    }
    

    让我们在第3节中使用@PostAuthorize注释取回示例:

    @PostAuthorize("returnObject.username == authentication.principal.nickName")
    public CustomUser loadUserDetail(String username) {
        return userRoleRepository.loadUserByUserName(username);
    }
    

    在这种情况下,只有返回的CustomUser的用户名等于当前身份验证主体的昵称时,该方法才会成功执行。

    如果我们想测试该方法,我们可以提供UserDetailsService的实现,它可以根据用户名加载我们的CustomUser

    @Test
    @WithUserDetails(
      value = "john", 
      userDetailsServiceBeanName = "userDetailService")
    public void whenJohn_callLoadUserDetail_thenOK() {
      
        CustomUser user = userService.loadUserDetail("jane");
     
        assertEquals("jane", user.getNickName());
    }
    

    这里,@ WithUserDetails注释声明我们将使用UserDetailsService来初始化我们经过身份验证的用户。该服务由userDetailsServiceBeanName属性引用。这个UserDetailsService可能是一个真正的实现,或者用于测试目的。

    此外,该服务将使用属性值的值作为加载UserDetails的用户名。

    方便的是,我们也可以在类级别使用@WithUserDetails注释进行修饰,类似于我们对@WithMockUser注释所做的操作

    5.4。使用Meta注释进行测试

    我们经常发现自己在各种测试中一遍又一遍地重复使用相同的用户/角色。

    对于这些情况,创建元注释很方便。

    修改前面的示例@WithMockUser(username =“john”,roles = {“VIEWER”}),我们可以将元注释声明为:

    @Retention(RetentionPolicy.RUNTIME)
    @WithMockUser(value = "john", roles = "VIEWER")
    public @interface WithMockJohnViewer { }
    

    然后我们可以在测试中简单地使用@WithMockJohnViewer:

    @Test
    @WithMockJohnViewer
    public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
        String userName = userRoleService.getUsername();
     
        assertEquals("john", userName);
    }
    

    同样,我们可以使用元注释来使用@WithUserDetails创建特定于域的用户。

    六,结论

    在本教程中,我们探讨了在Spring Security中使用Method Security的各种选项。

    我们还经历了一些技术来轻松测试方法安全性,并学习如何在不同的测试中重用模拟用户

    可以在Github上找到本教程的所有示例。

  • 相关阅读:
    JSON中toJSONString、ParseObject、parseArray的作用以及用 com.alibaba.fast.JSONArray解析字符串或者List集合
    几种操作Elasticsearch方法
    ElasticSearch 中boolQueryBuilder的使用
    maven依赖
    @javax.ws.rs Webservice注解
    Bug-滑稽
    Web安全之文件上传
    利用SSRF漏洞内网探测来攻击Redis(通过curl命令 & gopher协议)
    利用SSRF漏洞内网探测来攻击Redis(请求头CRLF方式)
    SVG XSS一般过程
  • 原文地址:https://www.cnblogs.com/xjknight/p/10945825.html
Copyright © 2011-2022 走看看