目录

一、简介

二、JWT简介

三、Zuul集成JWT过程

四、测试

五、总结


一、简介

使用微服务开发项目,肯定少不了身份认证,一般我们都会将身份认证放在网关中做,实现统一的身份认证。这里我们选用了JWT(Json Web Token)作为身份认证, jwt作为当下比较流行的身份认证方式之一主要的特点是无状态,把信息放在客户端,服务器端不需要保存session,适合在分布式环境下使用。微服务网关(如Zuul)验证token后,把解析出来的身份信息放在request header请求头或者请求体中返回给具体的业务微服务。

二、JWT简介

这里对JWT做一个简单的介绍。jwt,全称JSON Web Token,一般用于用户认证(前后端分离项目、微信小程序、APP端)。

  • 传统的token认证方式:用户登录,服务器端返回token,并将token保存在服务器端,以后用户再次访问时,需携带token,服务器端获取token后,再去数据库中获取token进行校验。
  • 基于jwt token认证方式:用户登录,服务器端给用户返回一个token,但是服务器端不保存,以后用户再次访问时,需要携带token,服务器端获取token后,再做token校验。

通过上面的对比,jwt跟传统方式最明显的区别就是服务端不保存token,这样服务端校验token时就少了很多数据库操作。

jwt token结构:jwt token一般分为三个部分:Header、Payload、Signature。下图是一个jwt token,每一种颜色对应一个部分。 

  • Header:内部包含算法和类型
{"alg": "HS256","typ": "JWT"
}

生成的逻辑:将json转换成字符串,然后使用Base64Url进行编码加密 。

  • Payload:主要存放自定义的信息,如userId、用户名等,但是不要把敏感信息放在这里。
{"userId": "1234567890","username": "John Doe","exp": 12345678950   #超时时间
}

生成的逻辑:将json转换成字符串,然后使用Base64Url进行编码加密

  • Signature:将第一部分、第二部分的密文使用.拼接起来,然后使用HS256算法进行加密 + 加盐。HS256算法进行加密后的密文再进行Base64Url进行编码加密。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

下面介绍一下jwt实现过程:

  1. 第一步:用户提交用户名和密码给服务器端,如果登录成功,使用jwt创建一个token,并给用户返回;
  2. 第二步:以后用户再来访问时,需携带token,后端需要对token进行校验;
  3. 第三步:获取token,对token进行切割,对第二段进行Base64Url解密,获取Payload消息,检测token是否超时;
  4. 第四步:将第一段和第二段进行拼接,然后使用HS256加密 + 加盐生成密文1,将第三段密文进行Base64Url解密生成密文2,最后比较两个密文1,2是否相等,如果相等表示token有效;

通过上面的介绍,相信大家已经对JWT有些初步的认识,那么下面就该动动手实现一下网关Zuul集成JWT认证。

三、Zuul集成JWT过程

下面会通过一个示例详细介绍如何在Spring Cloud Zuul中集成JWT实现用户认证功能,主要涉及到三个项目:

  • zuul-eureka-server:端口1111.,服务注册中心。
  • goods-service:端口2222,服务提供者。
  • api-gateway:端口3333,网关服务,具体集成JWT主要是在网关中进行。

注意,笔者这里使用的还是比较旧的微服务版本:

  • SpringBoot:1.5.8.RELEASE
  • SpringCloud:Camden.SR6

(一)、搭建zuul-eureka-server

【a】pom.xml引入对应的依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-eureka-server</artifactId></dependency>

【b】启动类加上@EnableEurekaServer注解

@SpringBootApplication
//@EnableEurekaServer注解的作用: 开启Eureka服务发现的功能
@EnableEurekaServer
public class ZuulEurekaServerApplication {public static void main(String[] args) {SpringApplication.run(ZuulEurekaServerApplication.class, args);}}

【c】application.yml配置文件

server:port: 1111  #服务端口号
eureka:client:fetch-registry: false  #是否检索服务register-with-eureka: false  #表示不向Eureka注册自身服务service-url: #服务注册中心地址,其他服务可以通过指定eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/注册到Eureka上defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/instance:#主机名hostname: localhost
spring:application:#服务名称name: eureka-server

至此,服务注册中心Eureka就搭建成功了。

(二)、搭建goods-service

【a】pom.xml引入相关依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-eureka</artifactId></dependency>

【b】启动类加上@EnableDiscoveryClient注解将服务注册到Eureka中

@SpringBootApplication
@EnableDiscoveryClient //注册Eureka客户端
public class GoodsServiceApplication {public static void main(String[] args) {SpringApplication.run(GoodsServiceApplication.class, args);}}

【c】application.yml配置文件

server:port: 2222
spring:application:name:  goods-service
eureka:instance:hostname: localhostclient:serviceUrl:defaultZone: http://localhost:1111/eureka/

【d】定义一个测试Controller接口

@RestController
public class GoodsController {//模拟几个商品private static List<String> goodsList = new ArrayList<>();static {goodsList.add("图书");goodsList.add("相册");goodsList.add("风扇");goodsList.add("手机");goodsList.add("电脑");}@GetMapping("/getGoodsList")public List<String> getGoodsList() {return goodsList;}}

至此,goods-service服务提供者也搭建完成,下面搭建最重要的网关服务。

(三)、搭建api-gateway

【a】pom.xml引入相关依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-eureka</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zuul</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.4</version></dependency>

注意这里引入了jwt工具包io.jsonwebtoken.jjwt。

【b】启动类加上注解@EnableDiscoveryClient开启服务注册功能、@EnableZuulProxy开启服务路由功能

@SpringBootApplication
@EnableDiscoveryClient
//@EnableZuulProxy注解用于开启Zuul路由功能(反向代理)
@EnableZuulProxy
public class ApiGatewayApplication {public static void main(String[] args) {SpringApplication.run(ApiGatewayApplication.class, args);}}

【c】application.yml配置文件

server:port: 3333
spring:application:name: api-gateway
eureka:client:service-url:defaultZone: http://localhost:1111/eureka/
zuul:routes:goods-service:path: /goods/**serviceId:  goods-serviceapi-gateway:path: /api/login/**serviceId: api-gateway
common:login:url: /api/login/userLogin  #登录请求地址,可设置多个,使用逗号分隔开
exclude:auth:url: /api/login/userLogin #不需要授权验证的请求地址,可设置多个,使用逗号分隔开,会跳过AuthFilter授权验证

注意:这里定义了网关路由的一些规则以及登录请求的URL以及需要排除掉不需要进行认证的URL配置信息,在后面的过滤器中使用到。

【d】编写LoginController,模拟登录接口

package com.springcloud.zuul.apigateway.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class LoginController {@GetMapping(value = "/userLogin")public String loginByPassword() {//此处省略具体的登录逻辑return "登录成功!";}
}

【e】封装JwtUtil,主要包含两个方法

  • 创建token
  • 解析(并验证)token
package com.springcloud.zuul.apigateway.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.Map;@Component
public class JwtUtils {/*** 签名用的密钥*/private static final String SIGNING_KEY = "u3HYQHDdH8JBblN0Jyhu4Fy9IMXEiilM";/*** 用户登录成功后生成Jwt token* 使用Hs256算法** @param exp    jwt过期时间* @param claims 保存在Payload(有效载荷)中的内容* @return token字符串*/public String createJwtToken(Date exp, Map<String, Object> claims) {//token生成的时间,默认取当前时间Date now = new Date(System.currentTimeMillis());//创建一个JwtBuilder,设置jwt的bodyJwtBuilder builder = Jwts.builder()//保存在Payload(有效载荷)中的内容, 自定义一些数据保存在这里.setClaims(claims)//iat: jwt的签发时间.setIssuedAt(now)//设置过期时间.setExpiration(exp)//使用HS256算法和签名使用的秘钥生成密文.signWith(SignatureAlgorithm.HS256, SIGNING_KEY);return builder.compact();}/*** 解析token,获取到Payload(有效载荷)中的内容,包括验证签名,判断是否过期** @param token 令牌* @return*/public Claims parseJwtToken(String token) {//得到DefaultJwtParserreturn Jwts.parser()//设置签名的秘钥.setSigningKey(SIGNING_KEY)//设置需要解析的token.parseClaimsJws(token).getBody();}}

【f】封装路由匹配工具类

package com.springcloud.zuul.apigateway.utils;import org.springframework.util.AntPathMatcher;public class PathMatchUtil {private static AntPathMatcher matcher = new AntPathMatcher();public static boolean isPathMatch(String pattern, String path) {return matcher.match(pattern, path);}
}

【g】定义LoginFilter拦截登录方法,登录成功后创建token,返回给前端

大体流程:

  1. 拦截类型是“post”后置拦截器,在路由方法响应之后拦截;
  2. 判断请求的uri是否是登录接口(与配置文件中设置的登录uri是否匹配),需要在配置文件配置登录接口地址;
  3. 判断登录方法返回成功,创建token,并添加到 response body或response header,返回给前端;
package com.springcloud.zuul.apigateway.filter;import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.springcloud.zuul.apigateway.utils.JwtUtils;
import com.springcloud.zuul.apigateway.utils.PathMatchUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** @author weixiaohuai* @Date 2020-07-19 14:34* @description 拦截登录请求后的过滤器*/
@Component
public class LoginFilter extends ZuulFilter {@Value("${common.login.url}")private String loginUrl;@Autowiredprivate JwtUtils jwtUtils;@Overridepublic String filterType() {return "post";}@Overridepublic int filterOrder() {return 1;}@Overridepublic boolean shouldFilter() {//只有路径与配置文件中配置的登录路径相匹配,才会放行该过滤器,执行过滤操作RequestContext ctx = RequestContext.getCurrentContext();String requestURI = ctx.getRequest().getRequestURI();for (String url : loginUrl.split(",")) {if (PathMatchUtil.isPathMatch(url, requestURI)) {return true;}}return false;}@Overridepublic Object run() {RequestContext requestContext = RequestContext.getCurrentContext();try {HttpServletRequest httpServletRequest = requestContext.getRequest();//此处简单模拟登录,并非生产环境登录使用.String username = httpServletRequest.getParameter("username");String password = httpServletRequest.getParameter("password");if ("weishihuai".equals(username) && "password".equals(password)) {//表示登录成功,服务器端需要生成token返回给客户端//过期时间: 2分钟Date expDate = new Date(System.currentTimeMillis() + 2 * 60 * 1000);Map<String, Object> claimsMap = new HashMap<>();claimsMap.put("username", "weishihuai");claimsMap.put("userId", "201324131147");claimsMap.put("expDate", expDate);String jwtToken = jwtUtils.createJwtToken(expDate, claimsMap);//响应头设置tokenrequestContext.addZuulResponseHeader("token", jwtToken);}} catch (Exception e) {e.printStackTrace();}return null;}
}

【h】定义认证过滤器AuthFilter,拦截具体的业务接口,验证token

大体流程:

  1. 拦截类型是“pre”前置过滤器,在调用业务接口之前拦截;
  2. 判断请求的uri是否排除在外不需要认证的URL,需要在配置文件配置业务接口地址;
  3. 判断token验证是否通过,通过则路由,不通过返回错误提示;
package com.springcloud.zuul.apigateway.filter;import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.springcloud.zuul.apigateway.utils.JwtUtils;
import com.springcloud.zuul.apigateway.utils.PathMatchUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;/*** @author weixiaohuai* @Date 2020-07-19 14:40* @description 验证授权的过滤器*/
@Component
public class AuthFilter extends ZuulFilter {private static Logger logger = LoggerFactory.getLogger(AuthFilter.class);/*** 读取配置文件中排除不需要授权的URL*/@Value("${exclude.auth.url}")private String excludeAuthUrl;@Autowiredprivate JwtUtils jwtUtils;@Overridepublic String filterType() {//由于授权需要在请求之前调用,所以这里使用前置过滤器return "pre";}@Overridepublic int filterOrder() {return 2;}@Overridepublic boolean shouldFilter() {//路径与配置文件中的相匹配,则执行过滤RequestContext ctx = RequestContext.getCurrentContext();String requestURI = ctx.getRequest().getRequestURI();List<String> excludesUrlList = Arrays.asList(excludeAuthUrl.split(","));return !excludesUrlList.contains(requestURI);}@Overridepublic Object run() {RequestContext requestContext = RequestContext.getCurrentContext();HttpServletRequest httpServletRequest = requestContext.getRequest();String token = httpServletRequest.getHeader("token");Claims claims;try {//解析没有异常则表示token验证通过,如有必要可根据自身需求增加验证逻辑claims = jwtUtils.parseJwtToken(token);//对请求进行路由requestContext.setSendZuulResponse(true);//请求头加入userId,传给具体的微服务requestContext.addZuulRequestHeader("userId", claims.get("userId").toString());} catch (ExpiredJwtException expiredJwtEx) {logger.error("token : {} 已过期", token);//不对请求进行路由requestContext.setSendZuulResponse(false);JSONObject resultJSONObject = new JSONObject();resultJSONObject.put("code", "40002");resultJSONObject.put("msg", "token已过期");requestContext.setResponseBody(resultJSONObject.toJSONString());HttpServletResponse response = requestContext.getResponse();response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType("application/json;charset=utf-8");} catch (Exception ex) {logger.error("token : {} 验证失败", token);//不对请求进行路由requestContext.setSendZuulResponse(false);JSONObject resultJSONObject = new JSONObject();resultJSONObject.put("code", "40001");resultJSONObject.put("msg", "非法token");requestContext.setResponseBody(resultJSONObject.toJSONString());HttpServletResponse response = requestContext.getResponse();response.setCharacterEncoding(StandardCharsets.UTF_8.name());response.setContentType("application/json;charset=utf-8");}return null;}
}

至此,网关服务集成JWT就算是搭建成功了,下面我们进行测试一下。

四、测试

(一)、测试登录接口,查看是否被LoginFilter所拦截,这里使用postman模拟发请求。

请求登录接口:http://localhost:3333/api/login/userLogin?username=weishihuai&password=password

我们看到header里都有了token:

eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTUxNjQ5ODYsInVzZXJJZCI6IjIwMTMyNDEzMTE0NyIsImlhdCI6MTU5NTE2NDg2NiwiZXhwRGF0ZSI6MTU5NTE2NDk4NjAwMywidXNlcm5hbWUiOiJ3ZWlzaGlodWFpIn0.IL1jRpAmykS4QsMaztQCRHaCnie8fyULOVT6by_zBW4

前面我们已经介绍过token分为了三段结构,第二段payload部分使我们存放的自定义信息,是经过base64URL加密后的密文,下面我们将它进行解密:

eyJleHAiOjE1OTUxNjQ5ODYsInVzZXJJZCI6IjIwMTMyNDEzMTE0NyIsImlhdCI6MTU5NTE2NDg2NiwiZXhwRGF0ZSI6MTU5NTE2NDk4NjAwMywidXNlcm5hbWUiOiJ3ZWlzaGlodWFpIn0

解密后的明文为:

{"exp":1595164986,"userId":"201324131147","iat":1595164866,"expDate":1595164986003,"username":"weishihuai"}

可见,其中包含了过期时间、用户id、token签发时间等。

(二)、测试业务接口

请求业务接口 :http://localhost:3333/goods/getGoodsList, 请求头不传token或传错误的token:

可以看到返回了错误信息:

{"msg": "非法token","code": "40001"
}

因为token有效期为两分钟,所以这里需要等待两分钟后,此时token已经过期。我们再次请求业务接口 :http://localhost:3333/goods/getGoodsList, 传入刚刚那个token,看是否会提示token已过期。

 可以看到返回了错误信息 :

{"msg": "token已过期","code": "40002"
}

因为刚刚那个token已经失效了,所以这里我们重新获取一个token,然后再次请求http://localhost:3333/goods/getGoodsList,传入正确的token,看下数据是否能够正常返回。

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

五、总结

本文主要总结了如何在Spring Clous Zuul网关中集成JWT实现身份认证功能,主要利用了JWT Token无状态特性,不需要保存在服务器端,所以节省了很多数据库IO操作,至于集成过程,主要是通过继承ZuulFilter拦截所有发往网关的请求,类似于拦截功能,详细的实现过程已经在前面介绍了。限于笔者水平有限,如有不对之处,还望指出。

Spring Cloud Zuul网关集成JWT身份验证学习总结相关推荐

  1. Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式

    时间过的很快,写springcloud(十):服务网关zuul初级篇还在半年前,现在已经是2018年了,我们继续探讨Zuul更高级的使用方式. 上篇文章主要介绍了Zuul网关使用模式,以及自动转发机制 ...

  2. Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

    时间过的很快,写springcloud(十):服务网关zuul初级篇还在半年前,现在已经是2018年了,我们继续探讨Zuul更高级的使用方式. 上篇文章主要介绍了Zuul网关使用模式,以及自动转发机制 ...

  3. Spring Cloud Zuul网关(快速搭建)

    zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用. 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架.相当于是设备和 Netflix ...

  4. Spring Cloud Zuul网关之ZuulFilter过滤

    Zuul的大部分功能都是基于Filter过滤器的,这个Filter不是我们Web开发中Servlet的Filter,而是Zuul自己的Filter,但其功能类似于Servlet框架的Filter. 1 ...

  5. 基于eureka如何使用spring cloud zuul 网关

    目录 1.增加依赖 2.打开zuul开关 3.application.yml配置 4.测试 1.增加依赖 <!--zuul --><dependency><groupId ...

  6. 14 基于网关Spring Cloud Zuul的接口限流实现方案

    在Spring Cloud Zuul网关中,限流业务是放在前置过滤器实现的,也就是在请求被Zuul转发给微服务之前进行限流.另外,当前置过滤器中同时存在限流.鉴权.身份认证等业务时,应该将限流业务放在 ...

  7. Spring Cloud Zuul API 网关服务

    API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤.它除了要实 ...

  8. Spring Cloud Zuul之ZuulFilter详解

    简介 Spring Cloud Zuul网关在整个微服务体系中肩负对外开放接口.请求拦截.路由转发等作用,其核心处理则是ZuulFilter ZuulFilter部分源码 Zuul Filter全部继 ...

  9. 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul

    <深入理解 Spring Cloud 与微服务构建>第十章 路由网关 Spring Cloud Zuul 文章目录 <深入理解 Spring Cloud 与微服务构建>第十章 ...

  10. Spring Cloud——API网关服务:Spring Cloud Zuul

    API网关像是整个微服务框架系统的门面一样,所有的客户端访问都需要经过它来进行调度和过滤.它实现了请求路由.负载均衡.校验过滤等功能.zuul包含了hystrix.ribbon.acturator等重 ...

最新文章

  1. android 应用uid,android adb 获取所有app 的uid
  2. 求正多边形的面积JAVA_第六章第三十六题(几何:正多边形的面积)(Geometry: area of a regular polygon)...
  3. VTK:标记关键点用法实战
  4. 学术英语:关于such as, for example, etc., and so on, i.e., 和e.g.的使用
  5. LeetCode MySQL 570. 至少有5名直接下属的经理
  6. 用html5做一个介绍自己家乡的页面_厚溥资讯 | HTML5的小知识点小集合(上)
  7. 在ASP.NET Core 3.1 MVC中集成Vue.js V4和使用Dropzone文件上传
  8. 安装nginx之前的组件
  9. linux 查看nginx,php-fpm运行用户及用户组
  10. c语言程序设计答案 第五版 谭浩强
  11. 学习云计算就业方向有哪些 一般薪资能拿多少
  12. 举个栗子!Tableau技巧(7):如何做帕累托图
  13. QObject::moveToThread: Current thread(...) is not the object`s thread. Cannot move to target thread(
  14. 呆萌却实际可怕的动物:蛇鹫会踢腿 大熊猫攻击凶猛
  15. 5700:还钱问题(贪心+思维)
  16. 天池-金融风控训练营-task5-模型融合
  17. 51单片机用c语言倒计时程序,51单片机实现100以内倒计时,求大佬指点。
  18. 什么是动态代理?实际开发中如何使用?
  19. 摄影镜头调制传输函数MTF解读
  20. MySQL查询语句in子查询的优化

热门文章

  1. 实战NSURLProtocol 拦截 APP网络请求NSURLConnection, NSURLSession, Alamofire
  2. 算法:翻转图片Rotate Image
  3. APNs Push Notification教程一
  4. java面试常考_java面试常考题
  5. 尼奥智能陪伴机器人如何绑定设备_巴巴腾 智能陪护儿童机器人A3,为儿童专业定制的小伙伴...
  6. C++ string append方法的常用用法
  7. 罗马仕php30重量,认真测评 篇三:罗马仕sence8P+两年使用报告
  8. 怎么是phpadmin连接mysql_phpmyadmin连接远程mysql
  9. 【自然语言处理系列】预训练模型原理和实践综述 | 附汇报PPT原稿和18篇论文
  10. 降维系列之 SNE与t-SNE