Spring Security in Action 第八章 配置授权:api授权

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总


本章包括

  • 选择使用匹配器方法限制请求

  • 学习每种匹配器方法的最佳实践

在第7章中,我们学会了如何根据权限和角色来配置访问,但我们只对所有的端点应用了配置。在本章中,我们将学习如何对一组特定的请求应用授权约束。 在开发中,我们不太可能对所有请求应用相同的规则。例如某些api,只有一些特定的用户可以调用,而其他api可能是所有人都可以访问的。每个应用程序,根据业务需求,都有自己的自定义授权配置。让我们来讨论一下,当我们写访问配置时,你有哪些选项可以参考不同的请求。

之前我们使用的第一个匹配器方法是anyRequest()方法。首先,我们来谈谈按路径选择请求的问题;然后,我们也可以把HTTP方法加入到这个场景中。为了选择我们应用授权配置的请求,我们使用匹配器方法。Spring Security为我们提供了三种类型的匹配器方法。

  • MVC matchers-使用MVC表达式的路径来选择api。
  • Ant matchers-使用Ant表达式的路径来选择api。
  • regex matchers-你使用正则表达式(regex)的路径来选择api。

8.1 使用匹配器方法来选择端点

在本节中,我们将学习如何使用一般的匹配器方法。先从一个简单的例子开始,我们创建一个暴露两个api的应用程序:/hello和/ciao。我们想确保只有拥有ADMIN角色的用户才能调用/hello端点。同样地,我们要确保只有拥有MANAGER角色的用户可以调用/ciao端点。你可以在项目sia-ch8-ex1中找到这个例子。下面的列表提供了控制器类的定义。

在配置类中,我们声明一个InMemoryUserDetailsManager作为我们的UserDetailsService实例,并添加两个具有不同角色的用户。用户John拥有ADMIN角色,而Jane拥有MANAGER角色。在授权请求时,拥有ADMIN角色的用户可以调用端点/hello,我们使用mvcMatchers()方法。下一个代码清单介绍了Configuration类的定义。

代码清单8.2 配置类的定义

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;


@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .authorities("ADMIN")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .authorities("MANAGER")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers("/hello").hasRole("ADMIN")
                .mvcMatchers("/ciao").hasRole("MANAGER");
    }
}

当用户John调用api /hello时,会得到一个成功的响应。但如果是用户Jane调用同一个api,响应状态返回HTTP 403 Forbidden。同样地,对于api /ciao,只能用Jane来获得一个成功的结果。对于用户John,响应状态会返回HTTP 403 Forbidden。

如果给应用程序添加其他的api,它默认是可以被任何人访问的,甚至是未认证的用户。例如一个新的api /hola,如下面的列表所示。

代码清单8.3 在应用程序中为路径/hola添加一个新的api

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class HelloController {
    
    
    //省略部分代码
    
    @GetMapping("/hola")
    public String hola(){
    
    
        return "Hola!";
    }
}

当访问这个新的api时,可以看到,无论是否有一个有效的用户,它都可以被访问。

可以通过使用 permitAll() 方法使这种行为更加明显,例如通过在请求授权的配置链末端使用 anyRequest() 匹配器方法来做到这一点,如代码清单 8.4 所示。

清单8.4 将额外的请求明确标记为无需认证即可访问

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    //省略部分代码

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers("/hello").hasRole("ADMIN")
                .mvcMatchers("/ciao").hasRole("MANAGER")
                .anyRequest().permitAll();
    }
}

注意 当你使用匹配器来指代请求时,规则的顺序应该是由特殊到一般。这就是为什么anyRequest()方法不能在像mvc- Matchers()这样更具体的匹配器方法之前被调用。

未认证与认证失败

如果程序中设计了一个任何人都可以访问的api,则可以在不提供用户名和密码认证的情况下调用它。在这种情况下,Spring Security不会进行认证。然而,如果提供了一个用户名和密码,Spring Security会在认证过程中校验它们。如果它们是错误的(不被系统所知),认证就会失败,响应状态将是401未授权。更准确地说,如果调用代码清单8.4中的配置的/hola端点,应用程序会按照预期返回主体 “Hola!”,响应状态是200 OK。

但如果用无效的凭证调用api,响应的状态是401未授权。

curl -u bill:abcde http://localhost:8080/hola

返回的json为:

{
“status”:401,
“error”:“Unauthorized”,
“message”:“Unauthorized”,
“path”:“/hola”
}

框架的这种行为可能看起来很奇怪,但它是有意义的,因为如果你在请求中提供了用户名和密码,框架就会校验它们。正如你在第7章中学到的,应用程序总是在授权之前进行认证,如图所示。

在这里插入图片描述

授权过滤器允许任何对/hola路径的请求。但是,由于应用程序首先执行了认证逻辑,请求从未被转发到授权过滤器。 相反,认证过滤器以HTTP 401 Unauthorized进行回复。

总之,任何认证失败的情况都会产生一个状态为401 Unauthorized的响应,应用程序不会将调用转发给终端。permitAll()方法仅指授权配置,如果认证失败,将不允许进一步调用。

代码清单8.5 使所有认证的用户都能访问其他请求

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    //省略部分代码

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers("/hello").hasRole("ADMIN")
                .mvcMatchers("/ciao").hasRole("MANAGER")
                .anyRequest().authenticated();
    }
}

在大多数实际情况下,多个api可以有相同的授权规则,所以不必逐个端点地设置它们。同样,有时需要指定HTTP方法,而不仅仅是路径,就像我们之前所做的那样。 有时,只需要在一个api的路径被HTTP GET调用时为其配置规则。在这种情况下,需要为HTTP POST和HTTP DELETE定义不同的规则。在接下来的章节中,我们将对每种类型的匹配器方法进行详细讨论。

8.2 使用MVC matchers选择需要授权的请求

在本节中,我们将讨论MVC匹配器。使用MVC表达式是应用授权配置的请求的一种常见方法。

这个匹配器使用标准的MVC语法来引用路径。这种语法与使用@RequestMapping、@GetMapping、@PostMapping等注释编写api映射时使用的语法相同。可以用两种方法来声明MVC匹配器,如下所示:

  • mvcMatchers(HttpMethod method, String… patterns)-同时指定限制所适用的HTTP方法和路径。 如果你想对同一路径的不同HTTP方法适用不同的限制,这个方法很有用。
  • mvcMatchers(String…pattern)–如果只需要根据路径来进行应用授权,则更简单,更容易使用。这些限制可以自动适用于与路径一起使用的任何HTTP方法。

在这一节中,我们将讲解使用mvcMatchers()方法的多种方式。为了证明这一点,我们先写一个暴露了多个api的应用程序。

这是我们第一次编写可以用除GET以外的其他HTTP方法调用的端点。你可能已经注意到,直到现在,我还在避免使用其他的HTTP方法。其原因是Spring Security默认应用了对跨站请求伪造(CSRF)的保护。在第一章中,我描述了CSRF,它是Web应用程序最常见的漏洞之一。在很长一段时间里,CSRF都出现在OWASP的十大漏洞中。在第10章,我们将讨论Spring Security如何通过使用CSRF令牌来缓解这一漏洞。但为了使目前的例子更简单,并且能够调用所有的端点,包括那些用POST、PUT或DELETE暴露的api,我们需要在configure()方法中禁用CSRF保护。

http.csrf().disable();

我们首先定义了四个api,用于我们的测试。

  • /a using the HTTP method GET
  • /a using the HTTP method POST
  • /a/b using the HTTP method GET
  • /a/b/c using the HTTP method GET

通过这些api,我们可以考虑不同的授权配置方案。在代码清单8.6中,可以看到这些端点的定义。

代码清单8.6 我们为其配置授权的四个端点的定义

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    
    
    @PostMapping("/a")
    public String postEndpointA() {
    
    
        return "Works!";
    }
    @GetMapping("/a")
    public String getEndpointA() {
    
    
        return "Works!";
    }
    @GetMapping("/a/b")
    public String getEnpointB() {
    
    
        return "Works!";
    }
    @GetMapping("/a/b/c")
    public String getEnpointC() {
    
    
        return "Works!";
    }
}

我们还需要几个具有不同角色的用户。为了保持简单,我们继续使用一个InMemoryUserDetailsManager。在下一个代码清单中,你可以看到配置类中的UserDetailsService的定义。

清单8.7 UserDetailsService的定义

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("ADMIN")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("MANAGER")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }

}

让我们从第一种情况开始。对于使用HTTP GET方法对/a路径进行的请求,应用程序需要对用户进行认证。对于/a请求,使用HTTP POST方法不需要认证,除此之外的其他请求被拒绝。下面的代码显示了需要编写的配置,以实现这一设置。

代码清单8.8 第一个方案的授权配置,/a

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("ADMIN")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("MANAGER")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers(HttpMethod.GET,"/a")
                .authenticated()
                .mvcMatchers(HttpMethod.POST,"/a")
                .permitAll()
                .anyRequest().denyAll();
    }
}

通过这个例子,我们知道如何根据HTTP方法来区分请求了。但是,如果多个路径有相同的授权规则怎么办?当然,我们可以列举所有适用授权规则的路径,但如果我们有太多的路径,这就会使阅读代码时感到不舒服。同样,我们可能一开始就知道,一组具有相同前缀的路径总是具有相同的授权规则。我们要确保,如果一个开发者在同一组中添加了一个新的路径,它不会同时改变授权配置。为了管理这些情况,我们使用路径表达式。

对于目前的项目,我们要确保对以/a/b开头的路径的所有请求适用相同的规则。在我们的例子中,这些路径是/a/b和/a/b/c。为了实现这一点,我们使用**操作符。(Spring MVC从Ant那里借用了路径匹配的语法。)

代码清单8.9 多个路径的配置类的变化

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    //省略其他代码


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers("/a/b/**")
                .authenticated()
                .anyRequest().permitAll();
    }
}

/**正如前面的例子中所介绍的,操作符指的是任何数量的路径名称。你可以像我们在上一个例子中那样使用它,这样你就可以匹配有已知前缀的路径的请求。同时也可以在中间使用,例如/a/**/c。因此,/a/**/c不仅可以匹配/a/b/c,还可以匹配/a/b/d/c和a/b/c/d/e/c,等等。如果你只想匹配一个路径名,那么你可以使用单个*。例如,a/*/c将匹配a/b/c和a/d/c,但不匹配a/b/d/c。
**/

现在让我们来看看另外一个例子。 假设一个api想拒绝所有参数值不是数字的其他请求,如下所示:

代码清单8.10 在控制器类中定义一个带有路径变量的端点

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {
    
    
    @GetMapping("/product/{code}")
    public String productCode(@PathVariable String code){
    
    
        return code;
    }
}

下一个代码清单显示了如何配置授权,使得只有那些数值只包含数字的访问总是被允许的,而所有其他的访问都被拒绝。

代码清单8.11 配置授权,只允许特定的数字

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
//省略部分代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests()
                .mvcMatchers("/product/{code:^[0-9]*$}")
                .permitAll()
                .anyRequest().denyAll();
    }
}

注意 当使用参数表达式与一个正则表达式时,确保在参数名称、冒号(:)和正则表达式之间没有空格。

运行这个例子,当路径变量值只有数字时,应用程序才会接受调用。

我们讨论了很多,并包括了很多关于如何使用MVC匹配器来引用请求的例子。表8.1是对在本节中使用的MVC表达式的复习。

表8.1 用MVC匹配器进行路径匹配的常用表达式

/a 只有路径/a。
/a/* *操作符替换了一个路径名。在这种情况下,它匹配/a/b或/a/c,但不匹配/a/b/c。
/a/** **操作符替换了多个路径名。在这种情况下,/a以及/a/b和/a/b/c是这个表达式的匹配对象。
/a/{param} 这个表达式适用于带有给定路径参数的路径/a。
/a/{param:regex} 只有当参数的值与给定的正则表达式相匹配时,这个表达式才适用于带有给定路径参数的路径/a。

8.3 使用Ant matchers选择需要授权的请求

在这一节中,我们将讨论Ant matchers,用于匹配相应规则的权限。因为Spring从Ant那里借用了MVC表达式来匹配api的路径,所以可以使用Ant matchers的语法与8.2节看到的语法相同。

在开发中,建议使用MVC matchers而不是Ant matchers。使用Ant匹配器时的三种方法是:

  • antMatchers(HttpMethod method, String patterns)-允许同时指定限制所适用的HTTP方法和指向路径的Ant patterns。如果你想对同一组路径的不同HTTP方法应用不同的限制,这个方法很有用。
  • antMatchers(String patterns)–如果只需要根据路径来应用授权限制,这些策略可以自动应用于HTTP方法。
  • antMatchers(HttpMethod method),相当于antMatchers(httpMethod, “/**”)–可以使我们不考虑路径而引用一个特定的HTTP方法。

应用这些匹配器的方式与上一节中的MVC matchers类似。 同时,我们用于引用路径的语法也是一样的。那么有什么不同呢? MVC匹配器是指你的Spring应用程序如何理解将请求与控制器动作相匹配。但是如果在路径后面再加上一个/,那么任何通往同一个动作的路径都可以被Spring解释。在这种情况下,/hello和/hello/调用的是同一个方法。如果你使用MVC匹配器并为/hello路径配置安全,它会自动用同样的规则保护/hello/路径,但是如果使用Ant matchers 则会导致/hello需要认证,而/hello/不需要认证。如果不清楚这一点并使用Ant匹配器的开发者可能会在没有注意到的情况下留下一个未受保护的路径,这将为应用程序带来重大的安全漏洞。

让我们用一个例子来测试这个行为。

代码清单8.12 控制器类中/hello端点的定义

@RestController
public class HelloController {
    
    
    @GetMapping("/hello")
    	public String hello() {
    
    
    	return "Hello!";
    }
}

代码清单8.13描述了这个配置类。在这种情况下,我使用一个MVC matchers来定义/hello路径的授权配置,对这个api的任何请求都需要认证。我在例子中省略了UserDetailsService和PasswordEncoder的定义,因为这些与代码清单8.7中的定义相同。

代码清单8.13 使用MVC匹配器的配置类

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
   	//省略部分代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests().mvcMatchers("/hello").authenticated();
    }
}

如果启动该应用程序并进行测试,会发现/hello和/hello/路径都需要认证。这可能是你所期望的结果。接下来的代码片段显示了用cURL对这些路径进行的请求。调用未认证的/hello api看起来像这样。

curl http://localhost:8080/hello
{
    
    
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/hello"
}

curl http://localhost:8080/hello/
{
    
    
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/hello"
}

curl -u jane:12345 http://localhost:8080/hello
Hello!

curl -u jane:12345 http://localhost:8080/hello/
Hello!

所有这些返回值都是我们期望的。但让我们看看如果我们改变实现方式,使用Ant匹配器会发生什么。如果你只是改变配置类,对相同的表达式使用Ant matchers,结果就会改变。如前所述,该应用并没有为/hello/路径应用授权配置。在这种情况下,/hello并不作为Ant表达式应用于/hello/路径。如果你也想确保/hello/路径的安全,你必须单独添加它,或者写一个Ant表达式来匹配它。下面的列表显示了在配置类中使用Ant匹配器而不是MVC匹配器所做的改变。

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
   	//省略部分代码
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests().antMatchers("/hello").authenticated();
    }
}

测试结果如下

curl http://localhost:8080/hello
{
    
    
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/hello"
}

curl http://localhost:8080/hello/
Hello!

所以推荐使用MVC匹配器。使用MVC匹配器,你可以避免Spring将路径映射到动作的方式带来的一些风险。这是因为你知道在授权规则中解释路径的方式与Spring本身在将路径映射到端点时解释的方式相同。当你使用Ant匹配器时,要谨慎行事,确保你的表达式确实与你需要应用授权规则的所有内容相匹配。

沟通和知识共享的影响

我一直鼓励以所有可能的方式分享知识:书籍、文章、会议、视频等等。有时,即使是简短的讨论也能提出问题,从而推动巨大的改进和变化。我将通过几年前我讲的关于Spring课程的一个故事来说明我的意思。

这次培训是为一群为特定项目工作的中级开发人员设计的。它与Spring Security没有直接关系,但在某些时候,我们开始在培训中使用匹配器方法,作为培训的一部分,我们正在处理一个例子。

我开始用MVC matchers配置api授权规则,而没有先教学员MVC matchers的知识。我认为他们已经在他们的项目中使用了这些规则;我认为没有必要先解释它们。当我正在进行配置和讲授我正在做的事情时,一个与会者学院问了一个问题。我仍然记得那位女士羞涩的声音说:“你能介绍一下你所使用的这些MVC方法吗?我们正在用一些Ant-something方法来配置我们的api的安全”。

我当时意识到,学员可能没有意识到他们在使用什么。他们确实在使用Ant matchers,但并不了解这些配置,很可能是在机械地使用它们。复制和粘贴编程是一种危险的方法,不幸的是,这种方法经常被使用,特别是被初级开发人员使用。你永远不应该在不了解它的作用的情况下使用它。

在我们讨论这个新话题的时候,这位女士在他们的讨论中发现了Ant匹配器被错误地应用的情况。培训结束后,他们的团队安排了一个完整的流程来验证和纠正这种错误,这可能会导致他们的应用程序出现非常危险的漏洞。

8.4 使用regex matchers选择需要授权的请求

在本节中,我们讨论正则表达式(regex)。你应该已经知道什么是正则表达式,但你不需要成为这方面的专家。你可以从中更深入地了解这个主题。对于编写正则表达式,我也经常使用在线生成器,如https://regexr.com/(图8.1)。

在这里插入图片描述

图8.1 让你的猫在键盘上玩耍并不是生成正则表达式(regex)的最佳方案。要学习如何生成正则表达式,你可以使用一个在线生成器,如https://regexr.com/。

我们在第8.2节和第8.3节中了解到,在大多数情况下,你可以使用MVC和Ant结合来引用你应用授权配置的请求。然而,在某些情况下,你可能有更特殊的要求,而你无法用Ant和MVC表达式来解决这些问题。这种需求的一个例子是这样的。“当路径包含特定的符号或字符时,拒绝所有请求”。对于这些情况,你需要使用一个更强大的表达式,如正则表达式。

你可以用正则表达式来表示字符串的任何格式,所以它们在这个问题上提供了无限的可能性。

你可以用以下两种方法来实现regex匹配器:

  • regexMatchers(HttpMethod method, String regex)-指定适用限制的HTTP方法和参考路径的regexes。如果你想对同一组路径的不同HTTP方法应用不同的限制,这个方法很有用。
  • regexMatchers(String regex)–如果你只需要根据路径应用授权限制,则更简单,更容易使用。这些限制会自动适用于任何HTTP方法。

为了证明正则表达式是如何工作的,让我们通过一个例子来演示:建立一个向用户提供视频内容的应用程序。预设视频的应用程序通过api /video/{country}/{language}获得其内容。 在这个例子中,应用程序从用户提出请求的地方接收两个路径变量中的国家和语言。我们认为,如果请求来自美国、加拿大或英国,或者他们使用英语,任何经过授权的用户都可以看到视频内容。

我们需要保护的api有两个路径变量,如以下代码清单所示。这使得用Ant或MVC匹配器实现这一需求变得复杂。

package com.hashnode.proj0001firstspringsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Guowei Chi
 * @date 2023/1/20
 * @description:
 **/
@RestController
public class VideoController {
    
    
    @GetMapping("/video/{country}/{language}")
    public String video(@PathVariable String country,
                        @PathVariable String language) {
    
    
        return "Video allowed for " + country + " " + language;
    }
}

对于一个单一路径变量的条件,我们可以直接在Ant或MVC表达式中写一个regex。我们在第8.3节中提到了这样一个例子,但我当时没有深入讨论它,因为我们当时没有讨论正则表达式。

假设有一个api /email/{email}。我们想用一个匹配器来应用一个规则,只适用于那些将以.com结尾的地址作为参数值的电子邮件的请求。在这种情况下,你要写一个MVC匹配器,如下面的代码片段所示。

http.authorizeRequests()
	.mvcMatchers("/email/{email:.*(.+@.+\\.com)}")
	.permitAll()
	.anyRequest()
	.denyAll();

需求有时是复杂的,当你发现像下面这样的情况时,你会发现使用regex匹配器会更方便。

  • 对所有含有电话号码或电子邮件地址的路径进行具体配置
  • 对所有具有某种格式的路径进行具体配置,包括通过所有路径变量发送的内容

回到我们的regex匹配器的例子:当需要写一个更复杂的规则,最终引用更多的路径模式和多个路径变量值时,写一个regex匹配器会更容易。在代码清单8.16中,可以找到配置类的定义,它使用了一个regex匹配器来解决为/video/{country}/{language}路径给出的要求。我们还添加了两个具有不同权限的用户来测试该实现。

代码清单8.16 使用正则表达式的配置类

package com.hashnode.proj0001firstspringsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("read")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("read","premium")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.csrf().disable();
        http.httpBasic();
        http.authorizeRequests().regexMatchers(".*/(us|uk|ca)+/(en|fr).*")
                .authenticated().anyRequest().hasAuthority("premium");
    }
}

运行并测试端点,确认应用程序正确应用了授权配置。用户John可以调用国家代码US和语言en的api,但由于我们配置的限制,他不能调用国家代码FR和语言fr的api。

curl -u john:12345 http://localhost:8080/video/us/en
Video allowed for us en

curl -u john:12345 http://localhost:8080/video/fr/fr
{
    
    
"status":403,
"error":"Forbidden",
"message":"Forbidden",
"path":"/video/fr/fr"
}

curl -u jane:12345 http://localhost:8080/video/us/en
Video allowed for us en

curl -u jane:12345 http://localhost:8080/video/fr/fr
Video allowed for fr fr

正则表达式是强大的工具。可以用它们来引用任何特定要求的路径。但是,由于正则表达式很难读,而且可能变得相当长,它们应该是你最后的选择。只有在MVC和Ant表达式不能为你提供问题的解决方案时,才使用这些工具。

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128740413
今日推荐