Java框架快速入门: Spring Security+OAuth2之核心角色与授权流程

纲要

  • OAuth2 协议的核心角色
    • Resource Owner(资源所有者)
    • Client(客户端应用)
    • Authorization Server(授权服务器)
    • Resource Server(资源服务器)
  • 客户端注册与凭据
    • client_idclient_secret 及回调地址
  • 主要授权流程概览
    • 授权码流程(Authorization Code)
    • 资源所有者密码流程(Resource Owner Password Credentials)
    • 客户端凭证流程(Client Credentials)
    • 刷新令牌流程(Refresh Token)
  • 基于 Spring Security OAuth2 的实践
    • 项目结构
    • 依赖与配置
    • 授权服务器配置
    • 资源服务器与安全配置
    • 客户端注册示例

OAuth2 协议的核心角色

OAuth2 是一种业界标准的授权协议,广泛用于微服务、社交登录等场景,解决跨系统间的授权问题。

一个典型的 OAuth2 交互涉及四个角色:

  • Resource Owner(资源所有者):拥有受保护资源的最终用户,例如微博账号的持有人。当第三方应用希望访问其资源时,需要获得该用户的明确许可。
  • Client(客户端):希望访问用户资源的第三方应用。它可以是 Web 应用、移动 App 或后端服务。例如使用微博登录的"简书"网站,在发起授权请求的那一刻就是客户端。
  • Authorization Server(授权服务器):负责对用户进行身份认证并颁发令牌。它提供授权页面,询问用户是否同意客户端访问特定资源。微博的授权页面就是一个授权服务器,逻辑上可以与资源服务器部署在一起,但角色是分离的。
  • Resource Server(资源服务器):托管受保护资源的服务器,仅接受持有有效令牌的请求。微博的用户信息、微博内容、评论等都属于资源服务器的范畴。在同一个系统中,授权服务器本身也可以是资源服务器(例如用户与角色管理)。

这些角色之间的交互解决了"如何在无需暴露用户密码的情况下,让第三方应用安全访问用户资源"的问题。


客户端注册与凭据

任何客户端在使用 OAuth2 之前,都需要先向授权服务器注册。注册时通常需要提供:

  • 应用名称与描述
  • 回调地址(重定向 URI)
  • 可能的图标或主页地址

授权服务器会为该客户端生成一对凭据:

  • client_id:客户端的唯一标识(公开)
  • client_secret:客户端的密钥(必须保密)

在某些授权流程(如客户端凭证流程)中,client_idclient_secret 就相当于客户端自身的"用户名"和"密码",用于向授权服务器证明自己的身份。对于不需要用户参与的场景(如微服务间的调用),仅凭这对凭据即可完成认证。

主要授权流程

OAuth2 定义了多种授权类型以覆盖不同场景。Spring Security 对以下四种流程提供了原生支持。

授权码流程 (Authorization Code)

这是最主流、最安全的流程,适用于 Web 应用、SPA 或移动端 App。其核心特征是"两次跳转、两次请求":

  1. 用户在客户端点击"微博登录",浏览器被重定向至授权服务器的登录页。
  2. 用户在授权服务器上使用自己的账号登录,并同意客户端访问特定资源的授权。
  3. 授权服务器将浏览器重定向回客户端预先注册的回调地址,并在 URL 中携带一个授权码
  4. 客户端在后端用该授权码,结合 client_idclient_secret,向授权服务器换取访问令牌(access_token)和刷新令牌(refresh_token)。
  5. 客户端使用 access_token 请求资源服务器的 API,获取用户数据或执行操作。

以下是该流程的时序图:
资源服务器 授权服务器 客户端 用户(资源所有者) 资源服务器 授权服务器 客户端 用户(资源所有者) #mermaid-svg-HwnNgH2MlbTfYOmN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HwnNgH2MlbTfYOmN .error-icon{fill:#552222;}#mermaid-svg-HwnNgH2MlbTfYOmN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HwnNgH2MlbTfYOmN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HwnNgH2MlbTfYOmN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HwnNgH2MlbTfYOmN .marker.cross{stroke:#333333;}#mermaid-svg-HwnNgH2MlbTfYOmN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HwnNgH2MlbTfYOmN p{margin:0;}#mermaid-svg-HwnNgH2MlbTfYOmN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HwnNgH2MlbTfYOmN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HwnNgH2MlbTfYOmN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HwnNgH2MlbTfYOmN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HwnNgH2MlbTfYOmN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HwnNgH2MlbTfYOmN .sequenceNumber{fill:white;}#mermaid-svg-HwnNgH2MlbTfYOmN #sequencenumber{fill:#333;}#mermaid-svg-HwnNgH2MlbTfYOmN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HwnNgH2MlbTfYOmN .messageText{fill:#333;stroke:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HwnNgH2MlbTfYOmN .labelText,#mermaid-svg-HwnNgH2MlbTfYOmN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .loopText,#mermaid-svg-HwnNgH2MlbTfYOmN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HwnNgH2MlbTfYOmN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HwnNgH2MlbTfYOmN .noteText,#mermaid-svg-HwnNgH2MlbTfYOmN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HwnNgH2MlbTfYOmN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HwnNgH2MlbTfYOmN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HwnNgH2MlbTfYOmN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HwnNgH2MlbTfYOmN .actorPopupMenu{position:absolute;}#mermaid-svg-HwnNgH2MlbTfYOmN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-HwnNgH2MlbTfYOmN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HwnNgH2MlbTfYOmN .actor-man circle,#mermaid-svg-HwnNgH2MlbTfYOmN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HwnNgH2MlbTfYOmN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击社交登录重定向到授权端点 (含client_id,redirect_uri等)显示登录页与授权页输入凭证并授权重定向回回调地址并携带授权码使用授权码换取令牌 (client_id+secret)返回 access_token (及refresh_token)请求资源 (Header中携带Bearer token)返回受保护资源

资源所有者密码流程 (Resource Owner Password Credentials)

此流程适用于受高度信任的官方应用,用户体验更好,但安全性较低。用户直接将用户名和密码提供给客户端,由客户端携带这些凭证去授权服务器直接换取令牌。

注意:第三方客户端不应使用此模式,因为它需要直接接触用户的明文密码。

  • 客户端收集用户的用户名和密码。
  • 客户端向授权服务器发送 POST 请求,包含 grant_type=passwordusernamepassword 以及自己的 client_id/client_secret
  • 授权服务器验证成功后,直接返回令牌,无需跳转或授权码。

客户端凭证流程 (Client Credentials)

专门为后端服务间调用 设计,整个交互不涉及用户。客户端仅凭自己的 client_idclient_secret 直接获取访问令牌,用于访问其他微服务的资源。

  • 服务 A 使用自己的 client_idclient_secret,请求授权服务器的令牌端点(grant_type=client_credentials)。
  • 授权服务器验证服务身份后返回令牌。
  • 服务 A 使用该令牌访问服务 B 的 API,服务 B 通过验证令牌来确认调用方身份。

该模式中没有"用户名密码"概念,服务的身份等价于 client_id

刷新令牌流程 (Refresh Token)

访问令牌(access_token)通常具有较短的有效期。当它过期时,客户端无需让用户重新登录,可以使用刷新令牌(refresh_token)来换取新的访问令牌。

  • 客户端向授权服务器发送请求,grant_type=refresh_token,并附上有效的 refresh_token 以及客户端凭据。
  • 授权服务器验证后,返回一对新的 access_tokenrefresh_token(旧令牌失效,实现令牌轮换)。

基于 Spring Security OAuth2 的实践

下面构建一个最小的授权服务器与资源服务器,演示授权码流程。所有代码可直接运行(基于 Spring Boot 2.x + spring-security-oauth2-autoconfigure)。

项目结构

dir 复制代码
src/main/java/com/example/oauth2
├── config
│   ├── AuthorizationServerConfig.java
│   ├── ResourceServerConfig.java
│   └── SecurityConfig.java
├── OAuth2Application.java
src/main/resources
└── application.properties

依赖配置

使用 Spring Boot 2.7.x 与 spring-security-oauth2-autoconfigure。在 pom.xml 中添加:

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.7.18</version>
    </dependency>
</dependencies>

配置文件

application.properties 中定义客户端信息(这里以内嵌方式简化,生产环境应从数据库加载):

properties 复制代码
# 授权服务器端口
server.port=8080
# 内存中注册的客户端
security.oauth2.client.client-id=myclient
security.oauth2.client.client-secret=mysecret
security.oauth2.client.authorized-grant-types=authorization_code,refresh_token,password,client_credentials
security.oauth2.client.scope=read,write
security.oauth2.client.redirect-uri=http://localhost:8080/login/oauth2/code/myclient
# 令牌签名密钥(演示用对称密钥)
security.oauth2.authorization.jwt.key-value=my-jwt-signing-key

授权服务器配置

java 复制代码
package com.example.oauth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
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;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 在内存中注册一个客户端
        clients.inMemory()
                .withClient("myclient")
                .secret(NoOpPasswordEncoder.getInstance().encode("mysecret")) // 生产环境请使用 BCrypt
                .authorizedGrantTypes("authorization_code", "refresh_token", "password", "client_credentials")
                .scopes("read", "write")
                .redirectUris("http://localhost:8080/login/oauth2/code/myclient")
                .autoApprove(true); // 演示时自动批准授权,实际可设为false
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // 使用默认端点即可,实际可注入 AuthenticationManager 等
    }
}

安全配置

为授权服务器提供用户身份认证(资源所有者)。这里创建一个内存用户。

java 复制代码
package com.example.oauth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user")
                .password("{noop}password")  // 明文密码,仅用于演示
                .roles("USER")
                .build());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();  // 提供表单登录页
    }
}

资源服务器与受保护资源

为了在同一进程中演示资源服务器,启用 @EnableResourceServer 并提供一个 REST 接口。

java 复制代码
package com.example.oauth2.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@Configuration
@EnableResourceServer
public class ResourceServerConfig {
}

创建受保护 API:

java 复制代码
package com.example.oauth2;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class UserController {

    @GetMapping("/user")
    public Principal user(Principal principal) {
        return principal; // 返回当前认证用户的身份信息
    }
}

启动类

java 复制代码
package com.example.oauth2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OAuth2Application {
    public static void main(String[] args) {
        SpringApplication.run(OAuth2Application.class, args);
    }
}

运行与验证

启动应用后,可以通过如下步骤体验授权码流程:

  1. 浏览器访问授权端点:
    http://localhost:8080/oauth/authorize?response_type=code&client_id=myclient&redirect_uri=http://localhost:8080/login/oauth2/code/myclient&scope=read
  2. 系统会跳转到登录页面,输入用户名 user、密码 password
  3. 登录后会展示授权页(本例已设置 autoApprove(true),因此自动批准),随后重定向到回调地址,并在 URL 中携带授权码。
  4. 使用 curl 或 Postman 换取令牌:
    POST http://localhost:8080/oauth/token
    Authorization: Basic myclient:mysecret
    Content-Type: application/x-www-form-urlencoded
    body: grant_type=authorization_code&code=<上一步获得的code>&redirect_uri=http://localhost:8080/login/oauth2/code/myclient
  5. 获取 access_token 后,携带令牌访问 /user
    GET http://localhost:8080/user
    Authorization: Bearer <access_token>

总结

本文梳理了 OAuth2 的四大角色及三种主要授权流程(授权码、密码、客户端凭证),并结合 Spring Security OAuth2 给出了一个可运行的授权码示例。

在实际项目中,授权服务器与资源服务器往往独立部署,且令牌通常采用 JWT 格式以实现无状态验证。

通过理解这些核心角色与流程,开发者可以灵活构建微服务间的安全通信,或为应用集成社交登录功能。

相关推荐
dong_junshuai1 小时前
每天一个开源项目#100 OpenCodeReview:2.6万星的低噪声AI审查器
程序员·开源·github
hai_android1 小时前
Kotlin Flow combine 源码剖析:一个 Channel 如何优雅合并多条流
android·java·kotlin
峰向AI1 小时前
还在用 Ableton?这个 3.5K Star 的免费音序器,让你在手机上写微分音音乐
github
caoerzhong1 小时前
JeeWMS 开源仓库管理系统权限与域验证解析:Java WMS 如何把数据权限管到仓、货主与人
java·python·开源·vue
君顾11 小时前
西安 24 小时自助健身房系统开发实战指南
java·开发语言·健身房
用户094248568031 小时前
第9章:OpenJDK堆分代与 Serial/Parallel GC 基础
java·jvm
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之RBAC与角色分层实践
java·网络·spring
caoerzhong2 小时前
JeeWMS 开源仓库管理系统多租户架构解析:一套 Java WMS 如何同时服务多个货主与多个仓库
java·架构·开源·vue
sunshine22 girl2 小时前
Java学习四 面向对象原理
java