【实战指南】Spring Authorization Server 三端联调:从零构建认证、资源与客户端闭环

张开发
2026/4/18 20:21:37 15 分钟阅读

分享文章

【实战指南】Spring Authorization Server 三端联调:从零构建认证、资源与客户端闭环
1. 为什么需要三端联调在构建现代分布式系统时认证授权是绕不开的话题。想象一下你开发了一个电商平台用户需要登录后才能查看订单而订单数据可能存储在另一个服务中。这就是典型的**认证Authentication和授权Authorization**分离的场景。Spring Authorization Server 作为 OAuth 2.1 规范的实现完美解决了这个问题。但很多开发者第一次接触时会困惑为什么需要 Server、Resource、Client 三个角色简单来说授权服务器Server负责颁发令牌Token就像公安局发放身份证资源服务器Resource保管实际数据需要验明令牌真伪就像银行要核对身份证客户端Client代表用户获取令牌并访问资源就像我们拿着身份证去办事三端联调的难点在于每个环节都有特定的协议要求和配置细节。我曾经在项目初期因为漏配一个 redirect_uri调试了整整两天。下面我们就用最新 Spring Authorization Server 1.1.0 版本手把手搭建完整闭环。2. 环境准备与项目初始化2.1 技术栈选型推荐使用这套经过生产验证的组合JDK 17LTS 长期支持版Spring Boot 3.2.0Spring Authorization Server 1.1.0MySQL 8.0持久化客户端配置Lombok简化代码实测发现Spring Authorization Server 对 JDK 11 有更好的支持特别是 JWT 处理部分。2.2 初始化父工程用 IDEA 创建 Maven 父工程关键 pom.xml 配置dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.2.0/version typepom/type scopeimport/scope /dependency !-- 授权服务器核心依赖 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-authorization-server/artifactId version1.1.0/version /dependency /dependencies /dependencyManagement2.3 创建子模块建立三个子模块端口规划如下oauth2-server9000端口oauth2-resource8001端口oauth2-client8000端口建议用端口区分服务避免本地调试时冲突。我在实际项目中曾因为端口重复导致诡异的 404 错误。3. 授权服务器深度配置3.1 数据库准备创建 oauth2_registered_client 等三张表官方提供DDL。这里有个坑MySQL 8.0 需要调整字段类型-- 修改 blob 为 text 避免编码问题 ALTER TABLE oauth2_authorization MODIFY attributes text, MODIFY authorization_code_value text;3.2 安全配置双保险授权服务器需要两个安全链// 认证安全链 Bean Order(1) SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher(/login**, /error**) .authorizeHttpRequests(auth - auth.anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); } // 授权安全链 Bean Order(2) SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher(/oauth2/**) .authorizeHttpRequests(auth - auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 - oauth2.jwt(Customizer.withDefaults())); return http.build(); }3.3 客户端注册实战推荐使用 JDBC 存储客户端配置Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { RegisteredClient client RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(web-app) .clientSecret({bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG) // 密码加密存储 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri(http://127.0.0.1:8000/login/oauth2/code/web-app) .scope(read) .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofHours(1)) .refreshTokenTimeToLive(Duration.ofDays(30)) .build()) .build(); JdbcRegisteredClientRepository clientRepository new JdbcRegisteredClientRepository(jdbcTemplate); clientRepository.save(client); return clientRepository; }踩坑提醒redirectUri 必须与客户端配置完全匹配包括末尾的斜杠。我曾因为 http://127.0.0.1:8000 和 http://127.0.0.1:8000/ 的差异浪费半天时间。4. 资源服务器关键实现4.1 JWT 解码配置资源服务器需要验证令牌签名Bean JwtDecoder jwtDecoder() throws Exception { // 从类路径加载公钥 Resource resource new ClassPathResource(public-key.pem); String publicKey new String(resource.getInputStream().readAllBytes()); RSAPublicKey key (RSAPublicKey) KeyFactory.getInstance(RSA) .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))); return NimbusJwtDecoder.withPublicKey(key).build(); }4.2 精细化权限控制基于 Scope 的资源权限管理Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/api/orders/**).hasAuthority(SCOPE_read) .requestMatchers(/api/admin/**).hasAuthority(SCOPE_write) .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 - oauth2 .jwt(jwt - jwt .decoder(jwtDecoder()) .jwtAuthenticationConverter(jwtAuthConverter()) ) ); return http.build(); }4.3 自定义权限转换处理 JWT 中的 claims 到权限的转换private ConverterJwt, ? extends AbstractAuthenticationToken jwtAuthConverter() { JwtAuthenticationConverter converter new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt - { CollectionString scopes jwt.getClaim(scope); return scopes.stream() .map(scope - new SimpleGrantedAuthority(SCOPE_ scope)) .collect(Collectors.toList()); }); return converter; }5. 客户端集成技巧5.1 OAuth2 Client 自动配置application.yml 关键配置spring: security: oauth2: client: registration: web-app: client-id: web-app client-secret: secret authorization-grant-type: authorization_code redirect-uri: {baseUrl}/login/oauth2/code/{registrationId} scope: read provider: web-app: issuer-uri: http://localhost:90005.2 安全链配置Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/, /login**).permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 - oauth2 .loginPage(/login) .defaultSuccessUrl(/home, true) ) .logout(logout - logout .logoutSuccessUrl(http://localhost:9000/logout) .permitAll() ); return http.build(); }5.3 获取用户信息在 Controller 中注入认证信息GetMapping(/user) public String userInfo(RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client) { OAuth2AccessToken accessToken client.getAccessToken(); Authentication auth SecurityContextHolder.getContext().getAuthentication(); return 用户名: auth.getName() br权限: auth.getAuthorities() brToken: accessToken.getTokenValue(); }6. 联调实战与排错指南6.1 完整流程测试访问客户端 http://localhost:8000点击登录跳转到授权服务器完成认证后携带 code 回跳客户端客户端用 code 换取 token使用 token 访问资源接口6.2 常见问题排查问题1redirect_uri 不匹配现象Error: invalid_request, Invalid redirect_uri解决检查客户端注册的 redirect_uri 必须完全匹配包括大小写和末尾斜杠问题2JWT 验签失败现象Invalid JWT signature解决确保资源服务器使用与授权服务器匹配的公钥问题3跨域问题现象CORS policy 阻止请求解决在授权服务器配置 CorsFilterBean CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); source.registerCorsConfiguration(/oauth2/**, config); return new CorsFilter(source); }7. 进阶技巧与生产建议7.1 令牌增强实践在授权服务器添加自定义 claimsBean OAuth2TokenCustomizerJwtEncodingContext tokenCustomizer() { return context - { if (context.getTokenType() OAuth2TokenType.ACCESS_TOKEN) { Authentication auth context.getPrincipal(); context.getClaims().claim(tenant_id, T001); } }; }7.2 资源服务器间调用当 Resource A 需要调用 Resource B 时使用客户端凭证模式Bean WebClient webClient(OAuth2AuthorizedClientManager clientManager) { ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager); oauth2.setDefaultClientRegistrationId(resource-server); return WebClient.builder().apply(oauth2.oauth2Configuration()).build(); }7.3 性能优化建议使用 Redis 缓存 JWT 解码结果对/public 接口配置 SecurityFilterChain 跳过 JWT 验证启用 HTTP/2 提升握手效率在微服务架构下良好的认证授权设计是系统安全的基石。经过三端联调实践后建议进一步探索基于 JWT 的无状态会话管理细粒度的权限控制策略与 API 网关的集成方案

更多文章