目录
背景
实现
gateway
maven配置
yml配置
页面登录拦截配置类
白名单配置
token工具类
登录配置类
全局过滤器类
项目启动类
分布式项目的单点登录分为认证服务(单点登录服务端)和业务服务(单点登录客户端)两个角色,
当访问业务服务时,认证服务客户端SDK校验一下是否有登录token,如果没有登录token,需要携带当前请求链接重定向到认证服务,认证通过后由认证服务重定向业务服务链接,实现单点登录。
gateway实现单点登录客户端功能,一般如果前后端项目是分离的,如果请求中没有携带登录token,直接返回需要认证,前后端没有分离的项目,可以做页面重定向操作。
本文主要讨论gateway的实现,认证服务需要自行实现。
注册中心、配置中心用的nacos
nacos官网:home
配置可以参考:Springcloud+Druid+Mybatis+Seata+Nacos动态切换多数据源,分布式事务的实现_殷长庆的博客-CSDN博客_seata多数据源切换
主要集成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
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的获取和校验工具
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);
}
}