如何将Spring Cloud Gateway与OAuth2模式有效结合使用?

2026-05-23 22:381阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2367个文字,预计阅读时间需要10分钟。

如何将Spring Cloud Gateway与OAuth2模式有效结合使用?

Spring Cloud Gateway 是一个构建在 Spring 框架之上的 API 网关。它基于 Spring Boot 2.x、Spring WebFlux 和 Project Reactor 构建。使用 Spring Cloud Gateway 可以方便地管理请求路由。

概述

Spring Cloud Gateway是一个构建在 Spring 生态之上的 API Gateway。 建立在​​Spring Boot 2.x​​、​​Spring WebFlux​​和​​Project Reactor​​之上。

本节中您将使用Spring Cloud Gateway将请求路由到Servlet API服务。

本文您将学到:

  • OpenID Connect 身份验证 - 用于用户身份验证
  • 令牌中继 - Spring Cloud Gateway API网关充当客户端将令牌转发到资源请求上

先决条件:

  • Java 8+
  • MySQL
  • Redis

OpenID Connect身份验证

OpenID Connect 定义了一种基于 OAuth2 授权代码流的最终用户身份验证机制。下图是Spring Cloud Gateway与授权服务进行身份验证完整流程,为了清楚起见,其中一些参数已被省略。

创建授权服务

本节中我们将使用​​Spring Authorization Server​​构建授权服务,支持OAuth2协议与OpenID Connect协议。同时我们还将使用RBAC0基本权限模型控制访问权限。并且该授权服务同时作为OAuth2客户端支持Github第三方登录。

相关数据库表结构

我们创建了基本RBAC0权限模型用于本文示例讲解,并提供了OAuth2授权服务持久化存储所需表结构和OAuth2客户端持久化存储所需表结构。通过oauth2_client_role定义外部系统角色与本平台角色映射关系。涉及相关创建表及初始化数据的SQL语句可以​​从这里​​获取。

角色说明

本节中授权服务默认提供两个角色,以下是角色属性及访问权限:

read

write

ROLE_ADMIN

ROLE_OPERATION

Maven依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>0.3.1</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.3</version></dependency>

配置

首先我们从application.yml配置开始,这里我们指定了端口号与MySQL连接配置:

server: port: 8080spring: datasource: druid: db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2server?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: <<username>> # 修改用户名 password: <<password>> # 修改密码

接下来我们将创建​​AuthorizationServerConfig​​,用于配置OAuth2及OIDC所需Bean,首先我们将新增OAuth2客户端信息,并持久化到数据库:

@Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { RegisteredClient registeredClient = RegisteredClient.withId("relive-messaging-oidc") .clientId("relive-client") .clientSecret("{noop}relive-client") .clientAuthenticationMethods(s -> { s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST); s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); }) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("127.0.0.1:8070/login/oauth2/code/messaging-gateway-oidc") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope(OidcScopes.EMAIL) .scope("read") .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(false) //不需要授权同意 .requireProofKey(false) .build()) .tokenSettings(TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 生成JWT令牌 .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))//accessTokenTimeToLive:access_token有效期 .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))//refreshTokenTimeToLive:refresh_token有效期 .reuseRefreshTokens(true) .build()) .build(); JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); registeredClientRepository.save(registeredClient); return registeredClientRepository; }

其次我们将创建授权过程中所需持久化容器类:

@Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); } @Bean public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); }

授权服务器需要其用于令牌的签名密钥,让我们生成一个 2048 字节的 RSA 密钥:

@Beanpublic JWKSource<SecurityContext> jwkSource() { RSAKey rsaKey = Jwks.generateRsa(); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);}static class Jwks { private Jwks() { } public static RSAKey generateRsa() { KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); return new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); }}static class KeyGeneratorUtils { private KeyGeneratorUtils() { } static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; }}

接下来我们将创建用于OAuth2授权的​​SecurityFilterChain​​,SecurityFilterChain是Spring Security提供的过滤器链,Spring Security的认证授权功能都是通过滤器完成:

@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity github.com/login/oauth/authorize") .tokenUri("github.com/login/oauth/access_token") .userInfoUri("api.github.com/user") .userNameAttributeName("login") .clientName("GitHub").build(); jdbcClientRegistrationRepository.save(clientRegistration); return jdbcClientRegistrationRepository; }

接下来我们将实例化OAuth2AuthorizedClientService和OAuth2AuthorizedClientRepository:

  • OAuth2AuthorizedClientService:负责OAuth2AuthorizedClient在 Web 请求之间进行持久化。
  • OAuth2AuthorizedClientRepository:用于在请求之间保存和持久化授权客户端。
@Bean OAuth2AuthorizedClientService authorizedClientService( JdbcTemplate jdbcTemplate, ClientRegistrationRepository clientRegistrationRepository) { return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository); } @Bean OAuth2AuthorizedClientRepository authorizedClientRepository( OAuth2AuthorizedClientService authorizedClientService) { return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); }

对于每个使用Github登录的用户,我们都要分配平台的角色以控制他们可以访问哪些资源,在此我们将新建AuthorityMappingOAuth2UserService类授予用户角色:

@RequiredArgsConstructorpublic class AuthorityMappingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { private DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { DefaultOAuth2User oAuth2User = (DefaultOAuth2User) delegate.loadUser(userRequest); Map<String, Object> additionalParameters = userRequest.getAdditionalParameters(); Set<String> role = new HashSet<>(); if (additionalParameters.containsKey("authority")) { role.addAll((Collection<? extends String>) additionalParameters.get("authority")); } if (additionalParameters.containsKey("role")) { role.addAll((Collection<? extends String>) additionalParameters.get("role")); } Set<SimpleGrantedAuthority> mappedAuthorities = role.stream() .map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r)) .map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); //当没有指定客户端角色,则默认赋予最小权限ROLE_OPERATION if (CollectionUtils.isEmpty(mappedAuthorities)) { mappedAuthorities = new HashSet<>( Collections.singletonList(new SimpleGrantedAuthority("ROLE_OPERATION"))); } String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); return new DefaultOAuth2User(mappedAuthorities, oAuth2User.getAttributes(), userNameAttributeName); }}

我们可以看到从​​authority​​和​​role​​属性中获取权限信息,在通过OAuth2ClientRoleRepository查找映射到本平台的角色属性。

注意:​​authority​​和​​role​​是由平台自定义属性,与OAuth2协议与Open ID Connect 协议无关,在生产环境中你可以与外部系统协商约定一个属性来传递权限信息。

OAuth2ClientRoleRepository为​​oauth2_client_role​​表持久层容器类,由JPA实现。

对于未获取到预先定义的映射角色信息,我们将赋予默认​​ROLE_OPERATION​​最小权限角色。而在本示例中GitHub登录的用户来说,也将被赋予​​ROLE_OPERATION​​角色。

针对GitHub认证成功并且首次登录的用户我们将获取用户信息并持久化到​​user​​表中,这里我们实现AuthenticationSuccessHandler并增加持久化用户逻辑:

public final class SavedUserAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); private Consumer<OAuth2User> oauth2UserHandler = (user) -> { }; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (authentication instanceof OAuth2AuthenticationToken) { if (authentication.getPrincipal() instanceof OAuth2User) { this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal()); } } this.delegate.onAuthenticationSuccess(request, response, authentication); } public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) { this.oauth2UserHandler = oauth2UserHandler; }}

我们将通过setOauth2UserHandler(Consumer oauth2UserHandler)方法将UserRepositoryOAuth2UserHandler注入到SavedUserAuthenticationSuccessHandler中,UserRepositoryOAuth2UserHandler定义了具体持久层操作:

@Component@RequiredArgsConstructorpublic final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> { private final UserRepository userRepository; private final RoleRepository roleRepository; @Override public void accept(OAuth2User oAuth2User) { DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) oAuth2User; if (this.userRepository.findUserByUsername(oAuth2User.getName()) == null) { User user = new User(); user.setUsername(defaultOAuth2User.getName()); Role role = roleRepository.findByRoleCode(defaultOAuth2User.getAuthorities() .stream().map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION")); user.setRoleList(Arrays.asList(role)); userRepository.save(user); } }}

我们通过defaultOAuth2User.getAuthorities()获取到映射后的角色信息,并将其与用户信息存储到数据库中。

UserRepository和RoleRepository为持久化容器类。

最后我们向SecurityFilterChain加入OAuth2 Login配置:

@Autowired UserRepositoryOAuth2UserHandler userHandler; @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity 127.0.0.1:8090 predicates: Path=/resource/** filters: - TokenRelay

TokenRelay 过滤器将提取存储在用户会话中的访问令牌,并将其作为​​Authorization​​标头添加到传出请求中。这允许下游服务对请求进行身份验证。

我们将在application.yml中添加OAuth2客户端信息:

spring: security: oauth2: client: registration: messaging-gateway-oidc: provider: gateway-client-provider client-id: relive-client client-secret: relive-client authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: - openid - profile client-name: messaging-gateway-oidc provider: gateway-client-provider: authorization-uri: 127.0.0.1:8080/oauth2/authorize token-uri: 127.0.0.1:8080/oauth2/token jwk-set-uri: 127.0.0.1:8080/oauth2/jwks user-info-uri: 127.0.0.1:8080/userinfo user-name-attribute: sub

OpenID Connect 使用一个特殊的权限范围值 openid 来控制对 UserInfo 端点的访问,其他信息与上节中授权服务注册客户端信息参数保持一致。

我们通过Spring Security拦截未认证请求到授权服务器进行认证。为了简单起见,​​CSRF​​被禁用。

如何将Spring Cloud Gateway与OAuth2模式有效结合使用?

@Configuration(proxyBeanMethods = false)@EnableWebFluxSecuritypublic class OAuth2LoginSecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity 127.0.0.1:8080 jwk-set-uri: 127.0.0.1:8080/oauth2/jwks server: port: 8090

创建​​ResourceServerConfig​​类来配置Spring Security安全模块,​​@EnableMethodSecurity​​注解来启用基于注解的安全性:

@Configuration(proxyBeanMethods = false)@EnableWebSecurity@EnableMethodSecuritypublic class ResourceServerConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity 127.0.0.1:8070/resource/article/read​​,我们将重定向到授权服务登录页,如图所示:

在我们输入用户名密码(admin/password)后,将获取到请求响应信息:

admin用户所属角色是​​ROLE_ADMIN​​,所以我们尝试请求​​127.0.0.1:8070/resource/article/write?name=article3​​

注销登录后,我们同样访问​​127.0.0.1:8070/resource/article/read​​,不过这次使用Github登录,响应信息如图所示:

可以看到响应信息中用户已经切换为你的Github用户名。

Github登录的用户默认赋予角色为​​ROLE_OPERATION​​,而​​ROLE_OPERATION​​是没有​​127.0.0.1:8070/resource/article/write?name=article3​​访问权限,我们来尝试测试下:

结果我们请求被拒绝,403状态码提示我们没有访问权限。

结论

本文中您了解到如何使用Spring Cloud Gateway结合OAuth2保护微服务。在示例中浏览器cookie仅存储sessionId,JWT访问令牌并没有暴露给浏览器,而是在内部服务中流转。这样我们体验到了JWT带来的优势,也同样利用cookie-session弥补了JWT的不足,例如当我们需要实现强制用户登出功能。

本文共计2367个文字,预计阅读时间需要10分钟。

如何将Spring Cloud Gateway与OAuth2模式有效结合使用?

Spring Cloud Gateway 是一个构建在 Spring 框架之上的 API 网关。它基于 Spring Boot 2.x、Spring WebFlux 和 Project Reactor 构建。使用 Spring Cloud Gateway 可以方便地管理请求路由。

概述

Spring Cloud Gateway是一个构建在 Spring 生态之上的 API Gateway。 建立在​​Spring Boot 2.x​​、​​Spring WebFlux​​和​​Project Reactor​​之上。

本节中您将使用Spring Cloud Gateway将请求路由到Servlet API服务。

本文您将学到:

  • OpenID Connect 身份验证 - 用于用户身份验证
  • 令牌中继 - Spring Cloud Gateway API网关充当客户端将令牌转发到资源请求上

先决条件:

  • Java 8+
  • MySQL
  • Redis

OpenID Connect身份验证

OpenID Connect 定义了一种基于 OAuth2 授权代码流的最终用户身份验证机制。下图是Spring Cloud Gateway与授权服务进行身份验证完整流程,为了清楚起见,其中一些参数已被省略。

创建授权服务

本节中我们将使用​​Spring Authorization Server​​构建授权服务,支持OAuth2协议与OpenID Connect协议。同时我们还将使用RBAC0基本权限模型控制访问权限。并且该授权服务同时作为OAuth2客户端支持Github第三方登录。

相关数据库表结构

我们创建了基本RBAC0权限模型用于本文示例讲解,并提供了OAuth2授权服务持久化存储所需表结构和OAuth2客户端持久化存储所需表结构。通过oauth2_client_role定义外部系统角色与本平台角色映射关系。涉及相关创建表及初始化数据的SQL语句可以​​从这里​​获取。

角色说明

本节中授权服务默认提供两个角色,以下是角色属性及访问权限:

read

write

ROLE_ADMIN

ROLE_OPERATION

Maven依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>0.3.1</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>2.6.7</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.3</version></dependency>

配置

首先我们从application.yml配置开始,这里我们指定了端口号与MySQL连接配置:

server: port: 8080spring: datasource: druid: db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2server?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: <<username>> # 修改用户名 password: <<password>> # 修改密码

接下来我们将创建​​AuthorizationServerConfig​​,用于配置OAuth2及OIDC所需Bean,首先我们将新增OAuth2客户端信息,并持久化到数据库:

@Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { RegisteredClient registeredClient = RegisteredClient.withId("relive-messaging-oidc") .clientId("relive-client") .clientSecret("{noop}relive-client") .clientAuthenticationMethods(s -> { s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST); s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); }) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("127.0.0.1:8070/login/oauth2/code/messaging-gateway-oidc") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope(OidcScopes.EMAIL) .scope("read") .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(false) //不需要授权同意 .requireProofKey(false) .build()) .tokenSettings(TokenSettings.builder() .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 生成JWT令牌 .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256) .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))//accessTokenTimeToLive:access_token有效期 .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))//refreshTokenTimeToLive:refresh_token有效期 .reuseRefreshTokens(true) .build()) .build(); JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); registeredClientRepository.save(registeredClient); return registeredClientRepository; }

其次我们将创建授权过程中所需持久化容器类:

@Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); } @Bean public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); }

授权服务器需要其用于令牌的签名密钥,让我们生成一个 2048 字节的 RSA 密钥:

@Beanpublic JWKSource<SecurityContext> jwkSource() { RSAKey rsaKey = Jwks.generateRsa(); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);}static class Jwks { private Jwks() { } public static RSAKey generateRsa() { KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); return new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); }}static class KeyGeneratorUtils { private KeyGeneratorUtils() { } static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; }}

接下来我们将创建用于OAuth2授权的​​SecurityFilterChain​​,SecurityFilterChain是Spring Security提供的过滤器链,Spring Security的认证授权功能都是通过滤器完成:

@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity github.com/login/oauth/authorize") .tokenUri("github.com/login/oauth/access_token") .userInfoUri("api.github.com/user") .userNameAttributeName("login") .clientName("GitHub").build(); jdbcClientRegistrationRepository.save(clientRegistration); return jdbcClientRegistrationRepository; }

接下来我们将实例化OAuth2AuthorizedClientService和OAuth2AuthorizedClientRepository:

  • OAuth2AuthorizedClientService:负责OAuth2AuthorizedClient在 Web 请求之间进行持久化。
  • OAuth2AuthorizedClientRepository:用于在请求之间保存和持久化授权客户端。
@Bean OAuth2AuthorizedClientService authorizedClientService( JdbcTemplate jdbcTemplate, ClientRegistrationRepository clientRegistrationRepository) { return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository); } @Bean OAuth2AuthorizedClientRepository authorizedClientRepository( OAuth2AuthorizedClientService authorizedClientService) { return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); }

对于每个使用Github登录的用户,我们都要分配平台的角色以控制他们可以访问哪些资源,在此我们将新建AuthorityMappingOAuth2UserService类授予用户角色:

@RequiredArgsConstructorpublic class AuthorityMappingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { private DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); private final OAuth2ClientRoleRepository oAuth2ClientRoleRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { DefaultOAuth2User oAuth2User = (DefaultOAuth2User) delegate.loadUser(userRequest); Map<String, Object> additionalParameters = userRequest.getAdditionalParameters(); Set<String> role = new HashSet<>(); if (additionalParameters.containsKey("authority")) { role.addAll((Collection<? extends String>) additionalParameters.get("authority")); } if (additionalParameters.containsKey("role")) { role.addAll((Collection<? extends String>) additionalParameters.get("role")); } Set<SimpleGrantedAuthority> mappedAuthorities = role.stream() .map(r -> oAuth2ClientRoleRepository.findByClientRegistrationIdAndRoleCode(userRequest.getClientRegistration().getRegistrationId(), r)) .map(OAuth2ClientRole::getRole).map(Role::getRoleCode).map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); //当没有指定客户端角色,则默认赋予最小权限ROLE_OPERATION if (CollectionUtils.isEmpty(mappedAuthorities)) { mappedAuthorities = new HashSet<>( Collections.singletonList(new SimpleGrantedAuthority("ROLE_OPERATION"))); } String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); return new DefaultOAuth2User(mappedAuthorities, oAuth2User.getAttributes(), userNameAttributeName); }}

我们可以看到从​​authority​​和​​role​​属性中获取权限信息,在通过OAuth2ClientRoleRepository查找映射到本平台的角色属性。

注意:​​authority​​和​​role​​是由平台自定义属性,与OAuth2协议与Open ID Connect 协议无关,在生产环境中你可以与外部系统协商约定一个属性来传递权限信息。

OAuth2ClientRoleRepository为​​oauth2_client_role​​表持久层容器类,由JPA实现。

对于未获取到预先定义的映射角色信息,我们将赋予默认​​ROLE_OPERATION​​最小权限角色。而在本示例中GitHub登录的用户来说,也将被赋予​​ROLE_OPERATION​​角色。

针对GitHub认证成功并且首次登录的用户我们将获取用户信息并持久化到​​user​​表中,这里我们实现AuthenticationSuccessHandler并增加持久化用户逻辑:

public final class SavedUserAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); private Consumer<OAuth2User> oauth2UserHandler = (user) -> { }; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (authentication instanceof OAuth2AuthenticationToken) { if (authentication.getPrincipal() instanceof OAuth2User) { this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal()); } } this.delegate.onAuthenticationSuccess(request, response, authentication); } public void setOauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) { this.oauth2UserHandler = oauth2UserHandler; }}

我们将通过setOauth2UserHandler(Consumer oauth2UserHandler)方法将UserRepositoryOAuth2UserHandler注入到SavedUserAuthenticationSuccessHandler中,UserRepositoryOAuth2UserHandler定义了具体持久层操作:

@Component@RequiredArgsConstructorpublic final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> { private final UserRepository userRepository; private final RoleRepository roleRepository; @Override public void accept(OAuth2User oAuth2User) { DefaultOAuth2User defaultOAuth2User = (DefaultOAuth2User) oAuth2User; if (this.userRepository.findUserByUsername(oAuth2User.getName()) == null) { User user = new User(); user.setUsername(defaultOAuth2User.getName()); Role role = roleRepository.findByRoleCode(defaultOAuth2User.getAuthorities() .stream().map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION")); user.setRoleList(Arrays.asList(role)); userRepository.save(user); } }}

我们通过defaultOAuth2User.getAuthorities()获取到映射后的角色信息,并将其与用户信息存储到数据库中。

UserRepository和RoleRepository为持久化容器类。

最后我们向SecurityFilterChain加入OAuth2 Login配置:

@Autowired UserRepositoryOAuth2UserHandler userHandler; @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity 127.0.0.1:8090 predicates: Path=/resource/** filters: - TokenRelay

TokenRelay 过滤器将提取存储在用户会话中的访问令牌,并将其作为​​Authorization​​标头添加到传出请求中。这允许下游服务对请求进行身份验证。

我们将在application.yml中添加OAuth2客户端信息:

spring: security: oauth2: client: registration: messaging-gateway-oidc: provider: gateway-client-provider client-id: relive-client client-secret: relive-client authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: - openid - profile client-name: messaging-gateway-oidc provider: gateway-client-provider: authorization-uri: 127.0.0.1:8080/oauth2/authorize token-uri: 127.0.0.1:8080/oauth2/token jwk-set-uri: 127.0.0.1:8080/oauth2/jwks user-info-uri: 127.0.0.1:8080/userinfo user-name-attribute: sub

OpenID Connect 使用一个特殊的权限范围值 openid 来控制对 UserInfo 端点的访问,其他信息与上节中授权服务注册客户端信息参数保持一致。

我们通过Spring Security拦截未认证请求到授权服务器进行认证。为了简单起见,​​CSRF​​被禁用。

如何将Spring Cloud Gateway与OAuth2模式有效结合使用?

@Configuration(proxyBeanMethods = false)@EnableWebFluxSecuritypublic class OAuth2LoginSecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity 127.0.0.1:8080 jwk-set-uri: 127.0.0.1:8080/oauth2/jwks server: port: 8090

创建​​ResourceServerConfig​​类来配置Spring Security安全模块,​​@EnableMethodSecurity​​注解来启用基于注解的安全性:

@Configuration(proxyBeanMethods = false)@EnableWebSecurity@EnableMethodSecuritypublic class ResourceServerConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity 127.0.0.1:8070/resource/article/read​​,我们将重定向到授权服务登录页,如图所示:

在我们输入用户名密码(admin/password)后,将获取到请求响应信息:

admin用户所属角色是​​ROLE_ADMIN​​,所以我们尝试请求​​127.0.0.1:8070/resource/article/write?name=article3​​

注销登录后,我们同样访问​​127.0.0.1:8070/resource/article/read​​,不过这次使用Github登录,响应信息如图所示:

可以看到响应信息中用户已经切换为你的Github用户名。

Github登录的用户默认赋予角色为​​ROLE_OPERATION​​,而​​ROLE_OPERATION​​是没有​​127.0.0.1:8070/resource/article/write?name=article3​​访问权限,我们来尝试测试下:

结果我们请求被拒绝,403状态码提示我们没有访问权限。

结论

本文中您了解到如何使用Spring Cloud Gateway结合OAuth2保护微服务。在示例中浏览器cookie仅存储sessionId,JWT访问令牌并没有暴露给浏览器,而是在内部服务中流转。这样我们体验到了JWT带来的优势,也同样利用cookie-session弥补了JWT的不足,例如当我们需要实现强制用户登出功能。