OAuth 2.0 规范总结

تم الإنشاء في: ١٨ يناير ٢٠٢٥

تمت الإجابة باستخدام OpenAI 01 بواسطة OpenAI01

سؤال

oauth2.0有哪些规范性的东西呢?

الفكرة

Gathering normative documents

I’m systematically compiling key RFCs for OAuth 2.0, covering core specifications, bearer tokens, PKCE, JWT profiles, and introspection.

Focusing on key documents

I’m identifying pivotal RFCs for OAuth 2.0: core authorization, bearer tokens, dynamic client registration, introspection, token revocation, JWT, PKCE, and device authorization grant.

Thought about OAuth 2.0规范,持续 4 秒

الإجابة

在 IETF 制定的 OAuth 2.0 生态中,最核心、最具规范性的文档都以 RFC(Request for Comments)形式发布。除了最基本的 OAuth 2.0 核心协议(即 RFC 6749、RFC 6750),围绕它还有众多扩展及补充规范,用于支持不同场景、功能和安全要求。下面列出主要的规范性文档(大多都可以在 IETF 的官方网站tools.ietf.org 上查阅):


1. OAuth 2.0 核心规范

  1. RFC 6749: The OAuth 2.0 Authorization Framework

    • 描述了 OAuth 2.0 的核心授权框架,包括四种授权方式(授权码、简化模式、密码模式、客户端凭证模式)。
    • 规定了授权服务器、资源服务器和客户端等角色的交互流程。
  2. RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage

    • 详细描述了 OAuth 2.0 中常用的“Bearer Token”在 HTTP 请求中的使用方法和安全考虑。

2. 扩展与补充规范

2.1 安全机制 & 流程增强

  1. RFC 7009: OAuth 2.0 Token Revocation

    • 描述了客户端或授权服务器如何对已发放的访问令牌或刷新令牌进行撤销(revocation)。
  2. RFC 7636: Proof Key for Code Exchange by OAuth Public Clients (PKCE)

    • 为“授权码模式”提供了额外的安全机制,避免在移动、桌面或单页应用中授权码被截获攻击(拦截重放)。
    • PKCE 在实际应用中已经成为业界推荐使用的最佳实践之一。
  3. RFC 7662: OAuth 2.0 Token Introspection

    • 为保护资源服务器提供了一个接口,让其可以与授权服务器通信,验证访问令牌(包括令牌是否过期、是否被撤销、作用范围等)。
  4. RFC 7008 (草案阶段) 等其他安全增强

    • 不同阶段还有一些 OAuth 2.0 安全最佳实践草案或补充,比如状态参数验证、nonce 参数等,在不同实现或框架中也有所体现。

2.2 客户端注册 & 动态管理

  1. RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol

    • 定义了客户端如何在授权服务器上进行动态注册(而非手动在管理后台配置)。
    • 适用于需要大规模自动化注册流程的场景。
  2. RFC 7592: OAuth 2.0 Dynamic Client Registration Management Protocol

    • 补充 RFC 7591,描述了已经在授权服务器注册的客户端,如何更新其注册信息(客户端名称、重定向 URI 等)。

2.3 各种授权扩展

  1. RFC 8628: OAuth 2.0 Device Authorization Grant (Device Flow)

    • 适用于没有传统 Web 浏览器或输入能力有限的设备(如智能电视、机顶盒、物联网设备等)。
    • 用户在独立设备或手机/电脑上输入验证码,完成授权流程。
  2. RFC 8252: OAuth 2.0 for Native Apps (BCP 212)

    • 针对原生应用(移动端、桌面端)的安全最佳实践,建议使用授权码模式 + PKCE,避免隐式授权模式等不安全做法。
  3. RFC 8707: Resource Indicators for OAuth 2.0

    • 允许客户端在授权请求和令牌请求中指明资源(resource indicators),让授权服务器区分发放针对不同资源的令牌,以实现更加精细化的访问控制。

2.4 令牌格式 & JWT 相关

  1. RFC 7519: JSON Web Token (JWT)

    • 不是 OAuth 专用,但常用于 OAuth 2.0 的令牌格式。
    • 之后又衍生出诸多相关规范(如 JWS、JWE、JWK 等),对令牌签名、加密、密钥管理作出说明。
  2. RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants

    • 指定了如何在 OAuth 2.0 中使用 JWT 作为客户端身份验证方式,或将 JWT 作为授权断言(assertion)来换取访问令牌。

3. OpenID Connect (OIDC) 规范

  • 虽然 OIDC 不属于纯粹的 OAuth 2.0 规范范畴,但它是基于 OAuth 2.0 扩展出来的身份层协议。
  • OIDC 提供了对用户身份信息(Profile、Email 等)的标准化获取方式(UserInfo Endpoint),非常常见于单点登录、社交登录等场景。
  • OIDC 的主要文档在 OpenID Foundation 网站上发布,包括 Core、Discovery、Dynamic Registration 等多篇规范,与 OAuth 2.0 是紧密结合的。

4. 业界最佳实践与其他资料

  • IETF OAuth 2.0 相关 Working Group
    除了正式发布的 RFC 外,OAuth 工作组还有一些草案文档(Draft),通常涵盖更前沿的应用场景和安全最佳实践。可以在 IETF Datatracker 中关注相关进展。

  • 各大云服务或社交平台的实现
    如 Google、微软、GitHub、微信企业号等在各自开发者文档中,都有遵循(或部分遵循)OAuth 2.0 规范的实现说明。有时还会结合一些私有扩展。

  • 开发者社区与官方示例

    • 诸如 Auth0、Okta 等身份服务厂商,都提供 OAuth 2.0 / OIDC 的 SDK、示例和最佳实践文档,可为初学者提供参考。

小结

OAuth 2.0 并不是只由单一文档定义的,而是由一系列 RFC 文档所构成的一个“框架+扩展”生态系统。其中最核心的规范是 RFC 6749(核心框架)和 RFC 6750(Bearer Token 使用)。围绕这两个核心,又有许多扩展规范,为不同的业务场景(如设备模式、动态注册、令牌格式、身份层扩展等)提供了可选的规范性指导。

如果你想系统了解 OAuth 2.0 的整体规范,可以从如下路径入手:

  1. 先精读 RFC 6749、RFC 6750:了解 OAuth 2.0 核心原理和 Bearer Token 基本用法。
  2. 根据需求查阅扩展 RFC:如 PKCE(RFC 7636)提升安全性,Token Revocation(RFC 7009)、Token Introspection(RFC 7662)增强令牌管控等。
  3. 关注最新草案和实践:IETF OAuth 工作组的草案文档、各大身份提供商的最佳实践,都能帮助你更好地应对真实生产环境中的需求和安全问题。

希望对你了解和使用 OAuth 2.0 的“规范性”部分有所帮助。

سؤال

很好,我是个开发人员,在开发oauth2.0 server的时候,在每一步,需要透出哪些字段,他们的格式/单位一般是什么,
申请code
获取token
更新refresh-token
获取用户信息

الفكرة

Mapping authorization flows

I'm piecing together the OAuth server implementation, focusing on authorization and token request endpoints. This involves outlining typical fields and their formats in requests and responses, considering both standard and PKCE scenarios.

Thought about OAuth 2.0 server字段,持续 9 秒

الإجابة

下面的说明主要基于 OAuth 2.0 核心规范(RFC 6749、RFC 6750) 和常见的实践(包括 PKCE、Refresh Token 等)进行整理,给出在开发 OAuth 2.0 授权服务器时“各步骤常见字段”和“它们的典型格式、单位或约定”。不同厂商或框架可能会在此基础上增加一些私有扩展字段,这里仅列出标准和通用做法,供参考。


1. 申请 Code(授权码)阶段

通常对应浏览器重定向到“授权端点(Authorization Endpoint)”的 GET 请求:

  1. 请求参数(客户端 -> 授权服务器)

    • response_type:固定值为 code
    • client_id:在授权服务器注册的客户端 ID。
    • redirect_uri:客户端希望在授权完成后重定向到的 URI。
    • scope:用空格分隔的字符串,比如 read write profile
    • state:客户端自带的防 CSRF 随机字符串(可选但强烈推荐)。
    • code_challenge(若使用 PKCE):以 Base64URL 编码的 PKCE Challenge。
    • code_challenge_method(若使用 PKCE):一般为 S256plain(推荐 S256)。

    示例:

    GET /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)
    
  2. 响应参数(授权服务器 -> 客户端浏览器重定向)

    • code:授权码(通常是一段随机字符串,例如 UUID、Base64URL、或其他安全生成方式)。
    • state:如果请求中带了 state,必须原样返回。

    示例:

    HTTP/1.1 302 Found
    Location: https://client.example.org/cb?code=abcd1234&state=xyz987
    

    授权码的有效期:通常只有几十秒到几分钟,并且只能使用一次。


2. 获取 Token 阶段

对应“令牌端点(Token Endpoint)”的 POST 请求,客户端会使用在上一步拿到的 code 来换取 access_token(以及可选的 refresh_token)。

  1. 请求参数(客户端 -> 授权服务器)

    • grant_type:固定值 authorization_code
    • code:上一步骤拿到的授权码。
    • redirect_uri:必须与申请 code 时使用的 redirect_uri 一致(如果当时指定了),用于校验。
    • client_id:客户端 ID。
    • client_secret:若是机密客户端(Confidential Client),此项必填;对于原生 APP 或浏览器 SPA,通常不使用/弱化此字段。
    • code_verifier(若使用 PKCE):对应授权阶段的 code_challenge,用于校验。PKCE 场景下必填。

    通常这些参数会以 application/x-www-form-urlencoded 方式传递在请求体中,或在 HTTP Basic Auth 头里携带 client_id、client_secret。

    示例(Body 里带参数)

    POST /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)
    
  2. 响应参数(授权服务器 -> 客户端)
    一般返回 JSON,常见字段:

    • access_token:访问令牌(字符串),可为随机字符串或 JWT 格式。
    • token_type:通常为 Bearer
    • expires_in:令牌有效期,单位是,如 3600
    • refresh_token(可选):若授权服务器支持刷新令牌,会返回该字段。
    • scope(可选):如果与之前申请的 scope 不一致,可在此说明实际赋予的 scope。
    • id_token(可选,针对 OIDC):如果实现了 OpenID Connect,就会返回 id_token,用于标识用户身份。

    示例

    { "access_token": "SlAV32hkKG", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "8xLOxBtZp8", "scope": "read profile" }

3. 使用 Refresh Token 刷新 Access Token

在令牌端点(Token Endpoint)再次发起请求,用 refresh_token 换取新的 access_token(以及可能新的 refresh_token)。

  1. 请求参数(客户端 -> 授权服务器)

    • grant_type:固定值 refresh_token
    • refresh_token:上一步得到的刷新令牌。
    • client_id:客户端 ID;同样有时也需要 client_secret(机密客户端)。
    • scope(可选):如果想请求更小范围的权限,可以带入;若不指定,则默认与原来的 scope 相同。

    示例

    POST /token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=refresh_token
    &refresh_token=8xLOxBtZp8
    &client_id=abc123
    &client_secret=ssh-secret
    
  2. 响应参数(授权服务器 -> 客户端)
    通常与“获取 Token”阶段的响应字段类似:

    • access_token:新的访问令牌。
    • token_typeBearer
    • expires_in:新的访问令牌有效期(秒)。
    • refresh_token(可选):有些服务器会颁发一个全新的 refresh_token;有些会让原来的继续有效。
    • scope(可选)。

    示例

    { "access_token": "hGFbs732sdf...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "zxcvb0987", "scope": "read profile" }

4. 获取用户信息(UserInfo Endpoint)

获取用户信息这一环节在 纯 OAuth 2.0 里并没有强制标准化的“用户信息端点”,但在 OpenID Connect(OIDC)里有明确的 userinfo_endpoint
如果只做 OAuth 2.0,你可以自定义一个资源端点让客户端拿着 access_token 去获取用户资料。在 OIDC 的场景下,用户信息是标准化的,常见字段包括 sub(用户唯一标识)、nameemail 等。

  1. 请求方式

    • 一般是 GET 请求到某个类似 https://server.example.com/userinfo 的端点。
    • 在 HTTP Header 中携带 Authorization: Bearer <access_token>

    示例

    GET /userinfo
    Host: server.example.com
    Authorization: Bearer SlAV32hkKG
    
  2. 响应内容

    • 通常是 JSON
    • 具体字段根据业务需求而定,如果是 OIDC,则会包含 subpreferred_usernameemailname 等常见 Claims。

    示例(OIDC 格式):

    { "sub": "248289761001", "name": "Jane Doe", "preferred_username": "j.doe", "email": "[email protected]", "picture": "http://example.com/janedoe/me.jpg" }

5. 小结:各字段格式及注意点

  1. client_id / client_secret

    • 都是字符串,一般在注册客户端时就固定,不能含有空格等特殊字符;有时是纯字母数字,有时是 Base64 编码。
  2. response_type

    • 字符串,一般为 code 或其他扩展值(隐式模式下是 token,但已不推荐)。
  3. grant_type

    • 字符串,一般是 authorization_coderefresh_tokenclient_credentialspassword(后两种并不推荐公开场景使用)。
  4. scope

    • 多个权限用空格分隔的小写字符串(如 read write profile)。
    • 也可用逗号、冒号,但根据 RFC 6749 最常见是空格分隔。
  5. redirect_uri

    • URL 编码,必须与注册时或授权请求时设置的 URI 完全匹配(如大小写、斜杠等)。
  6. code / access_token / refresh_token

    • 都是随机字符串;为了安全,建议具有足够长度和熵。
    • 或者使用 JWT 方式(JSON Web Token),内部包含签名、过期时间等。
  7. expires_in

    • 单位:秒
    • 典型值如 3600 (1 小时) 或 86400 (1 天)。
  8. token_type

    • 通常为 Bearer,表明这是一个不带签名验证(或说仅依赖于传输层安全)的令牌,需要在 HTTPS 下使用。
  9. id_token

    • OpenID Connect 扩展的字段,通常是一个 JWT,包含用户身份相关的声明(claims)。
  10. PKCE 相关字段

    • code_challenge:Base64URL 编码。
    • code_challenge_method:一般为 S256plain 只作为兼容保留。
    • code_verifier:与 code_challenge 一一对应,也是随机生成并用 Base64URL 编码。

6. 开发时的其他建议

  1. 内容类型

    • Token Endpoint 的请求通常使用 Content-Type: application/x-www-form-urlencoded
    • 响应通常使用 Content-Type: application/json; charset=utf-8
  2. 安全注意

    • 强制使用 HTTPS,避免访问令牌、授权码在网络中明文传输。
    • PKCE(RFC 7636)在移动端、SPA、Native App 等“公共客户端”场景已基本是强制或推荐使用的安全扩展。
    • 要实现 “Token Revocation” (RFC 7009) 或 “Token Introspection” (RFC 7662) 等端点,以便更好管理令牌生命周期。
  3. 日志和错误处理

    • 遵循 RFC 6749 第 5.2 节的错误响应格式,比如 errorerror_descriptionerror_uri 字段。
    • 返回错误时也建议用 JSON 格式,以便客户端能解析。
  4. OIDC 兼容

    • 如果计划提供单点登录(SSO)或用户资料查询等身份功能,可以结合 OpenID Connect Core 规范,提供 userinfo_endpointid_token 等。

结语

在开发 OAuth 2.0 授权服务器的过程中,最重要的是确保:

  1. 每个阶段的请求/响应字段都符合规范(尤其是必填项),并正确处理安全参数(如 statecode_verifier 等)。
  2. 字段格式(尤其是 expires_in 的时间单位)和编码方式(URL 编码、Base64URL 等)都要与 RFC 要求保持一致。
  3. 安全性:只在 HTTPS 环境下使用、避免不安全的授权模式(如隐式模式)、对令牌进行合理的过期和撤销管理。

上面列出的字段和示例基本涵盖了 OAuth 2.0 标准流程中常见的必需字段和可选字段,希望能帮你在实现自己的 OAuth 2.0 Server 时做参考。祝开发顺利!

سؤال

帮我实现一个基于access token 加scope的访问权限的注解,以及如何配合其他登录拦截的使用

الفكرة

Implementing access control

I’m piecing together an approach to use Spring Security for parsing an access token from the HTTP header and verifying the "read" scope. This method might involve custom filters or annotations.

Charting approaches

I’m mapping out a custom annotation or Spring AOP aspect to validate token scopes. Incorporating OAuth2 resource server config with JWTs emerges as a streamlined option.

Piecing together

I'm defining a RequiredScope annotation and a corresponding aspect to validate token scopes. This involves parsing tokens and integrating with existing login checks for comprehensive security.

Creating a minimal Spring Boot solution

I’m outlining a Java-based approach to implement @RequiredScope annotation, incorporating a custom method security expression and using @Around aspect for validation. This approach ensures seamless integration and validation.

Integrating JWT validation

I’m detailing how to parse tokens and check scopes using SecurityContextHolder. For easier integration, @PreAuthorize is recommended for standard permissions.

Thought about access token 权限注解,持续 10 秒

الإجابة

下面给出一个示例,展示如何在 Java/Spring 技术栈中,通过 自定义注解 + 切面(AOP) 的方式实现「基于 Access Token + Scope」的权限控制,并且演示如何与现有的登录/鉴权逻辑(如 Session 或其他登录拦截)进行配合。思路如下:

  1. 在请求到达业务方法前,会有一个通用的登录/认证拦截(如 Spring Security Filter、Session 校验等)——用来保证请求已通过基本身份鉴权(例如确认用户身份或确认 Token 有效)。
  2. 业务方法上标注一个自定义注解(如 @RequiredScope("xxx")),由 AOP 切面进行额外的 Scope 检查,只有当 Access Token 所包含的 Scope 满足注解要求时才允许执行方法。

当然,如果你已经使用了 Spring Security 的 Method Security(例如 @PreAuthorize("hasAuthority('SCOPE_xxx')")),也可以直接用它内置的方式来做 Scope 检查。但是本文重点示范“自定义注解 + AOP”的思路,便于定制化逻辑和扩展。


1. 定义自定义注解

先创建一个注解 @RequiredScope,可标注在类或方法上:

package 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) 表示运行时可见。

在使用时,可以写成:

@RequiredScope({"read", "write"}) public void someApiMethod() { ... }

或只写一个:

@RequiredScope("profile") public void userProfileApi() { ... }

2. 在认证环节中获取并存储 Access Token 与 Scope

为了后续能在切面中获取当前请求对应的 Access Token 及其 Scope,通常需要在“拦截请求”或“过滤器”的阶段先做基础验证。
Spring Boot + Spring Security 配置中,一般是这样的流程:

  1. Spring Security Filter 或者自定义 Filter 从请求头中获取 Authorization: Bearer <token>
  2. 验证 Token 是否有效(检查签名、过期时间等),如果无效则直接返回 401。
  3. 如果 Token 有效,就解析出 Token 里包含的 Scope(对于 JWT,通常在 scope 字段或 scp 字段中;也可能是一个字符串列表)。
  4. 用户信息和 Scope 放到 SecurityContextHolder 中的 Authentication 或其他共享上下文中,方便后续取用。

简单示例:自定义过滤器(可选)

如果没有使用 Spring Security 的 oauth2ResourceServer() 或更高级的配置,可以自定义一个 OncePerRequestFilter

@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/权限信息。


3. 编写 AOP 切面,解析注解并校验 Scope

有了上一步的数据上下文后,我们可以用一个 AOP 切面,在进入目标方法时检查是否具备所需 Scope。

package 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,只检查该方法。
  • 如果在FilterSpring Security过程中已经将 scope 信息转成 GrantedAuthority(形如 SCOPE_xxx),在切面里就能读取到。
  • 这里的异常处理只是简单抛了 RuntimeException,实际项目中你可能需要抛出一个 AccessDeniedException,然后让全局异常处理器捕获后返回相应的 403 响应。

4. 在 Controller 或 Service 中使用注解

有了注解和切面之后,就可以在业务方法上直接标注:

@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"; } }

或者在类级别使用:

@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 才可访问。


5. 与其他「登录拦截」配合

通常你会同时存在多种“鉴权/拦截”机制,比如:

  1. 基于 Session 的登录:后端检查用户是否有有效的 Session。
  2. 基于 Token 的鉴权:从 HTTP Header 中取 Bearer Token。
  3. 基于角色/权限的 RBAC:如 @PreAuthorize("hasRole('ADMIN')")
  4. 基于Scope的精细化资源权限(本文所述)。

Spring Security 框架中,你可以把它们统一到 Security Filter ChainMethodSecurity 中去。关键点在于:

  • 先在过滤器或 Security Filter 中统一做「是否登录/Token 是否有效」的检查。也就是先保证请求是“已登录/已认证”的
  • 然后再用切面或注解做“是否具备足够权限”或者“是否具备足够scope”的细分检查。

如果你想用 Spring Security 内置的注解来做 Scope 校验,可以直接用 @PreAuthorize("hasAuthority('SCOPE_read')") 这样的表达式,而不必自己写切面。要点是你的 Filter 中要把 Scope 转成 GrantedAuthority("SCOPE_xxx")

常见顺序

  1. 请求到达
    -> Security Filter(或自定义 Filter):校验 Token 或 Session,有效则把用户信息与权限(scope/roles)放到 SecurityContext
    -> DispatcherServlet
    -> Controller 方法:如果用自定义切面或 Spring Security 的方法安全,则会再执行 AOP 检查/@PreAuthorize 等。
    -> 返回响应

6. 常见问题 & 建议

  1. 存储 Scope
    • 一般做法:在认证通过后,就将 Token 里的 scope(字符串列表)转成 GrantedAuthority("SCOPE_xxx") 放到 Authentication 里,这样无论是自定义切面还是 Spring Security 内置的 @PreAuthorize 都能用“权限”方式来进行判断。
  2. 优先使用标准化做法
    • 如果对 Spring Security 熟悉,推荐直接使用 @EnableMethodSecurity(原 @EnableGlobalMethodSecurity)和 @PreAuthorize("hasAuthority('SCOPE_write')")
    • 自定义注解+切面的好处是灵活可扩展,你可以在切面里进行更复杂的业务逻辑(比如多种scope之间的组合策略,或者还需校验数据库中的资源权限等)。
  3. 异常处理
    • 为了让客户端更好地接收 403 或 401,你可以在切面里抛出 Spring Security 的 AccessDeniedExceptionAuthenticationException,然后通过 ExceptionHandler 或 Security 自带的处理器来返回 HTTP 状态码。
  4. 与 Session 登录并存
    • 如果某些接口既支持 Session 登录,也支持 Token 登录,通常可以在 Filter Chain 里配置多重策略:先看 Session 是否有效,如果无 Session 再看是否带 Bearer Token。如果都没有,就拒绝访问。
    • 业务层的方法只需关心“当前上下文是否Authenticated + 是否具备所需scope/权限”,并不需要关心到底是哪种登录方式。

总结

以上示例示范了一个 「自定义注解 + 切面 + Scope 检查」 的最小实现思路,并说明了它如何与已有的登录/认证拦截(Filter 或 Spring Security)配合:

  1. 登录拦截(或 Token 校验)阶段:确保请求本身合法、并在上下文中存储Token的scope
  2. 方法或类级别的注解:通过切面读取注解信息和上下文中的scope,做二次权限校验(是否具备特定scope)。
  3. 如果不想自定义注解,你也可以使用 Spring Security 内置的 @PreAuthorize("hasAuthority('SCOPE_read')") 等功能,但自定义注解更灵活,适合在需要做复杂业务检查时进行扩展。

这样就实现了一个基于 Access Token + Scope 的精细化访问控制,并且能够与 Session 或其他登录方式无缝配合。

سؤال

我还需要拦截accessToken

الفكرة

Crafting the solution

I’m working on a Spring Boot application to intercept and verify access tokens from incoming requests, ensuring security and proper handling of valid or invalid tokens.

Thought about intercepting access token,持续 几秒

الإجابة

下面给出一个在 Spring Boot 环境中「拦截 Access Token」的示例做法,帮助你在请求进来时,能够截获并验证 Token,然后再决定是否放行后续业务逻辑。结合之前提到的「自定义注解 + 切面 + Scope」方案,就可以形成完整的安全校验流程:


1. 基础思路

  1. 请求进入应用的最外层,我们配置一个 Filter(或 Spring Security 的 Filter Chain)来拦截并解析 Authorization Header 中的 Access Token
  2. 如果 Token 无效或缺失,直接返回 401(或 403)错误,再调用后续 Controller。
  3. 如果 Token 有效,则将解析出的用户信息 / Scope / 角色等,放入到 SecurityContextHolder 或者请求上下文中,供后续使用(例如自定义注解切面或 Spring Security 的方法安全 @PreAuthorize 来做更细粒度的权限判断)。

2. 自定义 Filter 拦截 Access Token

下面演示一个 自定义 Filter,在 Spring Boot 中可以让它继承 OncePerRequestFilter 并加上 @Component 或在配置类里注册为 Bean。

package 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; } }

2.1 将 Filter 生效

如果你使用了 Spring Boot 的自动配置和 @Component,一般情况下,这个 AccessTokenFilter 会在 Spring Security Filter Chain 中自动生效。但有时需要你在 Security 配置里显示配置 Filter 的顺序,例如:

@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 注册到正确的位置。


3. 在 Controller 或 Service 层如何获取 Token 信息

  • 在经过 AccessTokenFilter 之后,若 Token 有效,就会将 Authentication 放到 SecurityContextHolder 中。
  • 在业务代码中,你可以通过 SecurityContextHolder.getContext().getAuthentication() 获取当前用户信息和权限 (Scopes/Roles)。
  • 这样可以配合你之前的 “自定义注解 + AOP” 或 Spring Security 的 @PreAuthorize 来检查“是否具备特定scope或权限”。

例如,如果使用 自定义注解 + AOP

@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 等。


4. 与登录拦截并存

有时你还想同时支持 Session 登录Token 认证

  1. 如果请求里带了 JSESSIONID 并且session有效,则 Spring Security 认为已登录,可以访问相应资源;
  2. 如果请求头带了 Bearer token,也可通过 AccessTokenFilter 校验。
  3. 如果都没有,则拒绝访问或跳转登录页(具体要看你对匿名访问是否开放)。

Spring Security 配置里,可以让 Session 机制和 Bearer Token 机制同时存在。关键是确定各个 Filter 的顺序,以及是否允许匿名访问。


5. 常见问题 & 建议

  1. Token 放在哪个头里?
    • 大部分OAuth2约定使用 Authorization: Bearer <token>
    • 如果你想使用其他自定义 Header(如 X-Access-Token),也可以,但最好跟标准兼容。
  2. Token 校验的实现
    • 如果是 JWT,你需要在 validateTokenAndBuildAuthentication 方法里验证签名、检查 exp 过期时间、issaud 等字段;同时解析出 sub(用户ID)和 scope(权限列表)。
    • 如果是非 JWT(随机字符串),通常要在数据库或缓存里查找 Token 的有效性。
  3. 异常处理
    • 当 Token 无效时,通常返回 401 Unauthorized。有些场景可能需要返回 JSON,比如 {"error":"invalid_token"},你可以在Filter里写 response.getWriter().write(...) 并返回。
  4. 安全注意事项
    • 确保在 HTTPS 环境下传输 Token;
    • 避免把 Access Token 缓存在不安全的地方(前端或第三方服务);
    • Token 应当设置合理的过期时间,并支持撤销或刷新流程(见 OAuth2.0 Token Revocation / Token Introspection)。

结论

  • 自定义 Filter 用于拦截校验 Access Token,是最直接的做法:从 Authorization 头读取 Token,若合法则放行、若不合法则立即返回 401。
  • 在 Token 校验成功后,把Token中的信息(用户ID、Scope 等)存到 SecurityContextHolder 里,这样后续自定义注解 + AOP 或 Spring Security 的 方法级别安全@PreAuthorize等)都能正常读取并进行权限判断。
  • 这种方式可与现有的Session登录拦截角色权限控制等并行,核心是保证在最初的 Filter 中把 Token 验证好并存放到安全上下文,后面就可以无缝联动各种注解/切面/拦截机制。

这样,你就拥有了一个完整的「拦截 Access Token + Scope 权限注解」的安全框架。

سؤال

这时候对外开出的api,一般可以以什么路径开头

الفكرة

Crafting API paths

OK, let’s think this through: stable and versioned structures work for APIs. Consider /api/ for base, /v1/ for versioning, or /oauth2/ for standards.

Thought about API path conventions,持续 几秒

الإجابة

对外开放(尤其面向第三方)的 API 设计中,路径通常会遵循一些约定俗成的命名规范,以便清晰区分、方便维护和升级。常见的做法主要有以下几种:


1. 使用 /api 前缀 + 版本号

这是最常见的 RESTful API 约定,比如:

/api/v1/... 
/api/v2/...
  • /api:表示这是供外部或客户端调用的接口,而非前端网页或内部服务端点。
  • v1 / v2:在路径上加版本号,便于未来升级或不兼容变更时保留旧版本、引入新版本。

举例:

GET  /api/v1/users
POST /api/v1/users
...

日后如果接口发生不兼容的变化,就可以推出 /api/v2/... 并在一定过渡期后淘汰 /api/v1/...


2. 使用单独子域名 + 版本号(可选)

有些公司会将外部 API 独立到一个子域名,如 api.example.com,再在路径中使用 /v1/v2 进行版本标识:

https://api.example.com/v1/users
https://api.example.com/v2/users

这样可以在部署和配置层面更灵活,例如负载均衡、缓存、CDN、独立网关等,方便与主站区分。


3. OAuth 相关端点的常见路径

如果你还需要对外开放 OAuth2.0 授权、令牌等端点,通常会有类似:

POST /oauth/token
GET  /oauth/authorize
POST /oauth/token/introspect
POST /oauth/token/revoke

/oauth2/token/oauth2/authorize 等。

  • 这部分路径在 RFC 6749 等规范中并没有强制要求一定是 /oauth,但行业习惯多用 /oauth/oauth2 作为前缀。

4. 内部管理/系统端点

如果还存在后台管理端点或内部接口,一般会单独放在:

/admin/... 
/internal/... 

以区分与公开接口的边界,通常也会加更严格的访问控制。


5. 结合网关或服务拆分

在微服务或网关架构里,你可能会有 API Gateway 将各微服务的路径统一暴露成类似:

/api/user-service/v1/...
/api/order-service/v1/...

或通过 api.example.com/usersapi.example.com/orders 区分。核心思想仍然是:对外统一对内解耦,并且可以做版本管理和权限控制。


6. 建议和实践

  1. 必须明确版本:无论是在子域名还是在路径上,标注版本号都是较好的实践。
  2. 简洁易读:路径层级不要过深,模块名/资源名尽量直观,比如 /api/v1/orders/api/v1/products
  3. 保持一致:一旦确定前缀/版本号命名,在同一项目中尽量统一,减少对调用方的困惑。
  4. OAuth 端点:可以和业务资源端点一样放在 /api/v1/oauth/... 下,或者直接 /oauth2/...,只要保证公开文档里说明清楚即可。

示例

假设你要对外开放一组用户信息与订单管理的 RESTful API,并提供 OAuth2.0 认证,可能会这样设计:

# 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 通常以 /api/... 为前缀,加上版本号(如 v1v2)是常见最佳实践。
  • 如果你单独规划了子域名,可用 api.example.com/v1/ 的形式。
  • OAuth2.0 的授权、Token 等端点常见于 /oauth/.../oauth2/...
  • 内部、管理或系统接口可放在 /admin/.../internal/...
  • 最终选择要结合团队习惯、系统架构和对外文档可读性,核心是清晰、易维护、兼容升级

مشاركة هذا السؤال والجواب