Spring Cloud系列-Zuul网关集成JWT身份验证

前言

这两三年项目中一直在使用比较流行的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哦

原文地址:https://www.cnblogs.com/lookup/p/zuul-jwt.html

时间: 2024-11-07 23:13:26

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

spring cloud 学习笔记--网关zuul学习

微服务是多个服务共同完成一件事情,那么"一致对外"就很有必要,就像我们去买面包,不可能先去找农民买小麦,再.... 盗图 spring cloud 引入zuul方式来实现这一功能 添加依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </depend

上网行为管理AD域集成windos身份验证SSO单点登录

一.背景需求 在大型公司项目上会使用到AD域集成windos身份验证SS0单点登录,而不同用户有不同的环境,不同环境就会有不同的验证需求,使用微软AD域进行用户身份验证,会经常出现在客户的需求中. 二.网域上网行为管理集成windos身份验证功能. 什么是AD域集成windos身份验证了?就是基于windos系统的电脑端都需要加入到AD域或者这些没有加入到AD域的windos系统的电脑必须要通过AD域的用户验证信息才能上网.为什么大型企业需要windos身份验证?通过windos身份验证分配给每

Spring Cloud Gateway服务网关

原文:https://www.cnblogs.com/ityouknow/p/10141740.html Spring 官方最终还是按捺不住推出了自己的网关组件:Spring Cloud Gateway ,相比之前我们使用的 Zuul(1.x) 它有哪些优势呢?Zuul(1.x) 基于 Servlet,使用阻塞 API,它不支持任何长连接,如 WebSockets,Spring Cloud Gateway 使用非阻塞 API,支持 WebSockets,支持限流等新特性. Spring Clou

Spring Cloud 系列之 Eureka 实现服务注册与发现

如果你对 Spring Cloud 体系还不是很了解,可以先读一下 Spring Cloud 都有哪些模块 Eureka 是 Netflix 开源的服务注册发现组件,服务发现可以说是微服务架构的核心功能了,微服务部署之后,一定要有服务注册和发现的能力,Eureka 就是担任这个角色的.如果你用过 dubbo 的话,那一定知道 dubbo 中服务注册和发现的功能是用 zookeeper 来实现的. Eureka 目前是 2.x 版本,并且官方已经宣布不再维护更新.不过其实 Eureka 已经很稳定

Spring Cloud 之 Zuul.

一.概述 ?API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤.它除了要实现请求路由.负载均衡.校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合.请求转发时的熔断机制.服务的聚合等一系列高级功能. ?在 Spring Cloud 中了提供了基于 Net?ix Zuul 实现的 API 网关组件 Spring Cloud Zuul. 二.准备阶段 S

(四)Spring Cloud教程——Zuul(F版本)

参考:方志鹏的专栏 在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现.服务消费.负载均衡.断路器.智能路由.配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统.一个简单的微服务系统如下图: 注意:A服务和B服务是可以相互调用的,作图的时候忘记了.并且配置服务也是注册到服务注册中心的. 在Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(zuul.Ngnix),再到达服务网关(zuul集群),然后再到具体的服.,服务统一注

spring cloud 分布式链路跟踪(集成zipkin)

篇写了分布式链路追踪  spring cloud 分布式链路追踪 这样的链路追踪虽然可以解决问题 但日志太过于分散 如果微服务过多 就会变的相当复杂 zipkin就可以帮我们把链路调用的过程全部收集起来 它就像注册中心一样 分为客户端和服务端 想要使用 首先建一个模块 当作他的服务端 首先添加如下依赖 compile 'io.zipkin.java:zipkin-server' compile 'io.zipkin.java:zipkin-autoconfigure-ui' 在配置文件中配置它的

Spring Cloud之Zuul负载均衡

Zuul网关默认是实现负载均衡的,不需要任何配置.默认开启ribbon效果的 可以启启动两个服务端口,访问下. 原文地址:https://www.cnblogs.com/toov5/p/9972466.html

springmvc文件上传AND jwt身份验证

SpringMVC文件上传 思路:1.首先定义页面,定义多功能表单(enctype=“multipart/form-data”)2.在Controller里面定义一个方法,用参数(MultipartFile)来接收前台传递过来的文件对象3.然后文件上传就是把文件从一个地方(本地)复制到另外一个地方(服务器) 添加pom依赖 <dependency> <groupId>commons-fileupload</groupId> <artifactId>commo