一.分布式系统的认证授权大致架构
以云音乐系统为例:
注:一般情况下,我们会把认证这部分的接口提取为一个单独的认证服务模块中。
二.单点登录(Single Sign On)
单点登录,Single Sign On,简称为 SSO。
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
就比如说淘宝和天猫是两个系统,但有了单点登录统一认证后,我们在登录淘宝后,发现天猫也可以自动登录使用。
这样用户只需要认证一次便可以在多个拥有访问权限的系统服务中访问,提高了用户体验性。这个功能就叫做单点登录。
从实现角度简单说就是原来每个系统都需要一个认证服务,现在把这些认证服务集中在一个系统服务中统一认证即可。
好处:
用户角度 :用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。
系统管理员角度 : 管理员只需维护好一个统一的账号中心就可以了,方便。
新系统开发角度: 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。
三.使用JWT(JSON Web Token)
【1】JWT简单说就是一串JSON字符串,一串经过加密的签名密钥,里面包含了用户的身份信息。相比于基于session的传统方法,使用JWT我们可以无状态认证。
【2】传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,并且加大了服务端的存储压力。
并且校验令牌需要远程请求认证服务,客户端的每次访问都会远程校验,执行性能低,这种方式不适合在分布式系统中应用。
虽然有Session复制、Session黏贴等解决问题的方式,但是并不好用。
【3】使用JWT我们可以无状态认证。JWT能够让资源服务自己校验令牌的合法性将省去远程请求认证服务的成本,提高了性能,并且节省了存储的成本。
用户认证通过后会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。
四.引入网关
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
1.鉴权思路分析
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
每个微服务都需要知道JWT的秘钥,不安全
每个微服务重复编写登录校验代码、权限校验代码,麻烦
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把各个微服务中登录校验(认证)的工作提取到网关去做,这样之前说的问题就解决了:只需要在网关开发登录校验功能。
进一步改进在网关中我们还可以把我们jwt解析到的信息发给各个微服务,这样就不用再进行一次解析。并且只需要在网关和用户服务保存秘钥。
这样上面说的两个问题都解决了。
具体流程:
2.总结网关过滤器的职责
1、网站白名单维护
针对不用认证的URL全部放行。
2、校验jwt的合法性。
除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。
3.认证
可以采用白名单,将用户登录时候token的uuid存入redis,登出删除,每次登录判断是否在redis中。有才通过。
也可以采用黑名单。登出的token存入redis中,每次登录判断是否有在redis中,没有通过。
4.传递用户信息
将jwt中的用户信息,如用户id添加进请求头中。传递到微服务后通过过滤器存入TreadLocal。
总结流程:
对白名单放行->校验token(有无token,是否合法)->认证【用户是否在系统中,是否退出系统】->传递用户信息给各个微服务【避免在微服务中重复校验token】
3.网关不负责授权
网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。
4.网关过滤器介绍及代码书写
可以看到网关主要用于路径匹配以及作为统一的过滤器。
网关过滤器链中的过滤器有两种:GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
.GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
注意:过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。
其实GatewayFilter
和GlobalFilter
这两种过滤器的方法签名完全一致:
/**
* 处理请求并将其传递给下一个过滤器
* @param exchange 当前请求的上下文,其中包含request、response等各种数据
* @param chain 过滤器链,基于它向下传递请求
* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
【1】Gateway
中内置了33种GatewayFilter,我们只需书写配置即可使用。例:
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value
【2】无论是GatewayFilter
还是GlobalFilter
都支持自定义,只不过编码方式、使用方式略有差别。一般的话我们实现上述功能使用都是继承GlobalFilter进行自定义。
具体代码
@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
UserClients userClients;
//白名单
private static final List<String> whitelist = new ArrayList<>();
static {
//加载白名单
loadWhitelist();
}
//filter方法是GlobalFilter接口中定义的核心方法
// 用于实现过滤逻辑。当请求经过GlobalFilter时,会调用该方法对请求进行处理。
// 在filter方法中,你可以对请求进行修改、添加头信息、验证身份等操作,
// 也可以对响应进行处理,比如添加自定义的数据、修改响应状态码等。
//认证的基本逻辑:对登录之类的部分接口放行,除此之外
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
ServerHttpResponse response = exchange.getResponse();
AntPathMatcher pathMatcher = new AntPathMatcher();
//1.白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
System.out.println(requestUrl);
//2.检查token是否存在
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if(StringUtils.isBlank(tokenStr)){
return getVoidMono(response, Result.error(401,"token为空"));
}
//3.判断是否是有效的token
String userId=null;
try {
Claims claims = JwtUtil.parseJwt(tokenStr);
userId=claims.getSubject();
//4.判断是否在黑名单里
String uuid=claims.getId();
if(userClients.uuidIsInBlackListOrNot(uuid)){
return getVoidMono(response, Result.error(401, "该token已登出"));
}
} catch (Exception exception) {
exception.printStackTrace();
return getVoidMono(response,Result.error(401,"身份过期或身份证明非法"));
}
//5.传递用户id给各个微服务,避免再次解析token
String finalUserId = userId;
ServerWebExchange swe=exchange.mutate()
.request(builder -> builder.header("userId", finalUserId))
.build();
//6.检查通过放行
return chain.filter(swe);
}
//getOrder方法是GlobalFilter接口中的另一个方法,用于指定过滤器的执行顺序。Gateway会根据过滤器的order值进行排序,order值越小,
// 优先级越高,会先执行。可以通过重写getOrder方法来设置过滤器的执行顺序,确保过滤器按照预期的顺序执行。
@Override
public int getOrder() {
return 0;
}
//响应式的response包装
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse,Result result) {
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
private static void loadWhitelist() {
try (InputStream resourceAsStream = AuthFilter.class.getResourceAsStream("/filter/whitelist.properties")) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist.addAll(strings);
} catch (Exception e) {
log.error("加载/whitelist.properties出错:{}", e.getMessage());
}
}
}
五.OAuth 2.0--授权第三方平台/获取第三方平台权限
OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。而 OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。
实际上它就是一种授权机制,它的最终目的是为第三方应用颁发一个有时效性的令牌 Token,使得第三方应用能够通过该令牌获取自身相关的资源。
OAuth 2.0 比较常用的场景就是第三方登录以及常见于支付场景(微信支付、支付宝支付)和开发平台(微信开放平台、阿里开放平台等等)。