Java框架快速入门: Spring Security+OAuth2之资源服务器访问与客户端集成

发布时间:2026/9/16 8:19:08
Java框架快速入门: Spring Security+OAuth2之资源服务器访问与客户端集成
纲要核心概念OAuth2授权模式密码模式password、授权码模式authorization_code、客户端凭证client_credentials、刷新令牌refresh_token角色划分授权服务器、资源服务器、客户端JWT与JWK Set URI验签机制Scope权限控制资源服务器访问流程获取access_token并放入请求头Authorization: Bearer token资源服务器通过JWK Set URI验证JWT并校验Scope客户端访问时的Scope不匹配将导致403 Insufficient Scope代码实践使用OkHttp发起密码模式请求获取令牌使用OkHttp携带令牌访问资源服务器拦截器设计统一添加认证头、自动刷新令牌进阶提示Spring Security提供的ServletOAuth2AuthorizedClientExchangeFilterFunction适用于WebFlux社会化登录与资源访问的区别OAuth2 授权模式概览Spring Security搭建的授权服务器内建支持四种标准流程授权码、密码、客户端凭证、刷新令牌。其中授权码流程安全性最高但交互步骤最复杂密码模式和客户端凭证模式仅需一次请求即可换取access_token适合内部系统或高信任度客户端。资源服务器访问无论采用哪种授权模式访问资源服务器的核心规则只有一条将access_token以Bearer格式附加在 HTTP 请求头的Authorization字段中。例如curl-HAuthorization: Bearer eyJhbGciOi...http://localhost:8082/todos只要令牌合法且scope满足资源要求请求就能正常返回数据。Scope 权限控制 —— 一次典型的 403 排查很多开发者在拿到合法的JWT后仍然收到403 Insufficient Scope问题就出在scope。假设授权服务器为不同客户端配置了不同的scopeadminclient拥有的scope为read writewebclient拥有的scope为todo read而资源服务器/todos端点要求scope必须包含todo。当你用adminclient申请令牌后去访问该端点就会因为scope不匹配而被拒绝。解决方法是为adminclient补上todo范围或改用webclient申请令牌。该机制体现了 OAuth2 细粒度的权限控制即使令牌本身有效仍可能因权限不足而失败。实战使用密码模式获取令牌并访问资源下面通过一个完整的 Java 测试类演示整个过程。代码使用OkHttp发起 HTTP 请求你也可以替换为RestTemplate或WebClient。前置依赖MavendependencygroupIdcom.squareup.okhttp3/groupIdartifactIdokhttp/artifactIdversion4.11.0/version/dependencydependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactId/dependency完整测试代码packagecom.example.oauth2.client;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importokhttp3.*;importjava.io.IOException;publicclassOAuth2ResourceAccessTest{privatestaticfinalStringTOKEN_URLhttp://localhost:8080/oauth/token;privatestaticfinalStringRESOURCE_URLhttp://localhost:8082/todos;privatestaticfinalStringCLIENT_IDwebclient;privatestaticfinalStringCLIENT_SECRETsecret;privatestaticfinalStringUSERNAMEuser;privatestaticfinalStringPASSWORDpassword;privatestaticfinalStringSCOPEtodo;privatefinalOkHttpClienthttpClientnewOkHttpClient();privatefinalObjectMapperobjectMappernewObjectMapper();publicstaticvoidmain(String[]args)throwsIOException{OAuth2ResourceAccessTesttestnewOAuth2ResourceAccessTest();StringaccessTokentest.obtainAccessToken();System.out.println(Access Token: accessToken);test.callResourceServer(accessToken);}// 密码模式获取令牌publicStringobtainAccessToken()throwsIOException{RequestBodyformBodynewFormBody.Builder().add(grant_type,password).add(username,USERNAME).add(password,PASSWORD).add(scope,SCOPE).add(client_id,CLIENT_ID).add(client_secret,CLIENT_SECRET).build();RequestrequestnewRequest.Builder().url(TOKEN_URL).post(formBody).header(Content-Type,application/x-www-form-urlencoded).build();try(ResponseresponsehttpClient.newCall(request).execute()){if(!response.isSuccessful()){thrownewIOException(获取令牌失败: response);}JsonNodejsonNodeobjectMapper.readTree(response.body().string());returnjsonNode.get(access_token).asText();}}// 携带令牌访问资源服务器publicvoidcallResourceServer(StringaccessToken)throwsIOException{RequestrequestnewRequest.Builder().url(RESOURCE_URL).get().header(Authorization,Bearer accessToken).build();try(ResponseresponsehttpClient.newCall(request).execute()){if(!response.isSuccessful()){thrownewIOException(资源访问失败: response);}StringresponseBodyresponse.body().string();System.out.println(资源响应: responseBody);}}}运行代码前请确保授权服务器和资源服务器均已启动。若使用password模式授权服务器必须开启对该模式的支持。拦截器设计统一处理认证与令牌刷新在实际项目中不应在每个请求里手动拼接Authorization头。更推荐的做法是实现一个 HTTP 客户端拦截器集中完成两件事为所有请求自动添加access_token。当收到401 Unauthorized响应时使用refresh_token刷新令牌并重试原请求。以下是一个基于OkHttp的拦截器骨架示例publicclassOAuth2InterceptorimplementsInterceptor{privateStringaccessToken;privateStringrefreshToken;privatefinalTokenSuppliertokenSupplier;// 自定义函数式接口用于刷新令牌publicOAuth2Interceptor(StringaccessToken,StringrefreshToken,TokenSuppliertokenSupplier){this.accessTokenaccessToken;this.refreshTokenrefreshToken;this.tokenSuppliertokenSupplier;}NotNullOverridepublicResponseintercept(NotNullChainchain)throwsIOException{RequestoriginalRequestchain.request();RequestrequestWithAuthoriginalRequest.newBuilder().header(Authorization,Bearer accessToken).build();Responseresponsechain.proceed(requestWithAuth);if(response.code()401refreshToken!null){// 同步刷新令牌生产环境建议使用同步锁避免并发刷新TokenPairnewTokenstokenSupplier.refresh(refreshToken);this.accessTokennewTokens.accessToken;this.refreshTokennewTokens.refreshToken;// 用新令牌重试RequestretryRequestoriginalRequest.newBuilder().header(Authorization,Bearer accessToken).build();response.close();returnchain.proceed(retryRequest);}returnresponse;}FunctionalInterfacepublicinterfaceTokenSupplier{TokenPairrefresh(StringrefreshToken)throwsIOException;}publicstaticclassTokenPair{publicfinalStringaccessToken;publicfinalStringrefreshToken;publicTokenPair(StringaccessToken,StringrefreshToken){this.accessTokenaccessToken;this.refreshTokenrefreshToken;}}}将该拦截器加入OkHttpClient后业务代码完全无感令牌刷新逻辑也被透明封装。Spring Security 原生客户端支持Spring Security提供了oauth2Login()实现社交登录也能通过Spring Security OAuth2 Client模块管理令牌。对于WebFlux技术栈框架还提供了一个ServletOAuth2AuthorizedClientExchangeFilterFunction实际类位于 reactive 包中它本质上就是一个内置了令牌添加和刷新功能的拦截器。但由于该方案强依赖于WebFlux和函数式编程且当前生产环境普及度有限多数团队仍倾向于使用成熟的 HTTP 客户端如OkHttp、Apache HttpClient自行封装拦截层。项目结构参考典型的 OAuth2 实验项目通常包含多个独立服务oauth2-demo/ ├── auth-server/ # 授权服务器端口 8080 │ ├── src/main/java/... │ └── pom.xml ├── resource-server/ # 资源服务器端口 8082 │ ├── src/main/java/... │ └── pom.xml └── client-app/ # 客户端应用含测试用例 ├── src/test/java/... └── pom.xml核心流程时序图资源服务器授权服务器客户端资源服务器授权服务器客户端POST /oauth/token (grant_typepasswordusername...scopetodo)200 OK {access_token, refresh_token, scope}GET /todos (Header: Authorization: Bearer access_token)校验 JWT 签名 (JWK Set URI)校验 scope 是否包含 todo200 OK [资源数据]总结资源服务器访问的本质就是携带合法的Bearer令牌看似简单但背后涉及scope校验、JWT验签以及客户端的令牌管理。通过理解拦截器设计模式开发者可以将认证逻辑与业务代码彻底分离提升系统的可维护性。无论是直接采用Spring Security的oauth2Login还是自定义拦截器都应根据项目实际需求和技术栈做出选择。