弹簧云网关与OAuth2.0协议的安全实践相结合
#安全 #java #oauth2 #springsecurity

概述

弹簧云网关是建在弹簧生态系统顶部的API网关。它建在Spring BootSpring WebFluxProject Reactor的顶部。

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

您将在本文中学到什么:

  • OpenID连接身份验证 - 用于用户身份验证。
  • 令牌继电器 - 春季云网关API网关充当客户端,并将令牌转发到资源请求。

先决条件:

  • Java 8+
  • mysql
  • redis

OpenID连接身份验证

OpenID Connect定义了基于OAUTH2授权代码流的用户身份验证机制。下图显示了Spring Cloud Gateway和授权服务之间身份验证的完整过程。为了清楚起见,已经省略了一些参数。

Image description

建立授权服务

在本节中,我们将使用Spring Authorization Server来构建支持OAUTH2和OPENID CONNECT协议的授权服务。同时,我们将使用RBAC0基本权限模型来控制访问权限。此授权服务还支持GitHub第三方登录作为OAuth2客户端。

相关数据库表结构

我们为本文创建了一个基本的RBAC0权限模型,并提供了OAUTH2授权服务和OAUTH2客户端的持久存储所需的表结构。 oauth2_client_role 表定义了外部系统角色和与本地平台角色的映射关系。可以获得与创建相关表和初始化数据有关的SQL语句。

Image description

角色描述

默认情况下,本节中的授权服务提供了两个角色,具有以下角色属性和访问权限:

阅读
roun_admin
roun_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>> # Modify username
      password: <<password>> # Modify 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)
                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                        .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                        .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-BYTE 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授权的SecurityFilterChainSecurityFilterChain是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);
    }

接下来,我们将自定义访问令牌。在此示例中,我们使用RBAC0权限模型,因此我们将当前用户角色的权限代码添加到access_token

@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是角色表的持久性层对象。在此示例中,我们使用JPA框架,并且将在本文中显示相关代码。如果您不熟悉JPA,则可以使用mybatis。

下面我们将配置授权服务表格身份验证方法。

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

          ...

        return http.build();
    }

接下来,我们将创建一个实现UserDetailsServiceJdbcUserDetailsService,该UserDetailsService用于在身份验证过程中查找已登录用户的密码和权限信息。如果您对为什么需要实现UserDetailsService感兴趣,可以检查UsernamePasswordAuthenticationFilter-> ProviderManager-> DaoAuthenticationProvider的源代码。在DaoAuthenticationProvider中,用户信息是通过调用UserDetailsservice#loadUserByUsername(字符串用户名)获得的。

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

尝试访问未经验证的界面时,用户将被定向到登录页面并提示输入其用户名和密码,如下所示:

Image description

用户通常需要使用不同组织提供和托管的多个平台。这些用户可能需要为每个平台使用特定(和不同的)凭据。当用户具有许多不同的凭据时,他们通常会忘记登录凭据。

联合身份验证用于使用外部系统对用户进行身份验证。这可以与Google,GitHub或任何其他身份提供商一起使用。在这里,我将使用github进行用户身份验证和数据同步管理。

GitHub身份认证

首先,我们将配置GitHub客户端信息,您只需要更改 clientId clientsecret 即可。其次,我们将使用Spring Security Persistence OAuth2 Client中引入的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 :负责在Web请求之间持续oauth2authorizedclient。
  • oauth2authorizedClientRepository :用于在请求之间保存和坚持授权客户端。

    @Bean
    OAuth2AuthorizedClientService authorizedClientService(
            JdbcTemplate jdbcTemplate,
            ClientRegistrationRepository clientRegistrationRepository) {
        return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
    }

    @Bean
    OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

对于使用GitHub登录的每个用户,我们需要分配平台角色来控制他们可以访问的资源。在这里,我们将创建pertiatemappatoAuth2userService类以授予用户角色:

@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());
        //When no client role is specified, the least privilege ROLE_OPERATION is given by default
        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);
    }
}

我们可以看到权限信息是从权威和角色属性中获得的,并且映射到此平台的角色属性将通过oauth2clientrolererepository搜索。

注意:authorityrole是平台的自定义属性,与OAuth2协议无关,开放ID Connect协议。在生产环境中,您可以与外部系统进行协商,以同意传输权限信息的属性。

OAuth2ClientRoleRepository是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<OAuth2User> oauth2UserHandler)方法将UserRepositoryOAuth2UserHandler注入SavedUserAuthenticationSuccessHandlerUserRepositoryOAuth2UserHandler定义了特定的持久性层操作:

@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()映射的角色信息,并使用用户信息将其存储在数据库中。

用户介绍和rolerepository是持久的容器类。

最后,我们将OAuth2登录配置添加到SecurityFilterchain:

    @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 Security OAuth2 Login启用OpenID Connect Authentication并继电器访问令牌到下游服务。

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名称被指定为GATEWAY-CLIENT,以避免与授权服务相冲突。

通过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过滤器提取了存储在用户会话中的访问令牌,并将其添加为授权标题,以在即将推出的请求中。这允许下游服务对请求进行身份验证。

我们将在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 Authentication后,用户信息和令牌存储在会话中,因此我们添加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不是受众群体目标访问令牌)。这种方法背后的原因是某些服务受到保护,而另一些服务是公开的。即使在一项服务中,有时还需要保护一些端点,而不是每个端点。这就是为什么我将请求身份验证/授权留给特定服务。

当然,从实施角度来看,它并不能阻止我们在Spring Cloud Gateway中执行身份验证/授权,这只是一个选择。

资源服务器

在本节中,我们使用Spring Boot设置简单的资源服务器。该示例为资源服务器提供了两个API接口,并通过Spring Security OAuth2 Resource Server配置保护它们。

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 Security Module,并使用@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 Resource Server使用索赔中的scopescp属性默认情况下验证令牌并提取权限信息。

Spring Security JwtAuthenticationProvider通过JwtAuthenticationConverter辅助转换器提取权限信息和其他信息。

但是,在此示例中,内部权限使用当局属性,因此我们使用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,我们将被重定向到授权服务登录页面,如下所示:

Image description

我们输入用户名和密码(管理员/密码)后,我们将获得请求响应信息:

Image description

管理员用户的角色是ROLE_ADMIN,因此我们尝试请求http://127.0.0.1:8070/resource/article/write?name=article3

Image description

登录后,我们也访问http://127.0.0.1:8070/resource/article/read,但是这次我们使用github登录,响应信息如下所示:

Image description

我们可以看到响应信息表明用户已切换到您的github用户名。

使用GitHub登录的用户默认为ROLE_OPERATION角色,而ROLE_OPERATION无法访问http://127.0.0.1:8070/resource/article/write?name=article3。让我们尝试测试它:

Image description

结果表明我们的请求被拒绝,带有403状态代码,表明我们没有访问权限。

结论

在本文中,您学会了如何使用弹簧云网关使用OAuth2保护微服务。在示例中,浏览器cookie仅存储会话ID,而JWT访问令牌不暴露于浏览器,而是在服务中内部流动。这样,我们会体验JWT的优势,并使用Cookie-Session来弥补JWT的缺点,例如当我们需要实现强制用户注销功能时。

一如既往,本文中使用的源代码可用on GitHub

感谢您的阅读! ð