授权服务器

Introspection Endpoint:校验 Access Token 的合法性

image-20221102010635416

Spring Security OAuth2 架构

Authorization flow

Authorization Code Flow

image-20221102000540824

Maven

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>

API

1
2
3
4
5
6
7
@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserInfo {
String name;
String email;
}
1
2
3
4
5
6
7
8
9
@GetMapping("/api/user")
public ResponseEntity<UserInfo> getUser() {
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ResponseEntity.ok(
UserInfo.builder()
.name(user.getUsername())
.email(user.getUsername() + "@gmail.com")
.build());
}

Authorization Server

Authorization Server + Client App
authorization_code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("wechat")
.secret("654321")
.redirectUris("http://localhost:9000/callback")
.authorizedGrantTypes("authorization_code") // 授权码模式
.scopes("read_userinfo", "read_contacts");
}
}

Resource Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/api/**");
}
}

Resource owner

1
2
3
4
5
6
server:
port: 8000
security:
user:
name: zhongmingmao
password: 123456

Flow

未授权

1
2
3
4
5
$ curl -s --location --request GET 'http://localhost:8000/api/user' | jq
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

获取授权码,response_type=code
浏览器:http://localhost:8000/oauth/authorize?client_id=wechat&redirect_uri=http://localhost:9000/callback&response_type=code&scope=read_userinfo

image-20221103001315526

image-20221103001416403

image-20221103001636098

1
2
$ echo -n 'zhongmingmao:123456' | base64
emhvbmdtaW5nbWFvOjEyMzQ1Ng==
1
2
3
4
5
6
$ curl --location --request POST 'http://localhost:8000/oauth/authorize' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic emhvbmdtaW5nbWFvOjEyMzQ1Ng==' \
--data-urlencode 'user_oauth_approval=true' \
--data-urlencode 'scope.read_userinfo=true' \
--data-urlencode 'authorize=Authorize'

兑换授权码

1
2
$ echo -n 'wechat:654321' | base64
d2VjaGF0OjY1NDMyMQ==
1
2
3
4
5
6
7
8
9
10
11
12
13
$ curl -s --location --request POST 'http://localhost:8000/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic d2VjaGF0OjY1NDMyMQ==' \
--data-urlencode 'code=6qhVz7' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=http://localhost:9000/callback' \
--data-urlencode 'scope=read_userinfo' | jq
{
"access_token": "1aac6db3-c304-45c1-ab9d-9c7d63da842d",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read_userinfo"
}

访问资源

1
2
3
4
5
6
$ curl -s --location --request GET 'http://localhost:8000/api/user' \
--header 'authorization: Bearer 1aac6db3-c304-45c1-ab9d-9c7d63da842d' | jq
{
"name": "zhongmingmao",
"email": "[email protected]"
}

Implicit Grant Flow

相对于授权码模式,减少了授权码兑换的过程,直接获得 Access Token,非常不安全

image-20221102001550406

代码结构与授权码类似

Authorization Server

implicit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("wechat")
.secret("654321")
.redirectUris("http://localhost:9000/callback")
.authorizedGrantTypes("implicit")
.accessTokenValiditySeconds(1 << 8)
.scopes("read_userinfo", "read_contacts");
}
}

Flow

直接获取 Access Token:response_type=token
浏览器:http://localhost:8000/oauth/authorize?client_id=wechat&redirect_uri=http://localhost:9000/callback&response_type=token&scope=read_userinfo

image-20221103011001348

访问资源

1
2
3
4
5
6
$ curl -s --location --request GET 'http://localhost:8000/api/user' \
--header 'authorization: Bearer a5cf3dc2-d3e6-4915-aee3-155e95d9764e' | jq
{
"name": "zhongmingmao",
"email": "[email protected]"
}

Resource Owner Password Credentials Flow

Authorization Server

password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

// 认证
AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("wechat")
.secret("654321")
.authorizedGrantTypes("password")
.scopes("read_userinfo", "read_contacts");
}
}

Flow

1
2
$ echo -n 'wechat:654321' | base64
d2VjaGF0OjY1NDMyMQ==

客户应用直接使用用户账密去授权服务器申请 Access Token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ curl -s --location --request POST 'http://localhost:8000/oauth/token' \
--header 'accept: application/json' \
--header 'content-type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic d2VjaGF0OjY1NDMyMQ==' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=zhongmingmao' \
--data-urlencode 'password=123456' \
--data-urlencode 'scope=read_userinfo' | jq
{
"access_token": "e2849e93-125d-444a-862e-acc2c3a0bad9",
"token_type": "bearer",
"expires_in": 43145,
"scope": "read_userinfo"
}
1
2
3
4
5
6
$ curl -s --location --request GET 'http://localhost:8000/api/user' \
--header 'authorization: Bearer e2849e93-125d-444a-862e-acc2c3a0bad9' | jq
{
"name": "zhongmingmao",
"email": "[email protected]"
}

Client Credentials Flow

API

1
2
3
4
5
6
7
@GetMapping("/api/users")
public ResponseEntity<List<UserInfo>> getUsers() {
List<UserInfo> users = new ArrayList<>();
users.add(UserInfo.builder().name("zhongmingmao").email("[email protected]").build());
users.add(UserInfo.builder().name("zhongmingwu").email("[email protected]").build());
return ResponseEntity.ok(users);
}

Authorization Server

client_credentials

1
2
3
4
5
6
clients
.inMemory()
.withClient("wechat")
.secret("654321")
.authorizedGrantTypes("client_credentials")
.scopes("read_userinfo", "read_contacts");

Flow

1
2
$ echo -n 'wechat:654321' | base64
d2VjaGF0OjY1NDMyMQ==
1
2
3
4
5
6
7
8
9
10
11
$ curl -s --location --request POST 'http://localhost:8000/oauth/token' \
--header 'Authorization: Basic d2VjaGF0OjY1NDMyMQ==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=read_userinfo' | jq
{
"access_token": "5954cfbc-5b1b-4699-9478-8233774c05c6",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read_userinfo"
}
1
2
3
4
5
6
7
8
9
10
11
12
$ curl -s --location --request GET 'http://localhost:8000/api/users' \
--header 'authorization: Bearer 5954cfbc-5b1b-4699-9478-8233774c05c6' | jq
[
{
"name": "zhongmingmao",
"email": "[email protected]"
},
{
"name": "zhongmingwu",
"email": "[email protected]"
}
]

技术选型

参考

  1. 微服务架构实战 160 讲