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

目录

背景

实现

gateway

maven配置

yml配置

页面登录拦截配置类

白名单配置

token工具类

登录配置类

全局过滤器类

项目启动类


背景

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

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

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

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

实现

gateway

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

nacos官网:home

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

maven配置

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


        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
            org.projectlombok
            lombok
            true
        

        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
        
            org.springframework.boot
            spring-boot-configuration-processor
            true
        
        
        
            org.springframework.boot
            spring-boot-starter-websocket
            
                
                    org.springframework.boot
                    spring-boot-starter-web
                
            
        

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 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 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 run();
	}

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

			@Override
			public Mono 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 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 redirectSSO(ServerWebExchange exchange, ServerHttpRequest request, String path) {
				Mono 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 match(List patterns, List 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 patterns, String path) {
		return isMatch(patterns, path);
	}

	/**
	 * 路由匹配   
	 * @param patterns 路由匹配符集合 
	 * @param path 被匹配的路由  
	 * @return 是否匹配成功 
	 */
	public boolean isMatch(List 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 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);
	}

}

你可能感兴趣的:(微服务,spring,cloud,gateway)