上一篇:在 Spring 中实现 OAuth2(第 1 部分)中,我们已了解 OAuth2 的基本概念,以及如何在 Spring 中配置多种授权许可类型。本篇聚焦 OAuth2 的另一个核心概念:scope(范围)

为何需要 scope

保护应用访问通常分两步:认证(authentication)授权(authorization)。可以类比:你拿到一张能刷开大楼闸机的工牌——这相当于 OAuth 令牌;但能否进入某一楼层,还取决于权限策略

OAuth 里的 scope 就是用来描述「这张令牌允许操作哪些资源不允许哪些」;它提供比「有令牌 / 没令牌」更细的一层访问控制,作用上接近传统系统里的角色,但粒度由资源所有者与授权服务器约定。

实现

示例仍基于第 1 部分的仓库:zak905/oauth2-example

资源服务器中的 ResourceController 如下(为演示 HTTP 动词与 scope 的映射,部分路由改为 POST/DELETE):

@RestController("/")
public class ResourceController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/foo")
public String foo(){
return "foo";
}
@PostMapping("/bar")
public String bar(){
return "bar";
}
@DeleteMapping("/test")
public String test(){
return "test";
}
}

先在 授权服务器配置里声明客户端可用的 scopes(与第 1 部分一致):

clients.inMemory().withClient("my-trusted-client")
.authorizedGrantTypes("password",
"refresh_token", "implicit", "client_credentials", "authorization_code")
.authorities("CLIENT")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(60)
.redirectUris("http://localhost:8081/test.html")
.resourceIds("resource")
.secret("mysecret");

在资源服务器侧校验 scope,有两种常见做法:URL 级安全配置,或 方法级安全注解

方式一:在 HttpSecurity 中写 SpEL

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET,"/hello").access("#oauth2.hasScope("read")")
.antMatchers(HttpMethod.GET,"/foo").access("#oauth2.hasScope("read")")
.antMatchers(HttpMethod.POST,"/bar").access("#oauth2.hasScope("write")")
.antMatchers(HttpMethod.DELETE,"/test").access("#oauth2.hasScope("trust")")
.anyRequest().authenticated().
and().csrf().disable();
}

方式二:@PreAuthorize 等方法级注解

@PreAuthorize("#oauth2.hasScope("read")")
@GetMapping("/hello")
public String hello(){
return "hello";
}
@PreAuthorize("#oauth2.hasScope("read")")
@GetMapping("/foo")
public String foo(){
return "foo";
}
@PreAuthorize("#oauth2.hasScope("write")")
@PostMapping("/bar")
public String bar(){
return "bar";
}
@PreAuthorize("#oauth2.hasScope("trust")")
@DeleteMapping("/test")
public String test(){
return "test";
}

若使用第二种方式,需在某一配置类上启用 @EnableGlobalMethodSecurity(prePostEnabled = true)(示例见 ResourceSecurityConfiguration)。prePostEnabled = true 会启用 @PreAuthorize / @PostAuthorize 等前置/后置方法安全。

#oauth2.hasScope("trust") 一类表达式基于 Spring 表达式语言(SpEL),详见 Spring Framework 文档

按 scope 申请令牌

若请求令牌时未显式指定 scope,Spring 默认认为令牌拥有客户端配置中的全部 scopes。先只申请 read

Terminal window
curl -X POST --user my-trusted-client:mysecret localhost:8081/oauth/token -d "grant_type=client_credentials&client_id=my-trusted-client&scope=read" -H "Accept: application/json"
{
"access_token": "acadbb31-f126-411d-ae5b-6a278cee2ed6",
"token_type": "bearer",
"expires_in": 60,
"scope": "read"
}

用该令牌访问需要 read scope 的端点应成功:

Terminal window
curl -XGET localhost:8989/hello -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
hello
curl -XGET localhost:8989/foo -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
foo

若拿 read 令牌去调用需要 write 的接口,会被拒绝:

Terminal window
curl -XPOST localhost:8989/bar -H "Authorization: Bearer acadbb31-f126-411d-ae5b-6a278cee2ed6"
{
"error": "access_denied",
"error_description": "Access is denied"
}

换发只含 write 的令牌再试:

Terminal window
curl -X POST --user my-trusted-client:mysecret localhost:8081/oauth/token -d "grant_type=client_credentials&client_id=my-trusted-client&scope=write" -H "Accept: application/json"
{
"access_token": "bf0fa83a-23bd-4633-ac6c-a06f40d53e5f",
"token_type": "bearer",
"expires_in": 3599,
"scope": "write"
}
Terminal window
curl -XPOST localhost:8989/bar -H "Authorization: Bearer bf0fa83a-23bd-4633-ac6c-a06f40d53e5f"
bar

小结

scope 很重要:访问令牌本身往往不携带终端用户画像的完整信息,scope 负责在资源侧做最小权限约束。后续还可把 Google、Facebook 等外部授权服务器接入同一流程(作者原计划的下文主题)。

本文为学习目的的个人翻译,译文仅供参考。

原文链接:Using OAuth2 in Spring, Scopes, Part 2

版权归原作者或原刊登方所有。本文为非官方译本;如有不妥,请联系删除。