OAuth 2.0 安全授权
一、什么是 OAuth 2.0?
OAuth 2.0 是一个授权框架,它允许第三方应用程序在获得用户授权后,代表用户访问该用户在另一个服务提供商上存储的资源,而无需分享用户的密码。
- 重要区别:OAuth 2.0 是关于授权,而非认证。
- 认证:是确认“你是谁”的过程。(例如:用账号密码登录)
- 授权:是确认“你被允许做什么”的过程。(例如:授权一个App来管理你的QQ邮箱)
 
二、为什么需要 OAuth 2.0?
在没有 OAuth 的时代,如果你想用一个第三方应用(比如一个在线邮件管理工具)来管理你的多个邮箱,你不得不把邮箱的账号密码交给这个第三方应用。这会带来巨大的风险:
- 密码泄露:第三方应用可能不安全,或者会滥用你的密码。
- 权限过大:应用不仅获得了读取邮件的权限,还可能获得删除邮件、发送邮件等所有权限。
- 难以撤销:一旦密码给出,除非修改密码,否则无法单独撤销该应用的访问权。
OAuth 2.0 通过引入一个访问令牌 完美地解决了这些问题。
三、OAuth 2.0 的核心角色
在一个 OAuth 2.0 流程中,通常涉及四个角色:
- 资源所有者: 拥有受保护资源的用户(就是你)。
- 客户端: 想要访问用户资源的第三方应用程序(例如,一个想访问你微信好友列表的社交App)。
- 授权服务器: 服务提供商用来处理认证和授权的服务器。它负责在用户授权后,向客户端颁发访问令牌。
- 资源服务器: 存放用户受保护资源的API服务器。客户端使用访问令牌来访问资源服务器。
四、OAuth2.0 四种模式
基于不同的使用场景,OAuth2.0设计了四种模式来获取访问令牌。Spring Security OAuth2提供了内置的端点,四种授权模式分别使用不同的端点和参数,我们在客户端配置中指定允许的授权类型,并在端点配置中设置必要的组件(如authenticationManager)来支持密码模式。
- 授权端点(/oauth/authorize):用于处理授权请求。
- 令牌端点(/oauth/token):用于颁发访问令牌。
- 令牌验证端点(/oauth/check_token):用于验证令牌有效性。
- 令牌吊销端点(/oauth/revoke_token):用于吊销令牌。
- JWK端点( /oauth/token_key):获取JWT公钥。
4.1 授权码模式
这是最常用、最安全的模式,特别是用于有后端服务器的 Web 应用程序和移动应用程序。
- 
浏览器访问示例: - response_type=code
 http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://localhost:8081/callback&scope=all
- 
流程: - 第一步:用户访问授权端点
- 客户端将用户重定向到/oauth/authorize,参数包括response_type=code、client_id、redirect_uri、scope等。
 
- 第二步:用户认证并授权
- 用户登录并授权,授权服务器重定向到redirect_uri并带上授权码。
 
- 第三步:用授权码交换访问令牌
- 客户端使用授权码向/oauth/token请求访问令牌,参数包括grant_type=authorization_code、code、redirect_uri等,并且需要客户端认证(通过HTTP Basic认证或参数传递client_id和client_secret)。
- 换取访问令牌的请求:curl -X POST \http://localhost:8080/oauth/token \-H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \ # client1:123456的Base64编码-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=authorization_code&code=授权码&redirect_uri=http://localhost:8081/callback'
 
 
- 第一步:用户访问授权端点
- 
特点: - 安全性高:关键的访问令牌不会通过浏览器暴露,只有授权码在前端传递,而授权码本身是短暂的且无法直接访问资源。
- 支持刷新令牌:可以获取刷新令牌,用于在访问令牌过期后获取新的访问令牌,无需用户再次登录。
- 适用场景:有后端的传统 Web 应用、移动 App(使用 PKCE 扩展增强安全)。
 
4.2 隐式模式
这是一种简化模式,访问令牌直接在浏览器的重定向 URI 的片段(#后面)中返回。该模式在现代 OAuth 2.1 标准中已被废弃,不推荐使用。
- 
浏览器访问示例: - response_type=token
 http://localhost:8080/oauth/authorize?response_type=token&client_id=client1&redirect_uri=http://localhost:8081/callback&scope=all
- 
流程 - 第一步:用户访问授权端点
- 客户端将用户重定向到/oauth/authorize,参数包括response_type=token、client_id、redirect_uri、scope等。
 
- 第二步:用户认证并授权
- 用户登录并授权,授权服务器重定向到redirect_uri并在URL的hash片段中直接返回访问令牌。
 
 
- 第一步:用户访问授权端点
- 
特点: - 安全性低:访问令牌直接在浏览器地址栏中暴露,有被截获的风险。
- 不支持刷新令牌:由于安全性考虑,不会返回刷新令牌。
- 适用场景:过去用于纯前端 JavaScript 应用(单页应用 SPA),但现在已被授权码模式 + PKCE 完全取代。
 
4.3 密码模式
用户将其用户名和密码直接交给客户端应用,客户端应用再用这些信息向授权服务器申请令牌。
- 
浏览器访问示例: - grant_type=password
 curl -X POST \http://localhost:8080/oauth/token \-H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=password&username=user&password=123456&scope=all'
- 
流程: - 用户直接访问令牌端点
- 客户端直接向/oauth/token发送请求,参数包括grant_type=password、username、password、scope,并且需要客户端认证。
 
 
- 用户直接访问令牌端点
- 
特点: - 信任度要求极高:客户端应用会直接接触到用户的明文密码,这违背了 OAuth 的初衷(不暴露密码)。只在绝对信任的情况下使用,例如同一个公司内部的官方应用。
- 安全性风险:如果客户端应用被入侵,用户密码将泄露。
- 适用场景:第一方的高信任度应用,或者遗留系统迁移到 OAuth 的过渡方案。
 
4.4 客户端凭证模式
这种模式不涉及用户,是机器对机器的通信,用于客户端应用访问其自身拥有的资源,而不是代表某个用户。
- 
浏览器访问示例: - grant_type=client_credentials
 curl -X POST \http://localhost:8080/oauth/token \-H 'Authorization: Basic Y2xpZW50MToxMjM0NTY=' \-H 'Content-Type: application/x-www-form-urlencoded' \-d 'grant_type=client_credentials&scope=all'
- 
流程: - 用户直接访问令牌端点
- 客户端向/oauth/token发送请求,参数包括grant_type=client_credentials、scope,并且需要客户端认证。
 
 
- 用户直接访问令牌端点
- 
特点: - 无用户参与:整个流程没有用户授权环节。
- 访问自有资源:获取的令牌用于访问该客户端自己控制的资源(例如,一个后台服务调用另一个 API 来获取系统级别的数据)。
- 适用场景:微服务之间的 API 调用、后台作业、服务器与服务器之间的通信。
 
五、搭建 OAuth2-Server
这里使用Spring Boot 2.x 和 Spring Security OAuth2 来搭建一个 OAuth2 Server 认证中心。主要步骤:
- 创建Spring Boot项目,添加依赖。
- 配置授权服务器。
- 配置资源服务器(如果需要保护资源)。
- 配置安全规则。
- 测试授权并访问保护资源。
步骤1:创建Spring Boot项目
使用Spring Initializr 创建一个项目,并在pom.xml中添加依赖:
<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.1.0.RELEASE</version></dependency>
</dependencies>
步骤2:配置授权服务器
创建一个授权服务器配置类,使用@EnableAuthorizationServer注解。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
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.AuthorizationServerSecurityConfigurer;@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.tokenKeyAccess("permitAll()")   // 公开/oauth/token_key端点.checkTokenAccess("isAuthenticated()") // 认证后可访问/oauth/check_token端点.allowFormAuthenticationForClients(); // 允许客户端使用表单认证}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client1") // 客户端ID.secret(passwordEncoder.encode("123456")) // 客户端密钥.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit") // 授权模式.scopes("all") // 授权范围.redirectUris("http://localhost:8080/callback") // 重定向URI,用于授权码模式和简化模式.autoApprove(true) // 自动批准,跳过授权确认页面.and().withClient("client2").secret(passwordEncoder.encode("123456")).authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit").scopes("all").redirectUris("http://localhost:8080/callback").autoApprove(true);}
}
步骤3:配置资源服务器
创建一个资源服务器配置类,使用@EnableResourceServer注解。
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 {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/public/**").permitAll() // 公开访问.anyRequest().authenticated(); // 其他请求需要认证}
}
步骤4:配置安全规则
我们需要配置一个密码编码器,并设置基本的安全规则。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("123456")).roles("USER");}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/oauth/**").permitAll() // OAuth2端点允许访问.anyRequest().authenticated().and().formLogin().permitAll();}
}
步骤5:测试授权并访问保护资源
- 
创建一个测试控制器 import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;@RestController public class TestController {@GetMapping("/public/hello")public String publicHello() {return "Hello Public";}@GetMapping("/private/hello")public String privateHello() {return "Hello Private";} }
- 
获取访问令牌 
 这里使用授权码模式来获取访问令牌:- 在浏览器中访问:http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://localhost:8081/callback&scope=read
- 输入用户名和密码(user1/password1)进行登录。
- 授权服务器会重定向到http://localhost:8081/callback?code=授权码。
- 使用这个授权码,通过POST请求到/oauth/token获取访问令牌。
 
- 在浏览器中访问:
- 
使用获取到的访问令牌访问受保护资源: curl -X GET \http://localhost:8080/private/hello \-H 'Authorization: Bearer <access_token>'
