Springcloud gateway网关+认证服务+token方式,入口层认证统一微服务鉴权【设计实践】

11 篇文章 0 订阅
8 篇文章 0 订阅

目录

背景

实现

gateway

maven配置

yml配置

页面登录拦截配置类

白名单配置

token工具类

登录配置类

全局过滤器类

项目启动类


背景

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

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

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

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

实现

gateway

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

nacos官网:home

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

maven配置

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


        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 转发ws协议请求 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

yml配置

server:
  port: 8888
spring:
  profiles:
    active: dev
  application:
    name: luckgateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: luck-cloud
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        namespace: luck-cloud
    gateway:
      routes:
        - id: lucksso
          uri: lb://lucksso
          predicates:
            - Path=/lucksso/**
        - id: luckbiz
          uri: lb://luckbiz
          predicates:
            - Path=/luckbiz/**
        - id: luckim
          uri: lb:ws://luckim
          predicates:
            - Path=/luckim/**
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,默认false,true时网关转发的微服务链接不带path前缀
          lower-case-service-id: true #使用小写服务名,默认是大写

secure:
  ignore:
    urls: #配置白名单路径
      - "/actuator/**"
      - "/lucksso/**"
      - "/resources/**"
  page:
    urls: #配置需要登录的页面路径
      - "/**"

页面登录拦截配置类

package com.luck.config;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 页面登录拦截配置
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix = "secure.page")
public class PageUrlsConfig {
	private List<String> urls;
}

白名单配置

package com.luck.config;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 白名单配置
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Component
@ConfigurationProperties(prefix = "secure.ignore")
public class IgnoreUrlsConfig {
	private List<String> urls;
}

token工具类

token的获取和校验工具

package com.luck.config;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;

/**
 * 登录token工具类
 */
public class LoginTokenUtil {
	/** 登录服务地址 */
	public static final String LOGIN_PATH = "/lucksso/login?servers=";
	/** 登录成功后的回调接口地址 */
	public static final String LOGIN_CALLBACK_PATH = "/sso/login";
	/** 登录成功后的回调接口的token参数名称 */
	public static final String LOGIN_CALLBACK_TOKEN = "token";
	/** 登录成功后的回调接口的回调地址参数名称 */
	public static final String LOGIN_CALLBACK_URL = "url";

	/** 登录成功后的token名称 */
	public static final String LOGIN_TOKEN_NAME = "luckToken";

	/**
	 * 获取登录token
	 * @param exchange 上下文
	 * @return
	 */
	public static String getLoginToken(ServerWebExchange exchange) {

		if (null == exchange) {
			return null;
		}

		ServerHttpRequest request = exchange.getRequest();

		String loginToken = request.getHeaders().getFirst(LOGIN_TOKEN_NAME);

		if (StringUtils.isBlank(loginToken)) {
			Object token = exchange.getAttribute(LOGIN_TOKEN_NAME);
			if (null != token) {
				loginToken = (String) token;
			}
		}

		if (StringUtils.isBlank(loginToken)) {
			loginToken = request.getQueryParams().getFirst(LOGIN_TOKEN_NAME);
		}

		if (StringUtils.isBlank(loginToken)) {
			HttpCookie loginCookie = request.getCookies().getFirst(LOGIN_TOKEN_NAME);
			if (null != loginCookie) {
				loginToken = loginCookie.getValue();
			}
		}

		return loginToken;
	}

	/**
	 * 校验登录token是否有效
	 * @param loginToken 登录token
	 * @return
	 */
	public static boolean validateLoginToken(String loginToken) {
		if (StringUtils.isNoneBlank(loginToken)) {
			// do something
			return true;
		}
		return false;
	}

}

登录配置类

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

package com.luck.config;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Mono;

@Configuration
public class LoginConfig {
	/** 路由匹配器 */
	public static PathMatcher pathMatcher = new AntPathMatcher();
	/** 白名单 */
	@Autowired
	private IgnoreUrlsConfig ignoreUrlsConfig;
	/** 拦截名单 */
	@Autowired
	private PageUrlsConfig pageUrlsConfig;

	@FunctionalInterface
	public interface LoginFunction {
		public Mono<Void> run();
	}

	@Bean
	public WebFilter getSsoLoginFilter() {
		return new WebFilter() {

			@Override
			public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
				try {
					ServerHttpRequest request = exchange.getRequest();
					String path = request.getPath().toString();
					if (LoginTokenUtil.LOGIN_CALLBACK_PATH.equals(path)) {
						String loginToken = request.getQueryParams().getFirst(LoginTokenUtil.LOGIN_CALLBACK_TOKEN);
						String loginUrl = request.getQueryParams().getFirst(LoginTokenUtil.LOGIN_CALLBACK_URL);
						if (StringUtils.isAnyBlank(loginToken, loginUrl)) {
							throw new RuntimeException("参数错误!");
						}
						String url = URLDecoder.decode(loginUrl, "UTF-8");
						boolean validateLoginToken = LoginTokenUtil.validateLoginToken(loginToken);
						if (validateLoginToken) {
							ServerHttpResponse response = exchange.getResponse();
							response.setStatusCode(HttpStatus.SEE_OTHER);// 校验成功,重定向
							response.getHeaders().set(HttpHeaders.LOCATION, url);
							return exchange.getResponse().setComplete();
						}
						// 校验失败,抛异常或者重新执行登录
						// redirectSSO(exchange, request, url);
						throw new RuntimeException("token校验失败!");
					}
					String loginToken = LoginTokenUtil.getLoginToken(exchange);
					Mono<Void> result = null;
					if (StringUtils.isNoneBlank(loginToken)) {
						boolean hasLogin = LoginTokenUtil.validateLoginToken(loginToken);
						if (!hasLogin) {
							result = redirectSSO(exchange, request, path);
						} else {
							return chain.filter(exchange);
						}
					} else {
						result = redirectSSO(exchange, request, path);
					}
					if (null != result) {
						return result;
					}
					throw new RuntimeException("token校验失败!");
				} catch (Exception e) {
					exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
					return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
				}
			}

			/**
			 * 调单点登录
			 * @param exchange 上下文
			 * @param request 请求
			 * @param path 请求地址
			 * @return
			 */
			private Mono<Void> redirectSSO(ServerWebExchange exchange, ServerHttpRequest request, String path) {
				Mono<Void> result = match(pageUrlsConfig.getUrls(), ignoreUrlsConfig.getUrls(), path, () -> {
					if (isPage(path)) {
						URI uri = request.getURI();
						String url = "/";
						try {
							url = LoginTokenUtil.LOGIN_PATH // 登录服务地址
									+ URLEncoder.encode(uri.getScheme() + "://" + uri.getAuthority() // gateway服务(http://gateway)
											+ LoginTokenUtil.LOGIN_CALLBACK_PATH + "?" + LoginTokenUtil.LOGIN_CALLBACK_URL + "=" // gateway回调地址参数 http://gateway/sso/login?url=
											+ URLEncoder.encode(uri.toString(), "UTF-8"), "UTF-8");// 登录成功重定向地址
						} catch (UnsupportedEncodingException e1) {
							e1.printStackTrace();
						}
						ServerHttpResponse response = exchange.getResponse();
						response.setStatusCode(HttpStatus.SEE_OTHER);
						response.getHeaders().set(HttpHeaders.LOCATION, url);
						exchange.getResponse().getHeaders().set("Content-Type", "text/plain; charset=utf-8");
						return exchange.getResponse().setComplete();
					}
					return null;
				});
				return result;
			}

		};
	}

	/**
	 * 判断请求是不是页面请求
	 * @param path 请求路径
	 * @return
	 */
	private boolean isPage(String path) {
		return true;
	}

	/**
	 * 路由匹配 (并指定排除匹配符),如果匹配成功则执行认证函数 
	 * @param patterns 路由匹配符集合
	 * @param excludePatterns 要排除的路由匹配符集合
	 * @param path 请求链接
	 * @param function 要执行的方法 
	 */
	public Mono<Void> match(List<String> patterns, List<String> excludePatterns, String path, LoginFunction function) {
		if (isMatchCurrURI(patterns, path)) {
			if (isMatchCurrURI(excludePatterns, path) == false) {
				return function.run();
			}
		}
		return null;
	}

	/**
	 * 路由匹配 (使用当前URI) 
	 * @param patterns 路由匹配符集合 
	 * @param path 被匹配的路由  
	 * @return 是否匹配成功 
	 */
	public boolean isMatchCurrURI(List<String> patterns, String path) {
		return isMatch(patterns, path);
	}

	/**
	 * 路由匹配   
	 * @param patterns 路由匹配符集合 
	 * @param path 被匹配的路由  
	 * @return 是否匹配成功 
	 */
	public boolean isMatch(List<String> patterns, String path) {
		for (String pattern : patterns) {
			if (isMatch(pattern, path)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 路由匹配
	 * @param pattern 路由匹配符 
	 * @param path 被匹配的路由  
	 * @return 是否匹配成功 
	 */
	public boolean isMatch(String pattern, String path) {
		return pathMatcher.match(pattern, path);
	}
}

全局过滤器类

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

package com.luck.config;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

/**
 * 全局过滤器,添加登录token请求头
 */
@Component
public class ForwardAuthFilter implements GlobalFilter {
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		ServerHttpRequest request = exchange//
				.getRequest()//
				.mutate()//
				.header(LoginTokenUtil.LOGIN_TOKEN_NAME, LoginTokenUtil.getLoginToken(exchange))//
				.build();//
		ServerWebExchange newExchange = exchange.mutate().request(request).build();
		return chain.filter(newExchange);
	}
}

项目启动类

package com.luck;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayApplication.class, args);
	}

}

  • 8
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Cloud Gateway是一个基于Spring Boot 2.x的API网关,可以作为微服务架构中的统一入口,提供路由、转发、负载均衡、限流、降级、统一认证鉴权等功能。在实现统一认证鉴权时,可以结合Spring Security和JWT来实现。 具体实现步骤如下: 1. 引入Spring Security和JWT的依赖 在Spring Cloud Gateway的pom.xml文件中,引入Spring Security和JWT的依赖: ``` <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency> ``` 2. 配置Spring Security 在Spring Cloud Gateway的配置类中,配置Spring Security: ``` @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Autowired private JwtAuthenticationManager jwtAuthenticationManager; @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http.csrf().disable() .authorizeExchange() .pathMatchers("/login").permitAll() .anyExchange().authenticated() .and() .addFilterAt(new JwtAuthenticationFilter(jwtAuthenticationManager), SecurityWebFiltersOrder.AUTHENTICATION) .build(); } } ``` 在上面的配置中,首先禁用了CSRF防护,然后配置了登录接口不需要认证,其它接口都需要认证。最后添加了一个JWT认证过滤器。 3. 配置JWT 在Spring Cloud Gateway的配置类中,配置JWT: ``` @Configuration public class JwtConfig { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; @Bean public JwtAuthenticationManager jwtAuthenticationManager() { return new JwtAuthenticationManager(secret); } @Bean public JwtTokenGenerator jwtTokenGenerator() { return new JwtTokenGenerator(secret, expiration); } } ``` 在上面的配置中,配置了JWT的密钥和过期时间,并创建了JWT的管理器和生成器。 4. 实现登录接口 实现登录接口,生成JWT并返回给客户端: ``` @RestController public class LoginController { @Autowired private JwtTokenGenerator jwtTokenGenerator; @PostMapping("/login") public Mono<ResponseEntity<Map<String, String>>> login(@RequestBody LoginRequest loginRequest) { // 验证用户名和密码 if (validateUsernameAndPassword(loginRequest)) { // 生成JWT String token = jwtTokenGenerator.generateToken(loginRequest.getUsername()); // 返回JWT Map<String, String> responseBody = new HashMap<>(); responseBody.put("token", token); return Mono.just(ResponseEntity.ok(responseBody)); } else { return Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); } } private boolean validateUsernameAndPassword(LoginRequest loginRequest) { // 验证用户名和密码逻辑 } } ``` 在上面的代码中,先验证用户名和密码是否正确,如果正确则生成JWT并返回给客户端,否则返回401未授权状态码。 5. 实现JWT认证过滤器 实现JWT认证过滤器,从请求头中获取JWT并验证: ``` public class JwtAuthenticationFilter extends AuthenticationWebFilter { public JwtAuthenticationFilter(JwtAuthenticationManager jwtAuthenticationManager) { super(jwtAuthenticationManager); } @Override protected Mono<Void> onAuthSuccess(Authentication authentication, ServerWebExchange exchange) { return super.onAuthSuccess(authentication, exchange); } @Override protected Mono<Void> onAuthFailure(AuthenticationException e, ServerWebExchange exchange) { return super.onAuthFailure(e, exchange); } @Override public Mono<Void> filter(ServerWebExchange exchange, AuthenticationFilterChain chain) { String token = extractToken(exchange.getRequest().getHeaders().getFirst("Authorization")); if (StringUtils.isEmpty(token)) { return chain.filter(exchange); } else { JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(token); return super.filter(exchange, chain) .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(jwtAuthenticationToken)); } } private String extractToken(String header) { // 从Authorization头中提取JWT } } ``` 在上面的代码中,先从请求头中提取JWT,如果JWT为空则直接调用下一个过滤器,否则创建JwtAuthenticationToken并将其设置到SecurityContext中。 6. 实现JWT认证管理器 实现JWT认证管理器,验证JWT是否正确: ``` public class JwtAuthenticationManager implements ReactiveAuthenticationManager { private final String secret; public JwtAuthenticationManager(String secret) { this.secret = secret; } @Override public Mono<Authentication> authenticate(Authentication authentication) { String token = authentication.getCredentials().toString(); try { Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(token); String username = claimsJws.getBody().getSubject(); return Mono.just(new JwtAuthenticationToken(username, token)); } catch (JwtException e) { return Mono.error(e); } } } ``` 在上面的代码中,使用JWT解析器解析JWT,并验证签名和过期时间,如果验证通过则创建JwtAuthenticationToken。 7. 实现JWT认证令牌 实现JWT认证令牌: ``` public class JwtAuthenticationToken extends AbstractAuthenticationToken { private final String token; private final String username; public JwtAuthenticationToken(String token) { super(Collections.emptyList()); this.token = token; this.username = null; } public JwtAuthenticationToken(String username, String token) { super(Collections.emptyList()); this.token = token; this.username = username; setAuthenticated(true); } @Override public Object getCredentials() { return token; } @Override public Object getPrincipal() { return username; } } ``` 在上面的代码中,实现了AbstractAuthenticationToken的两个抽象方法,并添加了一个token和username属性。 8. 配置路由规则 最后,配置路由规则,启用Spring Cloud Gateway: ``` @Configuration public class GatewayConfig { @Autowired private JwtTokenGenerator jwtTokenGenerator; @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("login", r -> r.path("/login") .uri("http://localhost:8080/login")) .route("hello", r -> r.path("/hello") .filters(f -> f.requestHeader("Authorization", "Bearer " + jwtTokenGenerator.generateToken("user"))) .uri("http://localhost:8081/hello")) .build(); } } ``` 在上面的配置中,配置了两个路由规则,一个是登录接口,另一个是hello接口,hello接口需要通过JWT认证才能访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值