IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 将Spring Cloud Gateway 与OAuth2模式一起使用 -> 正文阅读

[Java知识库]将Spring Cloud Gateway 与OAuth2模式一起使用

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

概述

Spring Cloud Gateway是一个构建在 Spring 生态之上的 API Gateway。 建立在Spring Boot 2.xSpring WebFluxProject 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语句可以从这里获取。

在这里插入图片描述

角色说明

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

readwrite
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中scope替换为当前用户所属角色的权限(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("scope", 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);
    }
}

并将它注入到Spring中:

    @Bean
    UserDetailsService userDetailsService(UserRepository userRepository) {
        return new JdbcUserDetailsService(userRepository);
    }

在尝试请求未认证接口将会引导用户到登录页面并提示输入用户名密码,结果如下:

在这里插入图片描述



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

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

Github身份认证

首先我们将配置Github客户端信息,你只需要更改其中clientId和clientSecret。其次我们将使用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();
    }
}

最后我们将创建用于示例中测试的API接口,使用@PreAuthorize保护接口必须由相应权限才能访问:

@RestController
public class ArticleController {

    List<String> article = new ArrayList<String>() {{
        add("article1");
        add("article2");
    }};

    @PreAuthorize("hasAuthority('SCOPE_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('SCOPE_write')")
    @GetMapping("/resource/article/write")
    public String write(@RequestParam String name) {
        article.add(name);
        return "success";
    }
}

Spring 自动在所需scope名称前添加“SCOPE_”,这样实际所需的scope是“read”而不是“SCOPE_read”。

测试我们的应用程序

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

在这里插入图片描述


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

在这里插入图片描述


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

在这里插入图片描述


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

在这里插入图片描述

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


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

在这里插入图片描述

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

结论

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

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

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:50:24  更:2022-08-19 18:50:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 13:32:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码