本文已参与「新人创造礼」活动,一同开启创造之路。

Spring Cloud Gateway 与OAuth2形式一同运用

概述

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 Cloud Gateway 与OAuth2模式一起使用

创立授权服务

本节中咱们将运用Spring Authorization Server 构建授权服务,支撑OAuth2协议与OpenID Connect协议。一同咱们还将运用RBAC0基本权限模型操控拜访权限。并且该授权服务一同作为OAuth2客户端支撑Github第三方登录。


相关数据库表结构

咱们创立了基本RBAC0权限模型用于本文示例讲解,并供给了OAuth2授权服务耐久化存储所需表结构和OAuth2客户端耐久化存储所需表结构。经过oauth2_client_role界说外部体系人物与本渠道人物映射联系。触及相关创立表及初始化数据的SQL语句能够从这儿获取。

将Spring Cloud Gateway 与OAuth2模式一起使用

人物说明

本节中授权服务默许供给两个人物,以下是人物特点及拜访权限:

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: 8080
spring:
  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("http://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 密钥:

@Bean
public 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 http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer<>();
        //装备OIDC
        authorizationServerConfigurer.oidc(Customizer.withDefaults());
        RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
        return http.requestMatcher(endpointsMatcher)
                .authorizeRequests((authorizeRequests) -> {
                    ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) authorizeRequests.anyRequest()).authenticated();
                }).csrf((csrf) -> {
                    csrf.ignoringRequestMatchers(new RequestMatcher[]{endpointsMatcher});
                }).apply(authorizationServerConfigurer)
                .and()
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .exceptionHandling(exceptions -> exceptions.
                        authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
                .apply(authorizationServerConfigurer)
                .and()
                .build();
    }

上述咱们装备了OAuth2和OpenID Connect默许装备,并将为认证恳求重定向到登录页,一同咱们还启用了Spring Security供给的OAuth2资源服务装备,该装备用于维护OpenID Connect中/userinfo用户信息端点。


在启用Spring Security的OAuth2资源服务装备时咱们指定了JWT验证,所以咱们需求在application.yml中指定jwk-set-uri或声明式增加JwtDecoder,下面咱们运用声明式装备:

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

接下来咱们将自界说Access Token,在本示例中咱们运用RBAC0权限模型,所以咱们在Access Token中增加authorities为当前用户所属人物的权限(permissionCode):

@Configuration(proxyBeanMethods = false)
public class AccessTokenCustomizerConfig {
    @Autowired
    RoleRepository roleRepository;
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return (context) -> {
            if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
                context.getClaims().claims(claim -> {
                    claim.put("authorities", roleRepository.findByRoleCode(context.getPrincipal().getAuthorities().stream()
                            .map(GrantedAuthority::getAuthority).findFirst().orElse("ROLE_OPERATION"))
                            .getPermissions().stream().map(Permission::getPermissionCode).collect(Collectors.toSet()));
                });
            }
        };
    }
}

RoleRepository属于role表耐久层目标,在本示例中选用JPA结构,相关代码将不在文中展示,如果您并不了解JPA运用,能够运用Mybatis代替。


下面咱们将装备授权服务Form表单认证办法:

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(withDefaults())
          ...
        return http.build();
    }

接下来咱们将创立JdbcUserDetailsService 完成 UserDetailsService,用于在认证过程中查找登录用户的暗码及权限信息,至于为什么需求完成UserDetailsService,感兴趣能够查看UsernamePasswordAuthenticationFilter -> ProviderManager -> DaoAuthenticationProvider 源码,在DaoAuthenticationProvider中经过调用UserDetailsService#loadUserByUsername(String username)获取用户信息。

@RequiredArgsConstructor
public class JdbcUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        com.relive.entity.User user = userRepository.findUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("user is not found");
        }
        if (CollectionUtils.isEmpty(user.getRoleList())) {
            throw new UsernameNotFoundException("role is not found");
        }
        Set<SimpleGrantedAuthority> authorities = user.getRoleList().stream().map(Role::getRoleCode)
                .map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}
    @Bean
    UserDetailsService userDetailsService(UserRepository userRepository) {
        return new JdbcUserDetailsService(userRepository);
    }

在测验恳求未认证接口将会引导用户到登录页面并提示输入用户名暗码,结果如下:

将Spring Cloud Gateway 与OAuth2模式一起使用



用户一般需求运用多个渠道,这些渠道由不同组织供给和托管。 这些用户可能需求运用每个渠道的特定(和不同)的凭证。当用户具有许多不同的凭证时,他们常常会忘记登录凭证。

联合身份验证是运用外部体系对用户进行身份验证。这能够与Google,Github或任何其他身份供给商一同运用。在这儿,我将运用Github进行用户身份验证和数据同步管理。

Github身份认证

首要咱们将装备Github客户端信息,你只需求更改其中clientIdclientSecret。其次咱们将运用Spring Security 耐久化OAuth2客户端 文中介绍的JdbcClientRegistrationRepository耐久层容器类将GitHub客户端信息存储在数据库中:

    @Bean
    ClientRegistrationRepository clientRegistrationRepository(JdbcTemplate jdbcTemplate) {
        JdbcClientRegistrationRepository jdbcClientRegistrationRepository = new JdbcClientRegistrationRepository(jdbcTemplate);
        ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("github")
                .clientId("123456")
                .clientSecret("123456")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}")
                .scope(new String[]{"read:user"})
                .authorizationUri("https://github.com/login/oauth/authorize")
                .tokenUri("https://github.com/login/oauth/access_token")
                .userInfoUri("https://api.github.com/user")
                .userNameAttributeName("login")
                .clientName("GitHub").build();
        jdbcClientRegistrationRepository.save(clientRegistration);
        return jdbcClientRegistrationRepository;
    }

接下来咱们将实例化OAuth2AuthorizedClientServiceOAuth2AuthorizedClientRepository

  • 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类颁发用户人物:

@RequiredArgsConstructor
public 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);
    }
}

咱们能够看到从authorityrole特点中获取权限信息,在经过OAuth2ClientRoleRepository查找映射到本渠道的人物特点。

注意:authorityrole是由渠道自界说特点,与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
@RequiredArgsConstructor
public 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 http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .oauth2Login(oauth2login -> {
                    SavedUserAuthenticationSuccessHandler successHandler = new SavedUserAuthenticationSuccessHandler();
                    successHandler.setOauth2UserHandler(userHandler);
                    oauth2login.successHandler(successHandler);
                });
      	...
        return http.build();
    }

创立Spring Cloud Gateway应用程序

本节中咱们将在Spring Cloud Gateway中经过Spring Security OAuth2 Login 启用OpenID Connect身份验证,并将Access Token中继到下流服务。

Maven依靠

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
  <version>3.1.2</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-data-redis</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>2.6.3</version>
</dependency>
<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-all</artifactId>
  <version>4.1.76.Final</version>
</dependency>

装备

首要咱们在application.yml增加以下特点:

server:
  port: 8070
  servlet:
    session:
      cookie:
        name: GATEWAY-CLIENT

这儿指定了cookie name为GATEWAY-CLIENT,避免与授权服务JSESSIONID冲突。


经过Spring Cloud Gateway路由到资源服务器:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: resource-server
          uri: http://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: http://127.0.0.1:8080/oauth2/authorize
            token-uri: http://127.0.0.1:8080/oauth2/token
            jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
            user-info-uri: http://127.0.0.1:8080/userinfo
            user-name-attribute: sub

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


咱们经过Spring Security拦截未认证恳求到授权服务器进行认证。为了简略起见,CSRF被禁用。

@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
                .authorizeExchange(authorize -> authorize
                        .anyExchange().authenticated()
                )
                .oauth2Login(withDefaults())
                .cors().disable();
        return http.build();
    }
}

Spring Cloud Gateway在完结OpenID Connect身份验证后,将用户信息和令牌存储在session会话中,所以增加spring-session-data-redis供给由 Redis 支撑的分布式会话功用,在application.yml中增加以下装备:

spring:
  session:
    store-type: redis # 会话存储类型
    redis:
      flush-mode: on_save # 会话改写形式
      namespace: gateway:session # 用于存储会话的键的命名空间
  redis:
    host: localhost
    port: 6379
    password: 123456

根据上述示例咱们运用 Spring Cloud Gateway驱动身份验证,知道怎么对用户进行身份验证,能够为用户获取令牌(在用户同意后),但不对经过Gateway的恳求进行身份验证/授权(Spring Gateway Cloud并不是Access Token的受众目标)。这种办法背面的原因是一些服务是受维护的,而一些是公共的。即便在单个服务中,有时也只能维护几个端点而不是每个端点。这就是我将恳求的身份验证/授权留给特定服务的原因。

当然从完成视点并不阻碍咱们在Spring Cloud Gateway进行身份验证/授权,这仅仅一个挑选问题。

建立资源服务

本节中咱们运用Spring Boot建立一个简略的资源服务,示例中资源服务供给两个API接口,并经过Spring Security OAuth2资源服务装备维护。

Maven依靠

<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-security</artifactId>
  <version>2.6.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
  <version>2.6.7</version>
</dependency>

装备

application.yml中增加jwk-set-uri特点:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080
          jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks 
server:
  port: 8090

创立ResourceServerConfig类来装备Spring Security安全模块,@EnableMethodSecurity注解来启用根据注解的安全性:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
                 )
                .oauth2ResourceServer()
                .jwt();
        return http.build();
    }
}

Spring Security资源服务在验证token提取权限默许运用claim中scopescp特点。

Spring Security JwtAuthenticationProvider经过JwtAuthenticationConverter辅助转换器提取权限等信息。

但是在本示例中内部化权限运用authorities特点,所以咱们运用JwtAuthenticationConverter 手动提取权限:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
    grantedAuthoritiesConverter.setAuthorityPrefix("");
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}   

在这儿咱们将权限特点指定为authorities,并彻底删去权限前缀。


最终咱们将创立用于示例中测验的API接口,运用@PreAuthorize维护接口必须由相应权限才干拜访:

@RestController
public class ArticleController {
    List<String> article = new ArrayList<String>() {{
        add("article1");
        add("article2");
    }};
    @PreAuthorize("hasAuthority('read')")
    @GetMapping("/resource/article/read")
    public Map<String, Object> read(@AuthenticationPrincipal Jwt jwt) {
        Map<String, Object> result = new HashMap<>(2);
        result.put("principal", jwt.getClaims());
        result.put("article", article);
        return result;
    }
    @PreAuthorize("hasAuthority('write')")
    @GetMapping("/resource/article/write")
    public String write(@RequestParam String name) {
        article.add(name);
        return "success";
    }
}

测验咱们的应用程序

在咱们启动完结服务后,咱们在浏览器中拜访http://127.0.0.1:8070/resource/article/read ,咱们将重定向到授权服务登录页,如图所示:

将Spring Cloud Gateway 与OAuth2模式一起使用


在咱们输入用户名暗码(admin/password)后,将获取到恳求呼应信息:

将Spring Cloud Gateway 与OAuth2模式一起使用


admin用户所属人物是ROLE_ADMIN,所以咱们测验恳求http://127.0.0.1:8070/resource/article/write?name=article3

将Spring Cloud Gateway 与OAuth2模式一起使用


刊出登录后,咱们相同拜访http://127.0.0.1:8070/resource/article/read ,不过这次运用Github登录,呼应信息如图所示:

将Spring Cloud Gateway 与OAuth2模式一起使用

能够看到呼应信息中用户已经切换为你的Github用户名。


Github登录的用户默许赋予人物为ROLE_OPERATION,而ROLE_OPERATION是没有http://127.0.0.1:8070/resource/article/write?name=article3 拜访权限,咱们来测验测验下:

将Spring Cloud Gateway 与OAuth2模式一起使用

结果咱们恳求被拒绝,403状况码提示咱们没有拜访权限。

定论

本文中您了解到怎么运用Spring Cloud Gateway结合OAuth2维护微服务。在示例中浏览器cookie仅存储sessionId,JWT拜访令牌并没有露出给浏览器,而是在内部服务中流通。这样咱们体会到了JWT带来的优势,也相同运用cookie-session弥补了JWT的不足,例如当咱们需求完成强制用户登出功用。

与往常一样,本文中运用的源代码可在 GitHub 上取得。