OAuth 2.0 规范总结
Criado em: 18 de janeiro de 2025
Criado em: 18 de janeiro de 2025
oauth2.0有哪些规范性的东西呢?
在 IETF 制定的 OAuth 2.0 生态中,最核心、最具规范性的文档都以 RFC(Request for Comments)形式发布。除了最基本的 OAuth 2.0 核心协议(即 RFC 6749、RFC 6750),围绕它还有众多扩展及补充规范,用于支持不同场景、功能和安全要求。下面列出主要的规范性文档(大多都可以在 IETF 的官方网站 或 tools.ietf.org 上查阅):
RFC 6749: The OAuth 2.0 Authorization Framework
RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage
RFC 7009: OAuth 2.0 Token Revocation
RFC 7636: Proof Key for Code Exchange by OAuth Public Clients (PKCE)
RFC 7662: OAuth 2.0 Token Introspection
RFC 7008 (草案阶段) 等其他安全增强
RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol
RFC 7592: OAuth 2.0 Dynamic Client Registration Management Protocol
RFC 8628: OAuth 2.0 Device Authorization Grant (Device Flow)
RFC 8252: OAuth 2.0 for Native Apps (BCP 212)
RFC 8707: Resource Indicators for OAuth 2.0
RFC 7519: JSON Web Token (JWT)
RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
IETF OAuth 2.0 相关 Working Group
除了正式发布的 RFC 外,OAuth 工作组还有一些草案文档(Draft),通常涵盖更前沿的应用场景和安全最佳实践。可以在 IETF Datatracker 中关注相关进展。
各大云服务或社交平台的实现
如 Google、微软、GitHub、微信企业号等在各自开发者文档中,都有遵循(或部分遵循)OAuth 2.0 规范的实现说明。有时还会结合一些私有扩展。
开发者社区与官方示例
OAuth 2.0 并不是只由单一文档定义的,而是由一系列 RFC 文档所构成的一个“框架+扩展”生态系统。其中最核心的规范是 RFC 6749(核心框架)和 RFC 6750(Bearer Token 使用)。围绕这两个核心,又有许多扩展规范,为不同的业务场景(如设备模式、动态注册、令牌格式、身份层扩展等)提供了可选的规范性指导。
如果你想系统了解 OAuth 2.0 的整体规范,可以从如下路径入手:
希望对你了解和使用 OAuth 2.0 的“规范性”部分有所帮助。
很好,我是个开发人员,在开发oauth2.0 server的时候,在每一步,需要透出哪些字段,他们的格式/单位一般是什么,
申请code
获取token
更新refresh-token
获取用户信息
下面的说明主要基于 OAuth 2.0 核心规范(RFC 6749、RFC 6750) 和常见的实践(包括 PKCE、Refresh Token 等)进行整理,给出在开发 OAuth 2.0 授权服务器时“各步骤常见字段”和“它们的典型格式、单位或约定”。不同厂商或框架可能会在此基础上增加一些私有扩展字段,这里仅列出标准和通用做法,供参考。
通常对应浏览器重定向到“授权端点(Authorization Endpoint)”的 GET 请求:
请求参数(客户端 -> 授权服务器)
code
。read write profile
。S256
或 plain
(推荐 S256
)。示例:
textGET /authorize? response_type=code &client_id=abc123 &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb &scope=read%20profile &state=xyz987 &code_challenge=XXXXXXXX (若使用 PKCE) &code_challenge_method=S256 (若使用 PKCE)
响应参数(授权服务器 -> 客户端浏览器重定向)
示例:
textHTTP/1.1 302 Found Location: https://client.example.org/cb?code=abcd1234&state=xyz987
授权码的有效期:通常只有几十秒到几分钟,并且只能使用一次。
对应“令牌端点(Token Endpoint)”的 POST 请求,客户端会使用在上一步拿到的 code
来换取 access_token
(以及可选的 refresh_token
)。
请求参数(客户端 -> 授权服务器)
authorization_code
。通常这些参数会以 application/x-www-form-urlencoded
方式传递在请求体中,或在 HTTP Basic Auth 头里携带 client_id、client_secret。
示例(Body 里带参数):
textPOST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=abcd1234 &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb &client_id=abc123 &client_secret=ssh-secret &code_verifier=xxxxxx (若使用 PKCE)
响应参数(授权服务器 -> 客户端)
一般返回 JSON,常见字段:
Bearer
。3600
。示例:
json{ "access_token": "SlAV32hkKG", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "8xLOxBtZp8", "scope": "read profile" }
在令牌端点(Token Endpoint)再次发起请求,用 refresh_token
换取新的 access_token
(以及可能新的 refresh_token
)。
请求参数(客户端 -> 授权服务器)
refresh_token
。client_secret
(机密客户端)。示例:
textPOST /token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &refresh_token=8xLOxBtZp8 &client_id=abc123 &client_secret=ssh-secret
响应参数(授权服务器 -> 客户端)
通常与“获取 Token”阶段的响应字段类似:
Bearer
。示例:
json{ "access_token": "hGFbs732sdf...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "zxcvb0987", "scope": "read profile" }
获取用户信息这一环节在 纯 OAuth 2.0 里并没有强制标准化的“用户信息端点”,但在 OpenID Connect(OIDC)里有明确的 userinfo_endpoint
。
如果只做 OAuth 2.0,你可以自定义一个资源端点让客户端拿着 access_token
去获取用户资料。在 OIDC 的场景下,用户信息是标准化的,常见字段包括 sub
(用户唯一标识)、name
、email
等。
请求方式
https://server.example.com/userinfo
的端点。Authorization: Bearer <access_token>
。示例:
textGET /userinfo Host: server.example.com Authorization: Bearer SlAV32hkKG
响应内容
sub
、preferred_username
、email
、name
等常见 Claims。示例(OIDC 格式):
json{ "sub": "248289761001", "name": "Jane Doe", "preferred_username": "j.doe", "email": "[email protected]", "picture": "http://example.com/janedoe/me.jpg" }
client_id / client_secret
response_type
code
或其他扩展值(隐式模式下是 token
,但已不推荐)。grant_type
authorization_code
、refresh_token
、client_credentials
、password
(后两种并不推荐公开场景使用)。scope
read write profile
)。redirect_uri
code / access_token / refresh_token
expires_in
token_type
Bearer
,表明这是一个不带签名验证(或说仅依赖于传输层安全)的令牌,需要在 HTTPS 下使用。id_token
PKCE 相关字段
S256
;plain
只作为兼容保留。内容类型:
Content-Type: application/x-www-form-urlencoded
。Content-Type: application/json; charset=utf-8
。安全注意:
日志和错误处理:
error
、error_description
、error_uri
字段。OIDC 兼容:
userinfo_endpoint
、id_token
等。在开发 OAuth 2.0 授权服务器的过程中,最重要的是确保:
state
、code_verifier
等)。expires_in
的时间单位)和编码方式(URL 编码、Base64URL 等)都要与 RFC 要求保持一致。上面列出的字段和示例基本涵盖了 OAuth 2.0 标准流程中常见的必需字段和可选字段,希望能帮你在实现自己的 OAuth 2.0 Server 时做参考。祝开发顺利!
帮我实现一个基于access token 加scope的访问权限的注解,以及如何配合其他登录拦截的使用
下面给出一个示例,展示如何在 Java/Spring 技术栈中,通过 自定义注解 + 切面(AOP) 的方式实现「基于 Access Token + Scope」的权限控制,并且演示如何与现有的登录/鉴权逻辑(如 Session 或其他登录拦截)进行配合。思路如下:
@RequiredScope("xxx")
),由 AOP 切面进行额外的 Scope 检查,只有当 Access Token 所包含的 Scope 满足注解要求时才允许执行方法。当然,如果你已经使用了 Spring Security 的 Method Security(例如
@PreAuthorize("hasAuthority('SCOPE_xxx')")
),也可以直接用它内置的方式来做 Scope 检查。但是本文重点示范“自定义注解 + AOP”的思路,便于定制化逻辑和扩展。
先创建一个注解 @RequiredScope
,可标注在类或方法上:
javapackage com.example.security; import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequiredScope { /** * 需要具备的scope列表 */ String[] value() default {}; }
@Target({ElementType.METHOD, ElementType.TYPE})
表示可以注解在方法或类上。@Retention(RetentionPolicy.RUNTIME)
表示运行时可见。在使用时,可以写成:
java@RequiredScope({"read", "write"}) public void someApiMethod() { ... }
或只写一个:
java@RequiredScope("profile") public void userProfileApi() { ... }
为了后续能在切面中获取当前请求对应的 Access Token 及其 Scope,通常需要在“拦截请求”或“过滤器”的阶段先做基础验证。
在 Spring Boot + Spring Security 配置中,一般是这样的流程:
Authorization: Bearer <token>
。scope
字段或 scp
字段中;也可能是一个字符串列表)。SecurityContextHolder
中的 Authentication
或其他共享上下文中,方便后续取用。如果没有使用 Spring Security 的 oauth2ResourceServer()
或更高级的配置,可以自定义一个 OncePerRequestFilter
:
java@Component public class BearerTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // 假设我们有一个方法 parseToken() 去校验并解析 token // 返回里包含了 scopes、用户信息等 TokenPayload payload = parseToken(token); if (payload != null) { // 将解析结果放到 Spring Security 的上下文 // 这里只示例存放到 Principal 或 Authorities List<GrantedAuthority> authorities = payload.getScopes().stream() .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) .collect(Collectors.toList()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(payload.getUserId(), null, authorities); // 可以把token本身、scope列表再额外挂到 details 或其他地方 authentication.setDetails(payload); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } private TokenPayload parseToken(String token) { // 1. 解码/验证签名/JWT解析 // 2. 若成功,返回 TokenPayload(包括 userId, scopes 等) // 3. 若失败,返回 null 或抛异常 return null; // 仅示例 } }
这里的核心是:将 Token 对应的 Scope 转成 GrantedAuthority
并注入到 Spring Security 的 Authentication
里,这样后续的组件就可以从 SecurityContextHolder
获取到用户的 scope/权限信息。
有了上一步的数据上下文后,我们可以用一个 AOP 切面,在进入目标方法时检查是否具备所需 Scope。
javapackage com.example.security; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @Aspect @Component public class RequiredScopeAspect { @Around("@within(requiredScope) || @annotation(requiredScope)") public Object checkScope(ProceedingJoinPoint pjp, RequiredScope requiredScope) throws Throwable { if (requiredScope == null) { // 若类上/方法上没有注解,直接放行 return pjp.proceed(); } // 从注解上获取所需 scope String[] requiredScopes = requiredScope.value(); // 从SecurityContext里拿到当前用户/当前token的Authentication Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { throw new RuntimeException("用户未登录或未通过基本认证"); // 或者抛出自定义异常 } // 获取用户所拥有的Scopes,这里假设在Filter里已经存成 SCOPE_xxx Set<String> userScopes = new HashSet<>(); for (GrantedAuthority authority : auth.getAuthorities()) { // 例如 authority.getAuthority() = "SCOPE_read" if (authority.getAuthority().startsWith("SCOPE_")) { userScopes.add(authority.getAuthority().substring("SCOPE_".length())); } } // 判断是否包含所需的每一个 scope for (String rs : requiredScopes) { if (!userScopes.contains(rs)) { throw new RuntimeException("权限不足,需要 scope: " + rs); } } // 校验通过,继续执行目标方法 return pjp.proceed(); } }
说明:
@Around("@within(requiredScope) || @annotation(requiredScope)")
表示:
@RequiredScope
,则类下所有方法都要检查;@RequiredScope
,只检查该方法。GrantedAuthority
(形如 SCOPE_xxx
),在切面里就能读取到。RuntimeException
,实际项目中你可能需要抛出一个 AccessDeniedException
,然后让全局异常处理器捕获后返回相应的 403 响应。有了注解和切面之后,就可以在业务方法上直接标注:
java@RestController @RequestMapping("/api") public class UserController { // 需要具备 "read" scope 才能访问 @GetMapping("/user/profile") @RequiredScope("read") public String getUserProfile() { return "This is user profile"; } // 需要具备 "write" scope 才能访问 @PostMapping("/user/update") @RequiredScope("write") public String updateUserProfile() { return "Profile updated"; } }
或者在类级别使用:
java@RestController @RequestMapping("/api/orders") @RequiredScope("order") public class OrderController { @GetMapping("/{id}") public String getOrder(@PathVariable Long id) { return "Order detail of " + id; } @PostMapping("/") public String createOrder() { return "Order created"; } }
这样整个 OrderController
下面的接口都需要 scope = order
才可访问。
通常你会同时存在多种“鉴权/拦截”机制,比如:
@PreAuthorize("hasRole('ADMIN')")
。在 Spring Security 框架中,你可以把它们统一到 Security Filter Chain 和 MethodSecurity 中去。关键点在于:
如果你想用 Spring Security 内置的注解来做 Scope 校验,可以直接用
@PreAuthorize("hasAuthority('SCOPE_read')")
这样的表达式,而不必自己写切面。要点是你的 Filter 中要把 Scope 转成GrantedAuthority("SCOPE_xxx")
。
SecurityContext
。@PreAuthorize
等。GrantedAuthority("SCOPE_xxx")
放到 Authentication
里,这样无论是自定义切面还是 Spring Security 内置的 @PreAuthorize
都能用“权限”方式来进行判断。@EnableMethodSecurity
(原 @EnableGlobalMethodSecurity
)和 @PreAuthorize("hasAuthority('SCOPE_write')")
。AccessDeniedException
或 AuthenticationException
,然后通过 ExceptionHandler
或 Security 自带的处理器来返回 HTTP 状态码。以上示例示范了一个 「自定义注解 + 切面 + Scope 检查」 的最小实现思路,并说明了它如何与已有的登录/认证拦截(Filter 或 Spring Security)配合:
@PreAuthorize("hasAuthority('SCOPE_read')")
等功能,但自定义注解更灵活,适合在需要做复杂业务检查时进行扩展。这样就实现了一个基于 Access Token + Scope 的精细化访问控制,并且能够与 Session 或其他登录方式无缝配合。
我还需要拦截accessToken
下面给出一个在 Spring Boot 环境中「拦截 Access Token」的示例做法,帮助你在请求进来时,能够截获并验证 Token,然后再决定是否放行后续业务逻辑。结合之前提到的「自定义注解 + 切面 + Scope」方案,就可以形成完整的安全校验流程:
Authorization
Header 中的 Access Token。SecurityContextHolder
或者请求上下文中,供后续使用(例如自定义注解切面或 Spring Security 的方法安全 @PreAuthorize
来做更细粒度的权限判断)。下面演示一个 自定义 Filter,在 Spring Boot 中可以让它继承 OncePerRequestFilter
并加上 @Component
或在配置类里注册为 Bean。
javapackage com.example.security; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AccessTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1. 从请求头里拿到Authorization String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { // 2. 解析出accessToken String accessToken = authHeader.substring(7); try { // 3. 校验token有效性(签名、过期等) // 如果使用JWT,一般要验证签名、exp、issuer等 // 示例:Authentication auth = tokenService.parseAndValidate(accessToken); Authentication auth = validateTokenAndBuildAuthentication(accessToken); // 4. 如果校验成功,把用户信息/权限存到SecurityContext if (auth != null) { SecurityContextHolder.getContext().setAuthentication(auth); } else { // 校验失败的话,可以直接返回401 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } catch (Exception e) { // Token 无效或者过期 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } else { // 如果没有Bearer Token,你可以选择: // 1) 放行,后面的逻辑可能要求登录才可访问 // 2) 或直接拦截返回401,视业务需求而定 } // 继续过滤器链,后面会进入Controller filterChain.doFilter(request, response); } /** * 假设这是你自己实现的一个方法,用来校验accessToken并构建Authentication */ private Authentication validateTokenAndBuildAuthentication(String accessToken) { // 1. 解码、验证签名,如果是JWT,要解析其中的claims // 2. 如果有效,将token中的用户ID、scope/role 转成Spring Security的 Authentication // 这里仅作示例,返回null代表校验失败 // 正常你会构造 UsernamePasswordAuthenticationToken 或 JWTAuthenticationToken 等 return null; } }
如果你使用了 Spring Boot 的自动配置和 @Component
,一般情况下,这个 AccessTokenFilter
会在 Spring Security Filter Chain 中自动生效。但有时需要你在 Security 配置里显示配置 Filter 的顺序,例如:
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AccessTokenFilter accessTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http // 其他配置 .addFilterBefore(accessTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
在新版本 Spring Security 5.7+,
WebSecurityConfigurerAdapter
已被弃用,你可能需要用 SecurityFilterChain Bean 的方式来配置。核心思路类似:把你的自定义 Filter 注册到正确的位置。
AccessTokenFilter
之后,若 Token 有效,就会将 Authentication
放到 SecurityContextHolder
中。SecurityContextHolder.getContext().getAuthentication()
获取当前用户信息和权限 (Scopes/Roles)。@PreAuthorize
来检查“是否具备特定scope或权限”。例如,如果使用 自定义注解 + AOP:
java@RestController @RequestMapping("/api") public class SomeController { // 需要'write' scope才能访问 @RequiredScope("write") @PostMapping("/update") public String updateSomething() { // 此时,AccessTokenFilter已经验证了token,并写入了Authentication return "OK"; } }
在切面中,就可以拿到 SecurityContextHolder.getContext().getAuthentication()
,进而判断是否包含 "write"
scope 等。
有时你还想同时支持 Session 登录 和 Token 认证:
JSESSIONID
并且session有效,则 Spring Security 认为已登录,可以访问相应资源;Bearer token
,也可通过 AccessTokenFilter
校验。在 Spring Security 配置里,可以让 Session 机制和 Bearer Token 机制同时存在。关键是确定各个 Filter 的顺序,以及是否允许匿名访问。
Authorization: Bearer <token>
。X-Access-Token
),也可以,但最好跟标准兼容。validateTokenAndBuildAuthentication
方法里验证签名、检查 exp
过期时间、iss
、aud
等字段;同时解析出 sub
(用户ID)和 scope
(权限列表)。401 Unauthorized
。有些场景可能需要返回 JSON,比如 {"error":"invalid_token"}
,你可以在Filter里写 response.getWriter().write(...)
并返回。Authorization
头读取 Token,若合法则放行、若不合法则立即返回 401。SecurityContextHolder
里,这样后续自定义注解 + AOP 或 Spring Security 的 方法级别安全(@PreAuthorize
等)都能正常读取并进行权限判断。这样,你就拥有了一个完整的「拦截 Access Token + Scope 权限注解」的安全框架。
这时候对外开出的api,一般可以以什么路径开头
在对外开放(尤其面向第三方)的 API 设计中,路径通常会遵循一些约定俗成的命名规范,以便清晰区分、方便维护和升级。常见的做法主要有以下几种:
/api
前缀 + 版本号这是最常见的 RESTful API 约定,比如:
text/api/v1/... /api/v2/...
举例:
textGET /api/v1/users POST /api/v1/users ...
日后如果接口发生不兼容的变化,就可以推出 /api/v2/...
并在一定过渡期后淘汰 /api/v1/...
。
有些公司会将外部 API 独立到一个子域名,如 api.example.com
,再在路径中使用 /v1
、/v2
进行版本标识:
texthttps://api.example.com/v1/users https://api.example.com/v2/users
这样可以在部署和配置层面更灵活,例如负载均衡、缓存、CDN、独立网关等,方便与主站区分。
如果你还需要对外开放 OAuth2.0 授权、令牌等端点,通常会有类似:
textPOST /oauth/token GET /oauth/authorize POST /oauth/token/introspect POST /oauth/token/revoke
或 /oauth2/token
、/oauth2/authorize
等。
/oauth
,但行业习惯多用 /oauth
或 /oauth2
作为前缀。如果还存在后台管理端点或内部接口,一般会单独放在:
text/admin/... /internal/...
以区分与公开接口的边界,通常也会加更严格的访问控制。
在微服务或网关架构里,你可能会有 API Gateway 将各微服务的路径统一暴露成类似:
text/api/user-service/v1/... /api/order-service/v1/...
或通过 api.example.com/users
、api.example.com/orders
区分。核心思想仍然是:对外统一、对内解耦,并且可以做版本管理和权限控制。
/api/v1/orders
、/api/v1/products
。/api/v1/oauth/...
下,或者直接 /oauth2/...
,只要保证公开文档里说明清楚即可。假设你要对外开放一组用户信息与订单管理的 RESTful API,并提供 OAuth2.0 认证,可能会这样设计:
text# OAuth2.0 相关 POST /oauth/token -> 获取Token GET /oauth/authorize -> 授权端点 POST /oauth/token/revoke -> 撤销Token # API 主体(版本v1) GET /api/v1/users -> 获取用户列表 GET /api/v1/users/{id} -> 获取某个用户详情 POST /api/v1/users -> 创建新用户 GET /api/v1/orders -> 获取订单列表 GET /api/v1/orders/{id} -> 获取订单详情 POST /api/v1/orders -> 创建新订单
如果将来接口大改,就新增 /api/v2/...
,保持对旧版的兼容一段时间。
/api/...
为前缀,加上版本号(如 v1
、v2
)是常见最佳实践。api.example.com/v1/
的形式。/oauth/...
或 /oauth2/...
。/admin/...
、/internal/...
。