前言

这两三年项目中一直在使用比较流行的spring cloud框架,也算有一定积累,打算有时间就整理一些干货与大家分享。

本次分享zuul网关集成jwt身份验证

业务背景

项目开发少不了身份认证,jwt作为当下比较流行的身份认证方式之一主要的特点是无状态,把信息放在客户端,服务器端不需要保存session,适合分布式系统使用。

把jwt集成在网关的好处是业务工程不需要关心身份验证,专注业务逻辑(网关可验证token后,把解析出来的身份信息如userId,放在请求头传递给业务工程)。

顺便分享下如何自定义Zuul拦截器

代码详解

一、JwtUtil

为了方便,先封装好JwtUtil,主要包含两个方法,创建token和解析(并验证)token

这里引用了第三方的包jjwt,简单好用,maven依赖如下

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

jwtUtil封装如下

@Component
public class JwtUtil { /**
* 签名用的密钥
*/
private static final String SIGNING_KEY = "78sebr72umyz33i9876gc31urjgyfhgj"; /**
* 用户登录成功后生成Jwt
* 使用Hs256算法
*
* @param exp jwt过期时间
* @param claims 保存在Payload(有效载荷)中的内容
* @return token字符串
*/
public String createJWT(Date exp, Map<String, Object> claims) {
//指定签名的时候使用的签名算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis); //创建一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//保存在Payload(有效载荷)中的内容
.setClaims(claims)
//iat: jwt的签发时间
.setIssuedAt(now)
//设置过期时间
.setExpiration(exp)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, SIGNING_KEY); return builder.compact();
} /**
* 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期
*
* @param token
* @return
*/
public Claims parseJWT(String token) {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(SIGNING_KEY)
//设置需要解析的token
.parseClaimsJws(token).getBody();
return claims;
} }

二、自定义拦截器说明

继承自ZuulFilter,并注册到spring容器即可实现自定义拦截器,实现身份认证、参数校验、参数传递等功能

@Component
public class CustomFilter extends ZuulFilter { /**
* filterType:过滤器类型
* <p>
* pre:路由之前
* routing:路由之时
* post: 路由之后
* error:发送错误调用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
// return FilterConstants.POST_TYPE;
} /**
* filterOrder:过滤的顺序 序号配置可参照 https://blog.csdn.net/u010963948/article/details/100146656
*
* @return
*/
@Override
public int filterOrder() {
return 0;
} /**
* shouldFilter:判断是否要执行过滤
*
* @return true表示需要过滤,将对该请求执行run方法
*/
@Override
public boolean shouldFilter() {
return true;
} /**
* run:具体过滤的业务逻辑,可做身份验证,校验参数等等
*
* @return
*/
@Override
public Object run() throws ZuulException {
//获取请求上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = ctx.getRequest();
//获取response对象
HttpServletResponse response = ctx.getResponse();
//添加请求头,传递到业务服务
ctx.addZuulRequestHeader("xxx", "xxx");
//添加响应头,返回给前端
ctx.addZuulResponseHeader("xxx", "xxx");
return null;
}
}

三、LoginAddJwtPostFilter,拦截登录方法,登录成功时创建token,返回给前端

要点:

  1. 拦截类型是FilterConstants.POST_TYPE,在路由方法响应之后拦截
  2. 判断请求的uri是否是登录接口(与配置文件中设置的登录uri是否匹配),需要在配置文件配置登录接口地址
  3. 判断登录方法返回成功,创建token,并添加到 response body或response header,返回给前端
@Component
@Slf4j
public class LoginAddJwtPostFilter extends ZuulFilter { @Autowired
ObjectMapper objectMapper; @Autowired
JwtUtil jwtUtil; @Autowired
DataFilterConfig dataFilterConfig; /**
* pre:路由之前
* routing:路由之时
* post: 路由之后
* error:发送错误调用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
} /**
* filterOrder:过滤的顺序
*
* @return
*/
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 2;
} /**
* shouldFilter:这里可以写逻辑判断,是否要过滤
*
* @return
*/
@Override
public boolean shouldFilter() {
//路径与配置的相匹配,则执行过滤
RequestContext ctx = RequestContext.getCurrentContext();
for (String pathPattern : dataFilterConfig.getUserLoginPath()) {
if (PathUtil.isPathMatch(pathPattern, ctx.getRequest().getRequestURI())) {
return true;
}
}
return false;
} /**
* 执行过滤器逻辑,登录成功时给响应内容增加token
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
InputStream stream = ctx.getResponseDataStream();
String body = StreamUtils.copyToString(stream, StandardCharsets.UTF_8);
Result<HashMap<String, Object>> result = objectMapper.readValue(body, new TypeReference<Result<HashMap<String, Object>>>() {
});
//result.getCode() == 0 表示登录成功
if (result.getCode() == 0) {
HashMap<String, Object> jwtClaims = new HashMap<String, Object>() {{
put("userId", result.getData().get("userId"));
}};
Date expDate = DateTime.now().plusDays(7).toDate(); //过期时间 7 天
String token = jwtUtil.createJWT(expDate, jwtClaims);
//body json增加token
result.getData().put("token", token);
//序列化body json,设置到响应body中
body = objectMapper.writeValueAsString(result);
ctx.setResponseBody(body); //响应头设置token
ctx.addZuulResponseHeader("token", token);
}
} catch (Exception e) {
e.printStackTrace();
} return null;
}
}

四、JwtAuthPreFilter,拦截业务接口,验证token

要点:

  1. 拦截类型是FilterConstants.PRE_TYPE,在调用业务接口之前拦截
  2. 判断请求的uri是否是需要身份验证的接口(与配置文件中设置的uri是否匹配),需要在配置文件配置业务接口地址
  3. 判断token验证是否通过,通过则路由,不通过返回错误提示
@Component
@Slf4j
public class JwtAuthPreFilter extends ZuulFilter {
@Autowired
ObjectMapper objectMapper; @Autowired
JwtUtil jwtUtil; @Autowired
DataFilterConfig dataFilterConfig; /**
* pre:路由之前
* routing:路由之时
* post: 路由之后
* error:发送错误调用
*
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
} /**
* filterOrder:过滤的顺序 序号配置可参照 https://blog.csdn.net/u010963948/article/details/100146656
*
* @return
*/
@Override
public int filterOrder() {
return 2;
} /**
* shouldFilter:逻辑是否要过滤
*
* @return
*/
@Override
public boolean shouldFilter() {
//路径与配置的相匹配,则执行过滤
RequestContext ctx = RequestContext.getCurrentContext();
for (String pathPattern : dataFilterConfig.getAuthPath()) {
if (PathUtil.isPathMatch(pathPattern, ctx.getRequest().getRequestURI())) {
return true;
}
}
return false;
} /**
* 执行过滤器逻辑,验证token
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("token");
Claims claims;
try {
//解析没有异常则表示token验证通过,如有必要可根据自身需求增加验证逻辑
claims = jwtUtil.parseJWT(token);
log.info("token : {} 验证通过", token);
//对请求进行路由
ctx.setSendZuulResponse(true);
//请求头加入userId,传给业务服务
ctx.addZuulRequestHeader("userId", claims.get("userId").toString());
} catch (ExpiredJwtException expiredJwtEx) {
log.error("token : {} 过期", token );
//不对请求进行路由
ctx.setSendZuulResponse(false);
responseError(ctx, -402, "token expired");
} catch (Exception ex) {
log.error("token : {} 验证失败" , token );
//不对请求进行路由
ctx.setSendZuulResponse(false);
responseError(ctx, -401, "invalid token");
}
return null;
} /**
* 将异常信息响应给前端
*/
private void responseError(RequestContext ctx, int code, String message) {
HttpServletResponse response = ctx.getResponse();
Result errResult = new Result();
errResult.setCode(code);
errResult.setMessage(message);
ctx.setResponseBody(toJsonString(errResult));
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=utf-8");
} private String toJsonString(Object o) {
try {
return objectMapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
log.error("json序列化失败", e);
return null;
}
}
}

五、配置文件和路径匹配

在配置文件application.yml中配置登录接口路径 和 业务接口(需要身份验证的接口)路径,可配置多个,可使用通配符(基于Ant path匹配)

data-filter:
auth-path: #需要验证token的请求地址,可设置多个,会触发JwtAuthPreFilter
- /business/data/**
- /business/report/**
user-login-path: #登录请求地址,可设置多个,会触发LoginAddJwtPostFilter
- /business/login/**

PathUtil,封装路径匹配方法,用于判断请求的接口是否是需要拦截的接口

public class PathUtil {

    private static AntPathMatcher matcher = new AntPathMatcher();

    public static boolean isPathMatch(String pattern, String path) {
return matcher.match(pattern, path);
}
}

请求测试

一、测试登录接口

请求登录接口 http://localhost:8040/business/login/loginByPassword

看到响应body和header里都有了token:eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NzYzMTA3MDgsInVzZXJJZCI6IjEwMDEiLCJpYXQiOjE1NzU3MDU5MDl9.06MmrKGs5MK3nW5m6EaQTkkBviXQccPG33Nx1aF5zFw

把token的第二段 eyJleHAiOjE1NzYzMTA3MDgsInVzZXJJZCI6IjEwMDEiLCJpYXQiOjE1NzU3MDU5MDl9 使用base64解码

可以看到明文{"exp":1576310708,"userId":"1001","iat":1575705909}

包含了过期时间、用户id、签发时间

二、测试业务接口

请求业务接口 http://localhost:8040/business/data/getData 请求头不传token或传错误的token

可以看到返回了错误信息

{

"code": -401,

"message": "invalid token",

"data": null

}

请求业务接口 http://localhost:8040/business/data/getData 传入正确的token

可以看到返回了业务数据,说明已经请求到了业务接口,验证成功

源代码

最后,分享下源代码 https://gitee.com/tzjzcy/carson-cloud 有帮助的话记得给个star哦

Spring Cloud系列-Zuul网关集成JWT身份验证的更多相关文章

  1. spring cloud 通过zuul网关去请求的时候报404的几个原因。

    spring cloud 中 zuul 网关的那些坑: 1.检查你的服务是否正常启动. 2.检查你的服务是否正常注册到注册中心. 3.zuul网关的路由规则是会把你注册在注册中心的serviceId ...

  2. Spring Cloud之Zuul网关集群

    Nginx+Zuul 一主一备 或者 轮训多个 在微服务中,所有服务请求都会统一到Zuul网关上. Nginx 配置: #user nobody; worker_processes 1; #error ...

  3. Spring Cloud之Zuul网关路由

    前端请求先通过nginx走到zuul网关服务,zuul负责路由转发.请求过滤等网关接入层的功能,默认和ribbon整合实现了负载均衡 比如说你有20个服务,暴露出去,你的调用方,如果要跟20个服务打交 ...

  4. Spring Cloud使用Zuul网关时报错

    当开启了Eureka集群后,每创建一个服务都要往这两个集群中进行注册否则访问时会产生500

  5. Spring Cloud 系列之 Netflix Zuul 服务网关

    什么是 Zuul Zuul 是从设备和网站到应用程序后端的所有请求的前门.作为边缘服务应用程序,Zuul 旨在实现动态路由,监视,弹性和安全性.Zuul 包含了对请求的路由和过滤两个最主要的功能. Z ...

  6. Spring Cloud 系列之 Gateway 服务网关(一)

    什么是 Spring Cloud Gateway Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由 ...

  7. Spring Cloud 系列之 Gateway 服务网关(三)

    本篇文章为系列文章,未读第一集的同学请猛戳这里: Spring Cloud 系列之 Gateway 服务网关(一) Spring Cloud 系列之 Gateway 服务网关(二) 本篇文章讲解 Ga ...

  8. Spring Cloud 系列之 Gateway 服务网关(四)

    本篇文章为系列文章,未读第一集的同学请猛戳这里: Spring Cloud 系列之 Gateway 服务网关(一) Spring Cloud 系列之 Gateway 服务网关(二) Spring Cl ...

  9. Spring Cloud 系列之 Gateway 服务网关(二)

    本篇文章为系列文章,未读第一集的同学请猛戳这里:Spring Cloud 系列之 Gateway 服务网关(一) 本篇文章讲解 Gateway 网关的多种路由规则.动态路由规则(配合服务发现的路由规则 ...

随机推荐

  1. [windows篇] 使用Hexo建立个人博客,自定义域名https加密,搜索引擎google,baidu,360收录

    为了更好的阅读体验,欢迎阅读原文.原文链接在此. [windows篇] 使用Hexo建立个人博客,自定义域名https加密,搜索引擎google,baidu,360收录 Part 2: Using G ...

  2. 智学网电脑端查分小工具 已更新V2.2

    特别鸣谢这段代码的源作者,我的大佬同学\(MetalkgLZH\).由于我没有做什么工作,这篇随笔基本不含相关技术细节. 再次强调,这个程序的主要部分由\(MetalkgLZH\)完成.技术细节与源码 ...

  3. 暑期集训20190807 游戏(game)

    [问题描述] 小A 拿到了 n 个数,

  4. 分布式id生成方案总结

    本文已经收录自 JavaGuide (60k+ Star[Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.) 本文授权转载自:https://juejin.im/post/ ...

  5. 【Spdy协议简介】

    一.SPDY协议诞生记 SPDY (SPDY 是 Speedy 的昵音,意思是更快)是 Google 开发的基于传输控制协议 (TCP) 的应用层协议 ,那么为什么要搞一个SPDY出来呢?距离万维网之 ...

  6. 使用Typescript重构axios(十三)——让响应数据支持泛型

    0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...

  7. P3521 [POI2011]ROT-Tree Rotations(线段树合并)

    一句话题意(不用我改了.....):给一棵n(1≤n≤200000个叶子的二叉树,可以交换每个点的左右子树,要求前序遍历叶子的逆序对最少. ......这题输入很神烦呐... 给你一棵二叉树的dfs序 ...

  8. DAY 3 数论专场

    2019-07-23 今天的题目一个比一个神仙,很早之前就在讨论今天是不是晚上回宾馆就没脑子了,后来发现,是中午.... 一上午就讲了一个PPT,然而标题就两个子---数论... 这谁顶的住....整 ...

  9. sshd服务及基于密钥远程登陆(无需密码)

    上一条博客说明了用sshd服务远程登陆另一个系统,但是需要密码,如果不用密码呢?有没有简便的方法呢?下面为大家介绍一下,也就是基于密钥的安全验证:需要在本地生成”密钥对“后将公钥传送至服务端,进行公共 ...

  10. 通俗地说逻辑回归【Logistic regression】算法(二)sklearn逻辑回归实战

    前情提要: 通俗地说逻辑回归[Logistic regression]算法(一) 逻辑回归模型原理介绍 上一篇主要介绍了逻辑回归中,相对理论化的知识,这次主要是对上篇做一点点补充,以及介绍sklear ...