前言
在现在微服务架构中,我们常常将鉴权的工作交由 网关 来处理。那么类似于 登录、注册 的免鉴权接口,我们要如何实现呢?
鉴权方案
JWT 介绍
首先,我们先简单了解一下 JWT 的原理。
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
- 头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON 对象。
{"typ":"JWT","alg":"HS256"}
在头部指明了签名算法是 HS256 算法。 我们进行 BASE64编码 能得到如下结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- 载荷(playload)
载荷就是存放有效信息的地方,这里会在 生成 JWT时将 信息进行编码,可以将我们可能需要用到的东西存在这里面,如 userId、username 等,将来可以在解析后使用
定义一个 payload:
{"sub":"1234567890","name":"lucy","admin":true,"age":18}
然后将其进行 base64 编码,得到Jwt的第二部分。
base64 是双向的,所以前两部分是可以进行解密的
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==
- 签证(signature)
jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要 base64加密 后的 header 和 base64 加密后的payload使用 . 连接组成的字符串,然后通过 header 中声明的 加密方式 进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
hs256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Iml0bGlscyIsImFkbWluIjp0cnVlLCJhZ2UiOjE4fQ==",secret)
将这三部分用 ' . ' (点) 连接成一个完整的字符串,构成了最终的 JWT
JTdCJTIydHlwJTIyJTNBJTIySldUJTIyJTJDJTIyYWxnJTIyJTNBJTIySFMyNTYlMjIlN0Q=.JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
一般鉴权流程
微服务鉴权流程
其实微服务的鉴权流程没有太大差别,只是 鉴权的位置 不一样了。
一般我们会在 网关 gateway 里完成 验证、解析、处理等操作
所以用户的请求也都会向这里发送
免鉴权方案
网关代码示例
我们一般使用网关的 全局过滤器(Filter) 来完成这个工作,上代码
/**
* jwt 认证过滤器
*
* @author durance
*/
@Component
@Slf4j
public class AuthorizeFilter implements GlobalFilter, Ordered {
public static final String AUTHORIZE_TOKEN = "token";
private Set<String> matchersCheck;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 进行请求路径判度,放行不需要认证的接口
String path = request.getURI().getPath();
// 拿到jwt的值
String jwt = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
// token不为空时,优先解析token
if (!StringUtils.isEmpty(jwt)) {
// 如果不为空则进行效验
try {
// 效验jwt正确性,如果错误会抛出异常
JwtUtil.parseJwt(jwt);
// 解析jwt拿到jwt的载荷跟其余信息
Claims claims = JwtUtil.parseJwt(jwt);
Integer userId = (Integer) claims.get("userId");
ServerHttpRequest build = exchange.getRequest().mutate().header("userId", userId.toString()).build();
exchange = exchange.mutate().request(build).build();
// 只要token解析正确就进行返回
return chain.filter(exchange);
} catch (Exception e) {
// 出现异常可能是token过期或恶意攻击
log.warn("jwt解析错误:{}", e.getMessage());
// 如果解析错误(过期了也会解析错误),判断是否为免鉴权接口
if (verifyNoAuthentication(path)){
return chain.filter(exchange);
}
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
}
// 没有token,则判断是否为免鉴权接口
if(verifyNoAuthentication(path)){
return chain.filter(exchange);
}
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
/**
* 判断接口是否为免鉴权接口
*
* @param path 请求接口
* @return 判断结果
*/
private boolean verifyNoAuthentication(String path){
//将不需要认证的接口存储在 Set中,减少判断是否为非鉴权接口的时间复杂度
if(CollectionUtils.isEmpty(matchersCheck)){
matchersCheck = new HashSet<>();
String[] matchers = JwtProperties.matchers;
matchersCheck.addAll(Arrays.asList(matchers));
}
// 为非鉴权接口直接跳过, 否则返回未认证
return matchersCheck.contains(path);
}
@Override
public int getOrder() {
// 过滤器优先级,越小越先
return -1;
}
}
接口免鉴权操作流程
可以根据上诉代码来阅读逻辑:
- 这里我们使用一个配置类 来记录要免鉴权的接口
- 使用字符串数组 matchers 来定义多个不需要鉴权的接口。
- 这里我们使用系统提供的方法 request.getURI().getPath() 统就能得到用户请求的接口
- 使用 Set 来记录免鉴权的接口,减低判断的 时间复杂度 O(1)
- 如果传入 token 不为空,则进行效验,返回对应的结果
- 如果传入 token 为空,则判断是否为 免鉴权接口,是则通过
由此完成鉴权的操作
鉴权后处理
因为 JWT 不光光能够用来验证,我们也需要拿到验证之后的一些结果。
也就是当时在生成 JWT时存入的载荷。我们也需要将它解析后 再次存入请求头,其它的服务才能够正确地拿到对应的信息。如 userId 等,可以见上诉代码示例
优化
除去运行效率的优化,那么代码是否能够更优雅的去获取免鉴权的接口呢?
既然是 配置类,如果我们是将免鉴权的接口直接 硬编码 在上面那也太 low 了。
所以,我们可以使用 SpringBoot 提供的 @ConfigurationProperties(建议) 或者 @Value 来把免鉴权的接口获取进来。减低耦合程度。
当然,这些配置我们都可以在 nacos配置中心 或其他配置中心 里进行配置,方便管理。