【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践

        在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(OAuth2 授权码模式),这块接入工作是由我来负责,我们的项目是微服务架构,经过网上各种查阅资料发现网关作为OAuth2 Client接入单点登录,将用户信息解析传递给下游微服务是最佳方案,在本文中我将详细讲解怎么基于Spring Cloud Gateway 接入第三方单点登录。

  • 如文章中有明显错误或者用词不当的地方,欢迎大家在评论区批评指正,我看到后会及时修改。
  • 如想要和博主进行技术栈方面的讨论和交流可私信我。

目录

1. 前言

2. 流程图

3. 开发环境搭建

3.1. 项目结构

3.2. 所用版本工具

3.3. pom依赖

4. 核心代码

4.1. 网关模块核心代码

4.1.1. 编写网关yml配置

4.1.2. 编写Security授权配置主文件

4.1.3. 编写认证过滤器

4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver

 4.1.5. 编写OAuth2User实现类

 4.1.6. 编写url白名单配置类

4.1.7.  编写userInfo过滤器

 4.1.8. 编写ReactiveOAuth2UserService实现类

4.2. 资源服务器核心代码

4.2.1. 编写资源服务器yml

4.2.2. 编写资源服务器测试controller

5. 登录测试

6. 参考链接


1. 前言

        Spring Cloud Gateway是Spring Cloud生态系统中的一个组件,主要用于构建微服务架构中的网关服务。它提供了一种灵活而强大的方式来路由请求、过滤请求以及添加各种功能,如负载均衡、熔断、安全性等。通过将Spring Cloud Gateway作为OAuth2 Client,可以实现用户在系统中的统一认证体验。用户只需要一次登录,即可访问多个微服务,避免了在每个服务中都进行独立的认证,下游微服务只需要专注自己的业务代码即可。

2. 流程图

        让我们来先看一下基于网关集成单点登录的流程图(OAuth2授权码模式),我这边只是一个大致流程,想要看完整细致流程的同学可以去看一下大佬写的这篇文章:Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客

【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践_第1张图片

3. 开发环境搭建

3.1. 项目结构

基于Spring Cloud Gateway作为OAuth2 Client接入单点登录的项目结构如下图所示:

【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践_第2张图片

由上图可以看出这个项目(demo)是微服务组织架构,这里我只创建了两个moudle(父模块不算)即网关和资源服务器。

3.2. 所用版本工具

依赖 版本
Spring Boot 2.6.3

Spring Cloud Alibaba

2021.0.1.0
Spring Cloud  2021.0.1
java 1.8
redis 6.2

3.3. pom依赖

1. 父模块依赖

  
        8
        8
        UTF-8
        UTF-8
        1.8
        2021.0.1
        2021.0.1.0
    
    
        
            
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                ${cloud-alibaba.version}
                pom
                import
            
        
    

2.  网关模块依赖


    
        org.springframework.cloud
        spring-cloud-starter-gateway
    

    
        org.springframework.boot
        spring-boot-starter-oauth2-client
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    

    
        org.springframework.session
        spring-session-data-redis
    
    
        org.springframework.cloud
        spring-cloud-starter-bootstrap
    
    
        org.projectlombok
        lombok
    
    
        org.springframework.cloud
        spring-cloud-starter-loadbalancer
    

3. 资源服务器模块依赖


    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.cloud
        spring-cloud-starter-bootstrap
    

4. 核心代码

4.1. 网关模块核心代码

4.1.1. 编写网关yml配置
server:
  reactive:
    session:
      cookie:
        http-only: true
  port: 8888
system:
  whiteList:
    - "/auth"
    - "/oauth2"
    - "/favicon.ico"
    - "/login"
spring:
  cloud:
    gateway:
      routes:
        - id: geoscene-back-resource
          uri: http://127.0.0.1:8090
          predicates:
            - Path=/resource/**
          filters:
            - TokenRelay
            - UserInfoRelay
  session:
    store-type: redis # 会话存储类型
    redis:
      cleanup-cron: 0 * * * * *
      flush-mode: on_save # 会话刷新模式
      namespace: gateway:session # 用于存储会话的键的命名空间
      save-mode: on_set_attribute
  redis:
    host: localhost
    port: 6379
#    password: 123456
  security:
    filter:
      order: 5
    oauth2:
      client:
        registration:
          gas:
            provider: gas
            client-id: 在第三方授权中心获取的 client-id
            client-secret: 在第三方授权中心获取(自定义)的 client-secret
            redirect-uri: http://127.0.0.1:8888/login/oauth2/code/gas
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_basic
            scope: userinfo
        provider:
          gas:
            issuer-uri: 填写第三方认证地址
#
logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO
    org.springframework.cloud.gateway: INFO




4.1.2. 编写Security授权配置主文件
@Configuration(proxyBeanMethods = false)
@EnableWebFluxSecurity
public class Oauth2ClientSecurityConfig {
    private String oauth2LoginEndpoint = "/login/oauth2/code/gas";

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver) {
        http
                .authorizeExchange(authorize -> authorize
                        .pathMatchers("/auth/**", "/oauth2/**"
                        ).permitAll()
                        .anyExchange().authenticated()
                )
                .oauth2Login(oauth2Login -> oauth2Login
                        // 发起 OAuth2 登录的地址(服务端)
                        .authorizationRequestResolver(saveRequestServerOAuth2AuthorizationRequestResolver)
                        // OAuth2 外部用户登录授权后的跳转地址(服务端)
                        .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher(
                                oauth2LoginEndpoint))
                )
                .cors().disable();
        return http.build();
    }

    /**
     * OAuth2 Client Authorization Endpoint /oauth2/authoriztion/{clientRegId} 
     * 请求解析器扩展实现 - 支持提取query参数redirect_uri,用作后续OAuth2认证完成后网关重定向到该指定redirect_uri。
     * 适用场景:前端应用 -> 网关 -> 网关返回401 -> 前端应用重定向到/oauth2/authorization/{clientRegId}?redirect_uri=http://登录后界面 -> 网关完成OAuth2认证后再重定向回http://登录后界面
     */
    @Bean
    @Primary
    public ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        return new SaveRequestServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
    }

    /**
     * 自定义UserInfo过滤器工厂
     */
    @Bean
    public UserInfoRelayGatewayFilterFactory userInfoRelayGatewayFilterFactory() {
        return new UserInfoRelayGatewayFilterFactory();
    }

}
4.1.3. 编写认证过滤器
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class CustomWebFilter implements WebFilter {
    @Autowired
    private UrlConfig urlConfig;
    @Override
    public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
        // 请求对象
        ServerHttpRequest request = exchange.getRequest();
        // 响应对象
        ServerHttpResponse response = exchange.getResponse();

        return exchange.getSession().flatMap(webSession -> {

            for (int i = 0; i  {
            log.info("this is a post filter");
        }));
    }
}

上述代码的主要功能为拦截进入网关的每一个请求,若没有身份凭证(令牌)则返回/oauth2/authorization/{clientRegId}。

4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver
public class SaveRequestServerOAuth2AuthorizationRequestResolver extends DefaultServerOAuth2AuthorizationRequestResolver {
    private static final Log logger = LogFactory.getLog(SaveRequestServerOAuth2AuthorizationRequestResolver.class);
    /**
     * redirect uri参数名称
     */
    private static final String PARAM_REDIRECT_URI = "redirect_uri";
    /**
     * WebSession对应的saveRequest属性名
     * 完全沿用(兼容)WebSessionServerRequestCache定义
     */
    private static final String DEFAULT_SAVED_REQUEST_ATTR = "SPRING_SECURITY_SAVED_REQUEST";
    private String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR;

    /**
     * Creates a new instance
     *
     * @param clientRegistrationRepository the repository to resolve the
     *                                     {@link ClientRegistration}
     */
    public SaveRequestServerOAuth2AuthorizationRequestResolver(
            ReactiveClientRegistrationRepository clientRegistrationRepository) {
        super(clientRegistrationRepository);
    }
    @Override
    public Mono resolve(ServerWebExchange exchange) {

        return super.resolve(exchange)
                .doOnNext(OAuth2AuthorizationRequest -> {
//                    获取query参数redirect_uri
                    Optional.ofNullable(exchange.getRequest())
                            .map(ServerHttpRequest::getQueryParams)
                            .map(queryParams -> queryParams.get(PARAM_REDIRECT_URI))
                            .filter(redirectUris -> !CollectionUtils.isEmpty(redirectUris))
                            .map(redirectUris -> redirectUris.get(0))
                            .ifPresent(redirectUri -> {
                                //若redirect_uri非空,则覆盖Session中的SPRING_SECURITY_SAVED_REQUEST为redirect_uri
                                //即后续认证成功后可重定向回前端指定页面
                                exchange.getSession().subscribe(webSession -> {
                                    webSession.getAttributes().put(this.sessionAttrName, redirectUri);
                                    logger.debug(LogMessage.format("SCG OAuth2 authorization endpoint queryParam redirect_uri added to WebSession: '%s'", redirectUri));
                                });
                            });
                });
    }

}
 4.1.5. 编写OAuth2User实现类

public class CustomUser implements OAuth2User, Serializable {
    private Map attributes;
    private Collection authorities;
    private String name;

    public CustomUser(Map attributes, Collection authorities, String name) {
        this.attributes = attributes;
        this.authorities = authorities;
        this.name = name;
    }

    public CustomUser() {
    }

    @Override
    public Map getAttributes() {
        return attributes;
    }

    @Override
    public Collection getAuthorities() {
        return authorities;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setAttributes(Map attributes) {
        this.attributes = attributes;
    }

    public void setAuthorities(Collection authorities) {
        this.authorities = authorities;
    }

    public void setName(String name) {
        this.name = name;
    }
}
 4.1.6. 编写url白名单配置类
@Configuration
@ConfigurationProperties(prefix = "system")
public class UrlConfig {
    // 配置文件使用list接收
    private List whiteList;
    public List getWhiteList() {
        return whiteList;
    }
    public void setWhiteList(List whiteList) {
        this.whiteList = whiteList;
    }
}
4.1.7.  编写userInfo过滤器
public class UserInfoRelayGatewayFilterFactory extends AbstractGatewayFilterFactory {

    private final static String USER_INFO_HEADER = "userInfo";

    public UserInfoRelayGatewayFilterFactory() {
        super(Object.class);
    }

    public GatewayFilter apply() {
        return apply((Object) null);
    }

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> exchange.getPrincipal()
                // .log("token-relay-filter")
                .filter(principal -> principal instanceof OAuth2AuthenticationToken)
                .cast(OAuth2AuthenticationToken.class)
                //.flatMap(authentication -> authorizedClient(exchange, authentication))
                .map(OAuth2AuthenticationToken::getPrincipal)
                .map(oAuth2User -> withUserInfoHeader(exchange, oAuth2User))
                .defaultIfEmpty(exchange)
                .flatMap(chain::filter);
    }

    private ServerWebExchange withUserInfoHeader(ServerWebExchange exchange, OAuth2User oAuth2User) {
        //String userName = oAuth2User.getName();
        Map userAttrs = oAuth2User.getAttributes();
        if (oAuth2User instanceof OidcUser) {
            userAttrs = ((OidcUser) oAuth2User).getUserInfo().getClaims();
        }
        String userAttrsJson = JsonUtils.toJson(userAttrs);
        return exchange.mutate()
                .request(r -> r.headers(headers -> headers.add(USER_INFO_HEADER, userAttrsJson)))
                .build();
    }

} 
  
 4.1.8. 编写ReactiveOAuth2UserService实现类
@Component
public class CustomOAuth2UserService implements ReactiveOAuth2UserService {

    private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";

    private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";

    private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";

    private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() {
    };

    private static final ParameterizedTypeReference> STRING_STRING_MAP = new ParameterizedTypeReference>() {
    };

    private WebClient webClient = WebClient.create();
    
@Override
public Mono loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    return Mono.fromCallable(() -> {
        String tokenStr = userRequest.getAccessToken().getTokenValue();
        try {
            SignedJWT sjwt = SignedJWT.parse(tokenStr);
            JWTClaimsSet claims = sjwt.getJWTClaimsSet();
            claims.getSubject();
            Collection res = new ArrayList<>();
            CustomUser customUser=new CustomUser( claims.getClaims(),res,claims.getSubject());
            return customUser;

        } catch (ParseException e) {
            e.printStackTrace();
            throw new OAuth2AuthenticationException(new OAuth2Error("500"),"服务器返回错误的jwt");
        }
    });
}

}

4.2. 资源服务器核心代码

4.2.1. 编写资源服务器yml
server:
  port: 8090
  servlet:
    context-path: /resource
4.2.2. 编写资源服务器测试controller
@RestController
public class ArticleController {
    @GetMapping("/user-info")
    public String getUserName( @RequestHeader String userInfo){
        return userInfo;
    }
}

5. 登录测试

1. 直接访问资源服务器接口

【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践_第3张图片

由上图可看出无法直接访问资源服务器接口,前端接收到此返回信息后根据data中返回的路径加上redirect_uri(http://127.0.0.1:8888/oauth2/authorization/gas?redirect_uri=http://www.baidu.com),发送页面请求后可跳转至登录中心,认证成功后界面会重定向至redirect_uri所指定的界面(我这里写的百度)。

【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践_第4张图片

跳转至登录界面进行认证。

【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践_第5张图片

认证成功后重定向至redirect_uri所指定的界面(百度)。

2. 再次访问资源服务器接口

访问接口成功。

6. 参考链接

Spring Cloud Gateway作为OAuth2 Client_oauth2客户端接口为什么跳转到login_罗小爬EX的博客-CSDN博客

将Spring Cloud Gateway 与OAuth2模式一起使用_jwk-set-uri_ReLive27的博客-CSDN博客

第15章 Spring Security OAuth2 初始_authorizeexchange-CSDN博客

你可能感兴趣的:(安全框架,spring,cloud,gateway,spring)