一、前言
之前讲解了如何基于权限和角色配置访问。但是其中只应用了针对所有端点的配置。本章将介绍如何对特定的请求分组应用授权约束。在生产环境的应用程序中,不太可能对所有请求应用相同的规则。其中将具有只有某些特定用户才能调用的端点,而其他端点则可能每个用户都可以访问。根据业务需求,每个接口都有自己的自定义授权配置。
要选择应用授权配置的请求,可以使用匹配器方法。Spring Security提供了3种类型的匹配方法。
- MVC匹配器:将MVC表达式用于路径以便选择端点。
- Ant匹配器:将Ant表达式用于路径以便选择端点。
- regex匹配器:将正则表达式(regex)用于路径以便选择端点。
二、使用MVC匹配器方法选择端点
2.1、对单个请求无请求方法的匹配
首先看一个简单的示例,我们要创建一个暴露两个端点的应用程序,这两个端点是/hello和/xiao。我们希望确保只有ADMIN角色的用户才能调用/hello端点。类似地,只有USER角色的用户才能调用/xiao端点。
package com.mbw.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello world";
}
@GetMapping("/xiao")
public String xiao(){
return "纳西妲我抽爆!";}
}
然后我们就着上次搭建好的程序以及数据继续我们的学习,为了让指定接口具有特定的角色才能调用,我们需要使用mvcMatchers()方法,下面代码就是配置类中使用的示例:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
.mvcMatchers("/hello").hasRole("ADMIN")
.mvcMatchers("/xiao").hasRole("USER");
}
我们接下来通过张飞这个用户认证通过后访问/xiao这个端点,发现可以访问通过,因为张飞角色只有USER。
但是张飞并没有ADMIN这个角色,我们访问/hello端点,会发现报403,说明配置此时生效。
但是我们拿关羽访问该端点,关羽是三个角色都有,所以可以访问该端点
然后我们现在仍然是刚才的配置,我们新加上一个端点:
@GetMapping("/giao")
public String giao(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
return "giao,"+name;
}
这时你不去登陆直接访问该接口,你会发现你能请求的通:
ps:anonymousUser是因为未经任何认证,而接口又被放行,所以安全上下文没有任何认证用户信息,所以显示anonymousUser匿名用户。
因为我们的配置中只有
那么对于其他的请求,如果没有额外配置,默认情况下任何人任何权限都可以访问它,等同于permitAll().在这种情况下,Spring Security不会进行身份验证,不过你如果硬要进行身份验证也是可以的,框架也会对其进行评估,例如你输入正确的凭据,框架会显示正确的回复,但是凭据不正确,框架也会返回对应的错误,这个我们通过basic登录展示:
可以看到仍然会显示张飞的名字,说明框架会理会该请求并进行评估,但若是没有提供身份凭据,你也可以访问没做限制的请求而已。当然若你提供错误的身份凭据,框架理所当然会返回401:
那么回到MVC匹配器上,我们之前调用的是mvcMatchers(String… patterns)这个方法,这意味着我们不仅可以像刚才一样对单一请求匹配某个授权规则,我们还可以指定多个端点同时使用相同的授权规则。
2.2、对单个请求有请求方法的匹配
当然,有时我们还需要指定HTTP方法,而不仅仅是路径,这就要用到MVC匹配器的另一种方法:
mvcMatchers(HttpMethod method,String… patterns),它允许制定要应用限制的请求方法和路径,这一点对同一路径不同请求方法的端点会非常好用,例如我们现在增加两个路径相同但是所需请求方法不同的接口:
@PostMapping("/a")
public String postEndpointA(){
return "a";
}
@GetMapping("/a")
public String getEndpointA(){
return "a";
}
然后假设现在需要让post方法的/a请求需要经过认证,而get的不用。我们可以这样配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
.mvcMatchers(HttpMethod.POST,"/a").authenticated()
.mvcMatchers(HttpMethod.GET,"/a").permitAll();
}
我们现在来到Postman通过get方法调用/a并且使用No Auth的方式请求,发现无需认证也可以请求
但若是改为post方法的/a,如果还是no Auth,则报401:
现在认证通过后再访问,发现可以访问:
2.3、对多个路径的匹配
有时我们的controller会在类上加入@RequestMapping给该类的所有接口加上统一前缀路径,此时假设我们对这个类的所有接口需要假设只有ADMIN角色访问,我们总不能真的把一个个路径用逗号隔开写上去,太麻烦了。 Spring MVC从Ant中借用了路径匹配语法,这使得我们可以使用**操作符去匹配以某一路径开始的所有路径请求。
我们新建一个controller
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/xiao")
public String xiao(){
return "纳西妲我抽爆!";}
@GetMapping("/giao")
public String giao(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
return "giao,"+name;
}
@GetMapping("/a")
public String getEndpointA(){
return "a";
}
@GetMapping("/a/b")
public String getEndpointAB(){
return "ab";
}
}
我们现在假设对该controller下所有的接口要求只允许有ADMIN角色的才能访问,我们可以使用如下匹配方法:
.mvcMatchers("/test/xiao","/test/giao","/test/a","/test/a/b").hasRole("ADMIN")
但是这样太过于累赘,万一接口很多呢?我们可以通过MVC路径匹配表达式中的**解决
.mvcMatchers("/test/**").hasRole("ADMIN")
你会发现带有/test路径的所有接口必须要有ADMIN权限才能访问,若没有则报403
而拿关羽则可以通过:
如前面的示例所示,操作符可以指向任意数量的路径名。可以像上一个实例中所做的那样使用它,以便可以用具有已知前缀的路径来匹配请求。还可以在路径中间使用它指向任意数量的路径名,或者指向以特定模式(比如/a/**/c)结束的路径。因此,/a/**/c不仅可以匹配/a/b/c,还可以匹配/a/b/c/d/b/c和/a/b/c/d/c等等,如果你想匹配一个路径名,那么可以使用单个*。例如,a/*/c将匹配/a/b/c和/a/d/c等等。而不是/a/b/d/c。
2.4、当端点路径中带有路径变量的匹配
我们在get请求通常会使用路径变量,所以实际上为这类请求应用授权规则会非常有用。甚至可以应用指向路径变量值的规则。并且当实际运用的时候,搭配之前讲过的denyAll()会使用途和扩展性非常高。
例如下面这个带有路径变量的端点
@GetMapping("/product/{code}")
public String productCode(@PathVariable String code){
return code;
}
假设我们只让该请求路径变量仅包含数字时候才接受调用,其他所有请求均不能通过请求。
当使用带有正则表达式的参数表达式时,请确保在参数名称、冒号(:)和正则表达式之间没有空格,如下代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
.mvcMatchers("/test/product/{code:^[0-9]*$}").permitAll()
.anyRequest().denyAll()
}
此时调用端点,假设code=1234a,不符合全部都是数字,报401:
然后再次调用端点,code=12345,发现调用通过:
关于使用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 |
三、使用Ant匹配器选择用于授权的请求
使用Ant匹配器的3种方法如下:
- antMatchers(HttpMethod method,String patterns):允许指定应用限制的HTTP方法和指向路径的Ant模式。如果希望对同一组路径的不同HTTP方法应用不同的限制,则此方法非常有用。
- antMatchers(String patterns):如果只需要应用基于路径的授权限制,则这一方法使用起来更加简单。这些限制会自动适用于任何HTTP方法。
- antMatchers(HttpMethod method):他等同于antMatchers(httpMethod,“/**”),允许特定的HTTP方法,而不考虑路径。
MVC匹配器与Ant匹配器哪个好用?
MVC匹配器指的是Spring应用程序如何理解将请求与控制器相匹配。有时多个路径可以被Spring解析为匹配相同的操作。
例如:如果在路径之后添加一个/,那么指向相同操作的任何路径(例如/hello)都可以由Spring解析。在这种情况下,/hello和/hello/会调用相同的方法。如果使用MVC匹配器并且为/hello路径配置安全性,则它会自动使用相同的规则保护/hello/路径/。这会产生巨大的影响!开发人员如果不知道这一点,并且使用Ant匹配器,则可能会在毫不知情的情况下让路径不受保护。
下面以一个示例说明。
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello world";
}
}
3.1、使用MVC匹配器的配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
.mvcMatchers( "/hello").authenticated();
//.antMatchers( "/hello").authenticated();
}
使用http://localhost:9090/hello并且不认证测试:
使用http://localhost:9090/hello/并且不认证测试:发现仍然401,这说明之前的结论是正确的,由于/hello和/hello/指向相同的操作,均可以由Spring解析,所以MVC匹配器同样也会保护/hello/这个路径
3.2、使用Ant匹配器的配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
//.mvcMatchers( "/hello").authenticated();
.antMatchers( "/hello").authenticated();
}
不进行身份验证访问http://localhost:9090/hello
不进行身份验证访问http://localhost:8080/hello/
注意,此时居然访问成功了,所以Ant匹配器的粒度比较细,会出现我们不期待的结果。还需要为该路径再配置限制,所以平时使用MVC匹配器就可以了。
实际上,Ant匹配器会为模式精确地应用给定的Ant表达式,但它无法触及Spring MVC的精细功能。在本示例中,/hello不会作为Ant表达式应用于/hello路径。如果还想保护/hello/路径,则必须单独添加它,或者编写一个匹配它的Ant表达式。
四、使用正则表达式匹配器选择用于授权的请求
可以使用正则表达式表示字符串的任何格式,因此它们提供了无限的可能性。但缺点是难以阅读,即使应用于简单的场景也是如此。
实现正则表达式匹配器的两种方法如下:
- regexMatchers(HttpMethod method,String regex):同时指定应用限制的HTTP方法和指向路径的正则表达式。如果希望对同一组路径的不同HTTP方法应用不同的限制,则此方法非常有用。
- regexMatchers(String regex):如果只需要应用基于路径的授权限制,该方法使用起来会更加简单。这些限制将会自动适用于任何HTTP方法。
以一个简单的示例说明:
@RestController
public class VideoController {
@GetMapping("/video/{country}/{language}")
public String video(@PathVariable String country,
@PathVariable String language) {
return "Video allowed for " + country + " " + language;
}
}
当需要编写更复杂的规则,最终指向更多路径模式和多个路径变量值时,编写一个正则表达式匹配器的方式会更加容易。例如:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.successHandler(successHandler)
.failureHandler(failureHandler)
.and().httpBasic().and()
.authorizeRequests()
.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
.regexMatchers(".*/(us|uk|ca)+/(en|fr).*").authenticated()
//配置用户需要具有ADMIN角色才能访问的其他路径
.anyRequest().hasRole("ADMIN");
}
该配置限制了国家只能要求us/uk/ca,语言只能是en/fr,所以此时通过US国家和en语言可以调用,但若是FR国家和fr语言则不能调用,我们拿张飞进行测试:
由于张飞不具有ADMIN用户,且/video/FR/fr并不被正则匹配器所匹配,所以被拦截并报403.
正则表达式是功能强大的工具,可以使用它们指向任何指定需求的路径。但是由于正则表达式难以阅读,并且可能变得很长,因此它们是我们的最后选择。只有当MVC和Ant表达式不能为所面临的问题提供解决方案时,才使用它们。