当你使用浏览器访问网站, 或是使用手机 App , 一般都要求你注册一个帐号, 然后使用这个帐号登录先. 最传统的方式让用户注册自己的帐号, 很多时间比较懒的用户对于一个新 app 的试用就会在这里截止了. 现在越来越多的应用并不要求用户注册一个帐号, 而是允许用户使用微博或微信帐号来登录.
1. oAuth2 协议简介
1. OAuth2 的核心概念
• 资源所有者(Resource Owner) :通常是最终用户,拥有并可以授予对其资源的访问权限。
• 客户端(Client) :代表资源所有者的应用程序,请求访问资源。
• 资源服务器(Resource Server) :存储资源并负责响应请求。一般使用 Access Token 验证请求的合法性。
• 授权服务器(Authorization Server) :负责验证资源所有者的身份,并颁发 Token(授权码或 Access Token)。
2. 常用的 OAuth2 授权模式
• 授权码模式(Authorization Code) :安全性高,推荐在 Web 应用中使用。客户端通过用户授权得到授权码,再用授权码获取 Access Token。
• 简化模式(Implicit) :通常用于单页应用(SPA),直接向客户端颁发 Access Token。
• 密码模式(Resource Owner Password Credentials) :用户直接提供用户名和密码给客户端,适用于高度信任的客户端。
• 客户端模式(Client Credentials) :客户端以自身身份请求授权,通常用于服务间通信。
2. OAuth2 协议示例
举个例子, 我曾经当过秘书, 秘书如果需要调阅机密档案, 一般要经过如下步骤
- 秘书找保密室领导申请调阅某机密文档
- 领导签字批准
- 秘书拿着领导签字去找档案员拿钥匙
- 档案员给了一把档案柜密码锁的临时密码 (临时密码是有时效性的, 过期无效)
- 秘书拿着临时密码去开文件柜拿文档
- 秘书拿到文档查阅所需内容
这个过程就是一个典型的 OAuth2 过程, OAuth 协议定义的四种角色如下
- Client 是秘书
- Resource Owner 是领导
- Authorization Server 是 档案员
- Resource Server 是档案柜, resource 就是档案
其中
- Authorization Grant 是资源所有者(领导)给予的授权, 也就是 Resource Owner 所给予的凭据, OAuth协议中定义了四种方式
- authorization code 授权码, 就好比领导给的签字
- implicit 隐含方式
- resource owner password 领导给你的一个临时密码
- credentials, and client credentials
- Access Token 是档案柜密码锁的临时密码
- Protected Resource 是机密档案
标准流程如下:
-
Client 从 resource owner 要求授权许可, 这个请求可以直接找 resource owner, 或者通过 authorization server 间接找 resource owner
-
Client 收到一个授权许可, 也就是一个 credential,它代表 resource owner 的授权
-
Client 向 authorization server 认证并出示这个授权许可,请求一个 Access token
-
authorization server 认证 client 并校验这个授权许可,如果有效,则发布一个 access token
-
Client 通过出示 access token 来向 resource server 请求一个受保护的资源
-
Resource server 校验这个 Access token, 如果有效,则处理这个请求
API 示例
获取访问令牌 access_token
- 请求
POST /v1/oauth2/token HTTP/1.1
Authorization: Basic xxx
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=walter
&password=pass
&scope=api
- 响应
HTTP1.1 200 OK Content-Type: application/json
{
"token_type": "bearer",
"expires_in": "3000000",
"access_token": "xxxyyy",
"refresh_token": "xxxzzz"
}
通过访问令牌来调用 API
- 请求
GET /api/v1/users/4c65201a-2052-4cab-8b22-4c60148a1178 HTTP/1.1
Authorization: Bearer xxxyyy
- 响应
HTTP1.1 200 OK Content-Type: application/json
{
"id": "4c65201a-2052-4cab-8b22-4c60148a1178",
"name": "walter"
}
如果 Access_token 过期了则返回
HTTP1.1 401 Unauthorized Content-Type: application/json
{
"error": "invalid_token"
}
@Configuration
@EnableWebSecurity
public class BasicConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("password")
.roles("USER")
.and()
.withUser("admin")
.password("admin")
.roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
3. Spring 中 oAuth2 的实现
在 Spring 中实现 OAuth2 可以使用 Spring Security 提供的 OAuth2 支持,用于安全地限制应用访问用户资源,而不会将用户的凭证暴露给第三方应用。
Spring Security 通过支持各种 OAuth2 授权流程(例如 Authorization Code、Implicit、Client Credentials 和 Resource Owner Password Credentials)来实现这一点。以下是实现 OAuth2 的主要步骤和概念:
3.1 引入依赖****
在使用 Spring Boot 和 Spring Security 时,可以通过添加依赖来实现 OAuth2 授权服务器和资源服务器功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
3.2 配置授权服务器****
Spring 提供了 AuthorizationServerConfigurerAdapter 类来配置授权服务器。
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_id")
.secret("{noop}client_secret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password", "client_credentials")
.scopes("read", "write")
.redirectUris("http://localhost:8080/login/oauth2/code/my-client");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore());
}
}
在这个配置中:
- withClient 定义客户端 ID 和密码。
- authorizedGrantTypes 指定客户端授权类型,如授权码、密码模式等。
- scopes 定义客户端的权限范围。
- redirectUris 是授权码模式下的回调地址。
3.3 配置资源服务器****
在资源服务器上,配置 ResourceServerConfigurerAdapter 来保护 API 资源,并指定哪些 URL 需要访问令牌才能访问。
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/api/**").authenticated();
}
}
在这里:
- /public/**:开放访问的 URL,不需要认证。
- /api/**:受保护的 API,需要 Access Token 认证。
4. 配置客户端****
客户端应用需要请求授权码,然后用授权码换取 Access Token。在 Spring Security 中,可以使用 OAuth2AuthorizedClientService 来实现。
4.1 配置客户端注册****
在 application.yml 中配置客户端:
spring:
security:
oauth2:
client:
registration:
my-client:
client-id: client_id
client-secret: client_secret
scope: read,write
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/my-client"
client-name: My Client
provider:
my-provider:
authorization-uri: http://localhost:8080/oauth/authorize
token-uri: http://localhost:8080/oauth/token
4.2 使用 OAuth2RestTemplate 请求资源****
配置完毕后,客户端可以使用 OAuth2RestTemplate 请求资源。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApiController {
@Autowired
private OAuth2RestTemplate oAuth2RestTemplate;
@GetMapping("/fetchData")
public String fetchData() {
String resourceUrl = "http://localhost:8081/api/resource";
return oAuth2RestTemplate.getForObject(resourceUrl, String.class);
}
}
5. 使用 Access Token 访问受保护资源****
当客户端获得 Access Token 后,它可以在 HTTP 请求的 Authorization 头部携带该 Token 访问受保护资源。典型的请求头如下:
GET /api/resource HTTP/1.1
Host: localhost:8081
Authorization: Bearer ACCESS_TOKEN
6. OAuth2 流程的整体总结****
-
授权请求:客户端请求授权服务器获取授权码或 Access Token。
-
Access Token 生成:授权服务器验证请求并生成 Access Token。
-
资源访问:客户端使用 Access Token 请求资源服务器的数据。
-
Token 验证:资源服务器验证 Token 的合法性,允许合法的请求访问资源。
7. 示例工作流程****
假设使用授权码模式进行 OAuth2 授权:
-
客户端请求授权:用户在客户端登录,客户端重定向到授权服务器的授权 URL。
-
用户授权并返回授权码:用户在授权服务器上同意授权,授权服务器重定向回客户端并附带授权码。
-
客户端用授权码获取 Access Token:客户端用授权码向授权服务器请求 Access Token。
-
客户端用 Access Token 请求资源:客户端在请求中附带 Access Token 访问资源服务器的受保护资源。
参考资料
- tools.ietf.org/html/rfc674…: The OAuth 2.0 Authorization Framework
- tools.ietf.org/html/rfc675…: The OAuth 2.0 Authorization Framework: Bearer Token Usage
- www.baeldung.com/spring-boot…
- JSON Web Token (JWT): tools.ietf.org/html/draft-…
- JSON Web Signature (JWS) : tools.ietf.org/html/draft-…
- JSON Web Encryption (JWE): tools.ietf.org/html/draft-…
- JSON Web Algorithms (JWA): tools.ietf.org/html/draft-…
- JSON Web Key (JWK): tools.ietf.org/html/draft-…