当前位置:   article > 正文

Springcloud gateway网关+认证服务+token方式,入口层认证统一微服务鉴权【设计实践】_spring boot 2.7.6 spring-cloud-starter-gateway

spring boot 2.7.6 spring-cloud-starter-gateway

目录

背景

实现

gateway

maven配置

yml配置

页面登录拦截配置类

白名单配置

token工具类

登录配置类

全局过滤器类

项目启动类


背景

分布式项目的单点登录分为认证服务(单点登录服务端)和业务服务(单点登录客户端)两个角色,

当访问业务服务时,认证服务客户端SDK校验一下是否有登录token,如果没有登录token,需要携带当前请求链接重定向到认证服务,认证通过后由认证服务重定向业务服务链接,实现单点登录。

gateway实现单点登录客户端功能,一般如果前后端项目是分离的,如果请求中没有携带登录token,直接返回需要认证,前后端没有分离的项目,可以做页面重定向操作。

本文主要讨论gateway的实现,认证服务需要自行实现。

实现

gateway

注册中心、配置中心用的nacos

nacos官网:home

配置可以参考:Springcloud+Druid+Mybatis+Seata+Nacos动态切换多数据源,分布式事务的实现_殷长庆的博客-CSDN博客_seata多数据源切换

maven配置

主要集成nacos注册、配置中心和gateway网关

  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.alibaba.cloud</groupId>
  7. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-actuator</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.projectlombok</groupId>
  15. <artifactId>lombok</artifactId>
  16. <optional>true</optional>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.cloud</groupId>
  20. <artifactId>spring-cloud-starter-gateway</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-configuration-processor</artifactId>
  25. <optional>true</optional>
  26. </dependency>
  27. <!-- 转发ws协议请求 -->
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-websocket</artifactId>
  31. <exclusions>
  32. <exclusion>
  33. <groupId>org.springframework.boot</groupId>
  34. <artifactId>spring-boot-starter-web</artifactId>
  35. </exclusion>
  36. </exclusions>
  37. </dependency>

yml配置

  1. server:
  2. port: 8888
  3. spring:
  4. profiles:
  5. active: dev
  6. application:
  7. name: luckgateway
  8. cloud:
  9. nacos:
  10. discovery:
  11. server-addr: 127.0.0.1:8848
  12. namespace: luck-cloud
  13. config:
  14. server-addr: 127.0.0.1:8848
  15. file-extension: yaml
  16. namespace: luck-cloud
  17. gateway:
  18. routes:
  19. - id: lucksso
  20. uri: lb://lucksso
  21. predicates:
  22. - Path=/lucksso/**
  23. - id: luckbiz
  24. uri: lb://luckbiz
  25. predicates:
  26. - Path=/luckbiz/**
  27. - id: luckim
  28. uri: lb:ws://luckim
  29. predicates:
  30. - Path=/luckim/**
  31. discovery:
  32. locator:
  33. enabled: true #开启从注册中心动态创建路由的功能,默认false,true时网关转发的微服务链接不带path前缀
  34. lower-case-service-id: true #使用小写服务名,默认是大写
  35. secure:
  36. ignore:
  37. urls: #配置白名单路径
  38. - "/actuator/**"
  39. - "/lucksso/**"
  40. - "/resources/**"
  41. page:
  42. urls: #配置需要登录的页面路径
  43. - "/**"

页面登录拦截配置类

  1. package com.luck.config;
  2. import java.util.List;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.stereotype.Component;
  5. import lombok.Data;
  6. import lombok.EqualsAndHashCode;
  7. /**
  8. * 页面登录拦截配置
  9. */
  10. @Data
  11. @EqualsAndHashCode(callSuper = false)
  12. @Component
  13. @ConfigurationProperties(prefix = "secure.page")
  14. public class PageUrlsConfig {
  15. private List<String> urls;
  16. }

白名单配置

  1. package com.luck.config;
  2. import java.util.List;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.stereotype.Component;
  5. import lombok.Data;
  6. import lombok.EqualsAndHashCode;
  7. /**
  8. * 白名单配置
  9. */
  10. @Data
  11. @EqualsAndHashCode(callSuper = false)
  12. @Component
  13. @ConfigurationProperties(prefix = "secure.ignore")
  14. public class IgnoreUrlsConfig {
  15. private List<String> urls;
  16. }

token工具类

token的获取和校验工具

  1. package com.luck.config;
  2. import org.apache.commons.lang3.StringUtils;
  3. import org.springframework.http.HttpCookie;
  4. import org.springframework.http.server.reactive.ServerHttpRequest;
  5. import org.springframework.web.server.ServerWebExchange;
  6. /**
  7. * 登录token工具类
  8. */
  9. public class LoginTokenUtil {
  10. /** 登录服务地址 */
  11. public static final String LOGIN_PATH = "/lucksso/login?servers=";
  12. /** 登录成功后的回调接口地址 */
  13. public static final String LOGIN_CALLBACK_PATH = "/sso/login";
  14. /** 登录成功后的回调接口的token参数名称 */
  15. public static final String LOGIN_CALLBACK_TOKEN = "token";
  16. /** 登录成功后的回调接口的回调地址参数名称 */
  17. public static final String LOGIN_CALLBACK_URL = "url";
  18. /** 登录成功后的token名称 */
  19. public static final String LOGIN_TOKEN_NAME = "luckToken";
  20. /**
  21. * 获取登录token
  22. * @param exchange 上下文
  23. * @return
  24. */
  25. public static String getLoginToken(ServerWebExchange exchange) {
  26. if (null == exchange) {
  27. return null;
  28. }
  29. ServerHttpRequest request = exchange.getRequest();
  30. String loginToken = request.getHeaders().getFirst(LOGIN_TOKEN_NAME);
  31. if (StringUtils.isBlank(loginToken)) {
  32. Object token = exchange.getAttribute(LOGIN_TOKEN_NAME);
  33. if (null != token) {
  34. loginToken = (String) token;
  35. }
  36. }
  37. if (StringUtils.isBlank(loginToken)) {
  38. loginToken = request.getQueryParams().getFirst(LOGIN_TOKEN_NAME);
  39. }
  40. if (StringUtils.isBlank(loginToken)) {
  41. HttpCookie loginCookie = request.getCookies().getFirst(LOGIN_TOKEN_NAME);
  42. if (null != loginCookie) {
  43. loginToken = loginCookie.getValue();
  44. }
  45. }
  46. return loginToken;
  47. }
  48. /**
  49. * 校验登录token是否有效
  50. * @param loginToken 登录token
  51. * @return
  52. */
  53. public static boolean validateLoginToken(String loginToken) {
  54. if (StringUtils.isNoneBlank(loginToken)) {
  55. // do something
  56. return true;
  57. }
  58. return false;
  59. }
  60. }

登录配置类

实现单点登录客户端核心逻辑,根据请求判断有没有登录token,可以从请求头获取或者cookie、链接参数获取,如果没有则重定向到认证服务,认证服务实现登录逻辑,回调网关接口,完成登录

  1. package com.luck.config;
  2. import java.io.UnsupportedEncodingException;
  3. import java.net.URI;
  4. import java.net.URLDecoder;
  5. import java.net.URLEncoder;
  6. import java.util.List;
  7. import org.apache.commons.lang3.StringUtils;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.http.HttpHeaders;
  12. import org.springframework.http.HttpStatus;
  13. import org.springframework.http.server.reactive.ServerHttpRequest;
  14. import org.springframework.http.server.reactive.ServerHttpResponse;
  15. import org.springframework.util.AntPathMatcher;
  16. import org.springframework.util.PathMatcher;
  17. import org.springframework.web.server.ServerWebExchange;
  18. import org.springframework.web.server.WebFilter;
  19. import org.springframework.web.server.WebFilterChain;
  20. import reactor.core.publisher.Mono;
  21. @Configuration
  22. public class LoginConfig {
  23. /** 路由匹配器 */
  24. public static PathMatcher pathMatcher = new AntPathMatcher();
  25. /** 白名单 */
  26. @Autowired
  27. private IgnoreUrlsConfig ignoreUrlsConfig;
  28. /** 拦截名单 */
  29. @Autowired
  30. private PageUrlsConfig pageUrlsConfig;
  31. @FunctionalInterface
  32. public interface LoginFunction {
  33. public Mono<Void> run();
  34. }
  35. @Bean
  36. public WebFilter getSsoLoginFilter() {
  37. return new WebFilter() {
  38. @Override
  39. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  40. try {
  41. ServerHttpRequest request = exchange.getRequest();
  42. String path = request.getPath().toString();
  43. if (LoginTokenUtil.LOGIN_CALLBACK_PATH.equals(path)) {
  44. String loginToken = request.getQueryParams().getFirst(LoginTokenUtil.LOGIN_CALLBACK_TOKEN);
  45. String loginUrl = request.getQueryParams().getFirst(LoginTokenUtil.LOGIN_CALLBACK_URL);
  46. if (StringUtils.isAnyBlank(loginToken, loginUrl)) {
  47. throw new RuntimeException("参数错误!");
  48. }
  49. String url = URLDecoder.decode(loginUrl, "UTF-8");
  50. boolean validateLoginToken = LoginTokenUtil.validateLoginToken(loginToken);
  51. if (validateLoginToken) {
  52. ServerHttpResponse response = exchange.getResponse();
  53. response.setStatusCode(HttpStatus.SEE_OTHER);// 校验成功,重定向
  54. response.getHeaders().set(HttpHeaders.LOCATION, url);
  55. return exchange.getResponse().setComplete();
  56. }
  57. // 校验失败,抛异常或者重新执行登录
  58. // redirectSSO(exchange, request, url);
  59. throw new RuntimeException("token校验失败!");
  60. }
  61. String loginToken = LoginTokenUtil.getLoginToken(exchange);
  62. Mono<Void> result = null;
  63. if (StringUtils.isNoneBlank(loginToken)) {
  64. boolean hasLogin = LoginTokenUtil.validateLoginToken(loginToken);
  65. if (!hasLogin) {
  66. result = redirectSSO(exchange, request, path);
  67. } else {
  68. return chain.filter(exchange);
  69. }
  70. } else {
  71. result = redirectSSO(exchange, request, path);
  72. }
  73. if (null != result) {
  74. return result;
  75. }
  76. throw new RuntimeException("token校验失败!");
  77. } catch (Exception e) {
  78. exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
  79. return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
  80. }
  81. }
  82. /**
  83. * 调单点登录
  84. * @param exchange 上下文
  85. * @param request 请求
  86. * @param path 请求地址
  87. * @return
  88. */
  89. private Mono<Void> redirectSSO(ServerWebExchange exchange, ServerHttpRequest request, String path) {
  90. Mono<Void> result = match(pageUrlsConfig.getUrls(), ignoreUrlsConfig.getUrls(), path, () -> {
  91. if (isPage(path)) {
  92. URI uri = request.getURI();
  93. String url = "/";
  94. try {
  95. url = LoginTokenUtil.LOGIN_PATH // 登录服务地址
  96. + URLEncoder.encode(uri.getScheme() + "://" + uri.getAuthority() // gateway服务(http://gateway)
  97. + LoginTokenUtil.LOGIN_CALLBACK_PATH + "?" + LoginTokenUtil.LOGIN_CALLBACK_URL + "=" // gateway回调地址参数 http://gateway/sso/login?url=
  98. + URLEncoder.encode(uri.toString(), "UTF-8"), "UTF-8");// 登录成功重定向地址
  99. } catch (UnsupportedEncodingException e1) {
  100. e1.printStackTrace();
  101. }
  102. ServerHttpResponse response = exchange.getResponse();
  103. response.setStatusCode(HttpStatus.SEE_OTHER);
  104. response.getHeaders().set(HttpHeaders.LOCATION, url);
  105. exchange.getResponse().getHeaders().set("Content-Type", "text/plain; charset=utf-8");
  106. return exchange.getResponse().setComplete();
  107. }
  108. return null;
  109. });
  110. return result;
  111. }
  112. };
  113. }
  114. /**
  115. * 判断请求是不是页面请求
  116. * @param path 请求路径
  117. * @return
  118. */
  119. private boolean isPage(String path) {
  120. return true;
  121. }
  122. /**
  123. * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数
  124. * @param patterns 路由匹配符集合
  125. * @param excludePatterns 要排除的路由匹配符集合
  126. * @param path 请求链接
  127. * @param function 要执行的方法
  128. */
  129. public Mono<Void> match(List<String> patterns, List<String> excludePatterns, String path, LoginFunction function) {
  130. if (isMatchCurrURI(patterns, path)) {
  131. if (isMatchCurrURI(excludePatterns, path) == false) {
  132. return function.run();
  133. }
  134. }
  135. return null;
  136. }
  137. /**
  138. * 路由匹配 (使用当前URI)
  139. * @param patterns 路由匹配符集合
  140. * @param path 被匹配的路由
  141. * @return 是否匹配成功
  142. */
  143. public boolean isMatchCurrURI(List<String> patterns, String path) {
  144. return isMatch(patterns, path);
  145. }
  146. /**
  147. * 路由匹配
  148. * @param patterns 路由匹配符集合
  149. * @param path 被匹配的路由
  150. * @return 是否匹配成功
  151. */
  152. public boolean isMatch(List<String> patterns, String path) {
  153. for (String pattern : patterns) {
  154. if (isMatch(pattern, path)) {
  155. return true;
  156. }
  157. }
  158. return false;
  159. }
  160. /**
  161. * 路由匹配
  162. * @param pattern 路由匹配符
  163. * @param path 被匹配的路由
  164. * @return 是否匹配成功
  165. */
  166. public boolean isMatch(String pattern, String path) {
  167. return pathMatcher.match(pattern, path);
  168. }
  169. }

全局过滤器类

主要是为网关转发请求时添加登录token

  1. package com.luck.config;
  2. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  3. import org.springframework.cloud.gateway.filter.GlobalFilter;
  4. import org.springframework.http.server.reactive.ServerHttpRequest;
  5. import org.springframework.stereotype.Component;
  6. import org.springframework.web.server.ServerWebExchange;
  7. import reactor.core.publisher.Mono;
  8. /**
  9. * 全局过滤器,添加登录token请求头
  10. */
  11. @Component
  12. public class ForwardAuthFilter implements GlobalFilter {
  13. @Override
  14. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  15. ServerHttpRequest request = exchange//
  16. .getRequest()//
  17. .mutate()//
  18. .header(LoginTokenUtil.LOGIN_TOKEN_NAME, LoginTokenUtil.getLoginToken(exchange))//
  19. .build();//
  20. ServerWebExchange newExchange = exchange.mutate().request(request).build();
  21. return chain.filter(newExchange);
  22. }
  23. }

项目启动类

  1. package com.luck;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  5. @EnableDiscoveryClient
  6. @SpringBootApplication
  7. public class GatewayApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(GatewayApplication.class, args);
  10. }
  11. }

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

闽ICP备14008679号