当前位置:   article > 正文

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

gateway oauth2

        在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(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博客

3. 开发环境搭建

3.1. 项目结构

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

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

3.2. 所用版本工具

依赖版本
Spring Boot2.6.3

Spring Cloud Alibaba

2021.0.1.0
Spring Cloud 2021.0.1
java1.8
redis6.2

3.3. pom依赖

1. 父模块依赖

  1. <properties>
  2. <maven.compiler.source>8</maven.compiler.source>
  3. <maven.compiler.target>8</maven.compiler.target>
  4. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  5. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  6. <java.version>1.8</java.version>
  7. <spring-cloud.version>2021.0.1</spring-cloud.version>
  8. <cloud-alibaba.version>2021.0.1.0</cloud-alibaba.version>
  9. </properties>
  10. <dependencyManagement>
  11. <dependencies>
  12. <!-- springCloud -->
  13. <dependency>
  14. <groupId>org.springframework.cloud</groupId>
  15. <artifactId>spring-cloud-dependencies</artifactId>
  16. <version>${spring-cloud.version}</version>
  17. <type>pom</type>
  18. <scope>import</scope>
  19. </dependency>
  20. <dependency>
  21. <groupId>com.alibaba.cloud</groupId>
  22. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  23. <version>${cloud-alibaba.version}</version>
  24. <type>pom</type>
  25. <scope>import</scope>
  26. </dependency>
  27. </dependencies>
  28. </dependencyManagement>

2.  网关模块依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.cloud</groupId>
  4. <artifactId>spring-cloud-starter-gateway</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-oauth2-client</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework.boot</groupId>
  12. <artifactId>spring-boot-starter-data-redis</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>org.springframework.session</groupId>
  16. <artifactId>spring-session-data-redis</artifactId>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.cloud</groupId>
  20. <artifactId>spring-cloud-starter-bootstrap</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.projectlombok</groupId>
  24. <artifactId>lombok</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.cloud</groupId>
  28. <artifactId>spring-cloud-starter-loadbalancer</artifactId>
  29. </dependency>
  30. </dependencies>

3. 资源服务器模块依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-bootstrap</artifactId>
  9. </dependency>
  10. </dependencies>

4. 核心代码

4.1. 网关模块核心代码

4.1.1. 编写网关yml配置
  1. server:
  2. reactive:
  3. session:
  4. cookie:
  5. http-only: true
  6. port: 8888
  7. system:
  8. whiteList:
  9. - "/auth"
  10. - "/oauth2"
  11. - "/favicon.ico"
  12. - "/login"
  13. spring:
  14. cloud:
  15. gateway:
  16. routes:
  17. - id: geoscene-back-resource
  18. uri: http://127.0.0.1:8090
  19. predicates:
  20. - Path=/resource/**
  21. filters:
  22. - TokenRelay
  23. - UserInfoRelay
  24. session:
  25. store-type: redis # 会话存储类型
  26. redis:
  27. cleanup-cron: 0 * * * * *
  28. flush-mode: on_save # 会话刷新模式
  29. namespace: gateway:session # 用于存储会话的键的命名空间
  30. save-mode: on_set_attribute
  31. redis:
  32. host: localhost
  33. port: 6379
  34. # password: 123456
  35. security:
  36. filter:
  37. order: 5
  38. oauth2:
  39. client:
  40. registration:
  41. gas:
  42. provider: gas
  43. client-id: 在第三方授权中心获取的 client-id
  44. client-secret: 在第三方授权中心获取(自定义)的 client-secret
  45. redirect-uri: http://127.0.0.1:8888/login/oauth2/code/gas
  46. authorization-grant-type: authorization_code
  47. client-authentication-method: client_secret_basic
  48. scope: userinfo
  49. provider:
  50. gas:
  51. issuer-uri: 填写第三方认证地址
  52. #
  53. logging:
  54. level:
  55. root: INFO
  56. org.springframework.web: INFO
  57. org.springframework.security: INFO
  58. org.springframework.security.oauth2: INFO
  59. org.springframework.cloud.gateway: INFO
4.1.2. 编写Security授权配置主文件
  1. @Configuration(proxyBeanMethods = false)
  2. @EnableWebFluxSecurity
  3. public class Oauth2ClientSecurityConfig {
  4. private String oauth2LoginEndpoint = "/login/oauth2/code/gas";
  5. @Bean
  6. public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver) {
  7. http
  8. .authorizeExchange(authorize -> authorize
  9. .pathMatchers("/auth/**", "/oauth2/**"
  10. ).permitAll()
  11. .anyExchange().authenticated()
  12. )
  13. .oauth2Login(oauth2Login -> oauth2Login
  14. // 发起 OAuth2 登录的地址(服务端)
  15. .authorizationRequestResolver(saveRequestServerOAuth2AuthorizationRequestResolver)
  16. // OAuth2 外部用户登录授权后的跳转地址(服务端)
  17. .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher(
  18. oauth2LoginEndpoint))
  19. )
  20. .cors().disable();
  21. return http.build();
  22. }
  23. /**
  24. * OAuth2 Client Authorization Endpoint /oauth2/authoriztion/{clientRegId}
  25. * 请求解析器扩展实现 - 支持提取query参数redirect_uri,用作后续OAuth2认证完成后网关重定向到该指定redirect_uri。
  26. * 适用场景:前端应用 -> 网关 -> 网关返回401 -> 前端应用重定向到/oauth2/authorization/{clientRegId}?redirect_uri=http://登录后界面 -> 网关完成OAuth2认证后再重定向回http://登录后界面
  27. */
  28. @Bean
  29. @Primary
  30. public ServerOAuth2AuthorizationRequestResolver saveRequestServerOAuth2AuthorizationRequestResolver(ReactiveClientRegistrationRepository clientRegistrationRepository) {
  31. return new SaveRequestServerOAuth2AuthorizationRequestResolver(clientRegistrationRepository);
  32. }
  33. /**
  34. * 自定义UserInfo过滤器工厂
  35. */
  36. @Bean
  37. public UserInfoRelayGatewayFilterFactory userInfoRelayGatewayFilterFactory() {
  38. return new UserInfoRelayGatewayFilterFactory();
  39. }
  40. }
4.1.3. 编写认证过滤器
  1. @Component
  2. @Order(Ordered.HIGHEST_PRECEDENCE)
  3. @Slf4j
  4. public class CustomWebFilter implements WebFilter {
  5. @Autowired
  6. private UrlConfig urlConfig;
  7. @Override
  8. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  9. // 请求对象
  10. ServerHttpRequest request = exchange.getRequest();
  11. // 响应对象
  12. ServerHttpResponse response = exchange.getResponse();
  13. return exchange.getSession().flatMap(webSession -> {
  14. for (int i = 0; i <urlConfig.getWhiteList().size() ; i++) {
  15. if (request.getURI().getPath().contains(urlConfig.getWhiteList().get(i))) {
  16. return chain.filter(exchange);
  17. }
  18. }
  19. if( webSession.getAttribute("SPRING_SECURITY_CONTEXT")==null||!((SecurityContext)webSession.getAttribute("SPRING_SECURITY_CONTEXT")).getAuthentication().isAuthenticated()){
  20. JSONObject message = new JSONObject();
  21. message.put("code", 401);
  22. message.put("status","fail");
  23. message.put("message", "缺少身份凭证");
  24. message.put("data", "http://127.0.0.1:8888/oauth2/authorization/gas");
  25. // 转换响应消息内容对象为字节
  26. byte[] bits = message.toJSONString().getBytes(StandardCharsets.UTF_8);
  27. DataBuffer buffer = response.bufferFactory().wrap(bits);
  28. // 设置响应对象状态码 401
  29. response.setStatusCode(HttpStatus.UNAUTHORIZED);
  30. // 设置响应对象内容并且指定编码,否则在浏览器中会中文乱码
  31. response.getHeaders().add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");
  32. // 返回响应对象
  33. return response.writeWith( Mono.just(buffer) );
  34. }
  35. return chain.filter(exchange);
  36. }).then(Mono.fromRunnable(() -> {
  37. log.info("this is a post filter");
  38. }));
  39. }
  40. }

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

4.1.4. 重写DefaultServerOAuth2AuthorizationRequestResolver
  1. public class SaveRequestServerOAuth2AuthorizationRequestResolver extends DefaultServerOAuth2AuthorizationRequestResolver {
  2. private static final Log logger = LogFactory.getLog(SaveRequestServerOAuth2AuthorizationRequestResolver.class);
  3. /**
  4. * redirect uri参数名称
  5. */
  6. private static final String PARAM_REDIRECT_URI = "redirect_uri";
  7. /**
  8. * WebSession对应的saveRequest属性名
  9. * 完全沿用(兼容)WebSessionServerRequestCache定义
  10. */
  11. private static final String DEFAULT_SAVED_REQUEST_ATTR = "SPRING_SECURITY_SAVED_REQUEST";
  12. private String sessionAttrName = DEFAULT_SAVED_REQUEST_ATTR;
  13. /**
  14. * Creates a new instance
  15. *
  16. * @param clientRegistrationRepository the repository to resolve the
  17. * {@link ClientRegistration}
  18. */
  19. public SaveRequestServerOAuth2AuthorizationRequestResolver(
  20. ReactiveClientRegistrationRepository clientRegistrationRepository) {
  21. super(clientRegistrationRepository);
  22. }
  23. @Override
  24. public Mono<OAuth2AuthorizationRequest> resolve(ServerWebExchange exchange) {
  25. return super.resolve(exchange)
  26. .doOnNext(OAuth2AuthorizationRequest -> {
  27. // 获取query参数redirect_uri
  28. Optional.ofNullable(exchange.getRequest())
  29. .map(ServerHttpRequest::getQueryParams)
  30. .map(queryParams -> queryParams.get(PARAM_REDIRECT_URI))
  31. .filter(redirectUris -> !CollectionUtils.isEmpty(redirectUris))
  32. .map(redirectUris -> redirectUris.get(0))
  33. .ifPresent(redirectUri -> {
  34. //若redirect_uri非空,则覆盖Session中的SPRING_SECURITY_SAVED_REQUEST为redirect_uri
  35. //即后续认证成功后可重定向回前端指定页面
  36. exchange.getSession().subscribe(webSession -> {
  37. webSession.getAttributes().put(this.sessionAttrName, redirectUri);
  38. logger.debug(LogMessage.format("SCG OAuth2 authorization endpoint queryParam redirect_uri added to WebSession: '%s'", redirectUri));
  39. });
  40. });
  41. });
  42. }
  43. }
 4.1.5. 编写OAuth2User实现类
  1. public class CustomUser implements OAuth2User, Serializable {
  2. private Map<String, Object> attributes;
  3. private Collection<? extends GrantedAuthority> authorities;
  4. private String name;
  5. public CustomUser(Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities, String name) {
  6. this.attributes = attributes;
  7. this.authorities = authorities;
  8. this.name = name;
  9. }
  10. public CustomUser() {
  11. }
  12. @Override
  13. public Map<String, Object> getAttributes() {
  14. return attributes;
  15. }
  16. @Override
  17. public Collection<? extends GrantedAuthority> getAuthorities() {
  18. return authorities;
  19. }
  20. @Override
  21. public String getName() {
  22. return name;
  23. }
  24. public void setAttributes(Map<String, Object> attributes) {
  25. this.attributes = attributes;
  26. }
  27. public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
  28. this.authorities = authorities;
  29. }
  30. public void setName(String name) {
  31. this.name = name;
  32. }
  33. }
 4.1.6. 编写url白名单配置类
  1. @Configuration
  2. @ConfigurationProperties(prefix = "system")
  3. public class UrlConfig {
  4. // 配置文件使用list接收
  5. private List<String> whiteList;
  6. public List<String> getWhiteList() {
  7. return whiteList;
  8. }
  9. public void setWhiteList(List<String> whiteList) {
  10. this.whiteList = whiteList;
  11. }
  12. }
4.1.7.  编写userInfo过滤器
  1. public class UserInfoRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
  2. private final static String USER_INFO_HEADER = "userInfo";
  3. public UserInfoRelayGatewayFilterFactory() {
  4. super(Object.class);
  5. }
  6. public GatewayFilter apply() {
  7. return apply((Object) null);
  8. }
  9. @Override
  10. public GatewayFilter apply(Object config) {
  11. return (exchange, chain) -> exchange.getPrincipal()
  12. // .log("token-relay-filter")
  13. .filter(principal -> principal instanceof OAuth2AuthenticationToken)
  14. .cast(OAuth2AuthenticationToken.class)
  15. //.flatMap(authentication -> authorizedClient(exchange, authentication))
  16. .map(OAuth2AuthenticationToken::getPrincipal)
  17. .map(oAuth2User -> withUserInfoHeader(exchange, oAuth2User))
  18. .defaultIfEmpty(exchange)
  19. .flatMap(chain::filter);
  20. }
  21. private ServerWebExchange withUserInfoHeader(ServerWebExchange exchange, OAuth2User oAuth2User) {
  22. //String userName = oAuth2User.getName();
  23. Map<String, Object> userAttrs = oAuth2User.getAttributes();
  24. if (oAuth2User instanceof OidcUser) {
  25. userAttrs = ((OidcUser) oAuth2User).getUserInfo().getClaims();
  26. }
  27. String userAttrsJson = JsonUtils.toJson(userAttrs);
  28. return exchange.mutate()
  29. .request(r -> r.headers(headers -> headers.add(USER_INFO_HEADER, userAttrsJson)))
  30. .build();
  31. }
  32. }
 4.1.8. 编写ReactiveOAuth2UserService实现类
  1. @Component
  2. public class CustomOAuth2UserService implements ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> {
  3. private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
  4. private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
  5. private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
  6. private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<Map<String, Object>>() {
  7. };
  8. private static final ParameterizedTypeReference<Map<String, String>> STRING_STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
  9. };
  10. private WebClient webClient = WebClient.create();
  11. @Override
  12. public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
  13. return Mono.fromCallable(() -> {
  14. String tokenStr = userRequest.getAccessToken().getTokenValue();
  15. try {
  16. SignedJWT sjwt = SignedJWT.parse(tokenStr);
  17. JWTClaimsSet claims = sjwt.getJWTClaimsSet();
  18. claims.getSubject();
  19. Collection<? extends GrantedAuthority> res = new ArrayList<>();
  20. CustomUser customUser=new CustomUser( claims.getClaims(),res,claims.getSubject());
  21. return customUser;
  22. } catch (ParseException e) {
  23. e.printStackTrace();
  24. throw new OAuth2AuthenticationException(new OAuth2Error("500"),"服务器返回错误的jwt");
  25. }
  26. });
  27. }
  28. }

4.2. 资源服务器核心代码

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

5. 登录测试

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

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

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

认证成功后重定向至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博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/83487
推荐阅读
相关标签
  

闽ICP备14008679号