分布式系统--感性认识JWT

好久没写博客了,因为最近公司要求我学spring cloud ,早点将以前软件迁移到新的架构上。所以我那个拼命的学呐,总是图快,很多关键的笔记没有做好记录,现在又遗忘了很多关键的技术点,极其罪恶!

现在想一想,还是踏踏实实的走比较好。这不,今天我冒了个泡,来补一补前面我所学所忘的知识点。

想要解锁更多新姿势?请访问我的博客

常见的认证机制

今天我么聊一聊JWT。

关于JWT,相信很多人都已经看过用过,他是基于json数据结构的认证规范,简单的说就是验证用户登没登陆的玩意。这时候你可能回想,哎哟,不是又那个session么,分布式系统用redis做分布式session,那这个jwt有什么好处呢?

请听我慢慢诉说这历史!

最原始的办法--HTTP BASIC AUTH

HTTP BASIC auth,别看它名字那么长那么生,你就认为这个玩意很高大上。其实原理很简单,简单的说就是每次请求API的时候,都会把用户名和密码通过restful API传给服务端。这样就可以实现一个无状态思想,即每次HTTP请求和以前都没有啥关系,只是获取目标URI,得到目标内容之后,这次连接就被杀死,没有任何痕迹。你可别一听无状态,正是现在的热门思想,就觉得很厉害。其实他的缺点还是又的,我们通过http请求发送给服务端的时候,很有可能将我们的用户名密码直接暴漏给第三方客户端,风险特别大,因此生产环境下用这个方法很少。

Session和cookie

session和cookie老生常谈了。开始时,都会在服务端全局创建session对象,session对象保存着各种关键信息,同时向客户端发送一组sessionId,成为一个cookie对象保存在浏览器中。

当认证时,cookie的数据会传入服务端与session进行匹配,进而进行数据认证。

分布式系统--感性认识JWT

此时,实现的是一个有状态的思想,即该服务的实例可以将一部分数据随时进行备份,并且在创建一个新的有状态服务时,可以通过备份恢复这些数据,以达到数据持久化的目的。

缺点

这种认证方法基本是现在软件最常用的方法了,它有一些自己的缺点:

  • 安全性。cookies的安全性不好,攻击者可以通过获取本地cookies进行欺骗或者利用cookies进行CSRF攻击。
  • 跨域问题。使用cookies时,在多个域名下,会存在跨域问题。
  • 有状态。session在一定的时间里,需要存放在服务端,因此当拥有大量用户时,也会大幅度降低服务端的性能。
  • 状态问题。当有多台机器时,如何共享session也会是一个问题,也就是说,用户第一个访问的时候是服务器A,而第二个请求被转发给了服务器B,那服务器B如何得知其状态。
  • 移动手机问题。现在的智能手机,包括安卓,原生不支持cookie,要使用cookie挺麻烦。

Token认证(使用jwt规范)

token 即使是在计算机领域中也有不同的定义,这里我们说的token,是指 访问资源的凭据 。使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是 这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

Token机制,我认为其本质思想就是将session中的信息简化很多,当作cookie用,也就是客户端的“session”。

好处

那Token机制相对于Cookie机制又有什么好处呢?

  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提 是传输的用户认证信息通过HTTP头传输.
  • 无状态:Token机制本质是校验, 他得到的会话状态完全来自于客户端, Token机制在服务端不需要存储session信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript, HTML,图片等),而你的服务端只要提供API即可.
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等) 时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认 证机制就会简单得多。 CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防 范。
  • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256 计算 的Token验证和解析要费时得多. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要 为登录页面做特殊处理.
  • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)

缺陷在哪?

说了那么多token认证的好处,但他其实并没有想象的那么神,token 也并不是没有问题。

  1. 占带宽

    正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。听起来并不多,但日积月累也是不小一笔开销。实际上,许多人会在 JWT 中存储的信息会更多。

  2. 无论如何你需要操作数据库

    在网站上使用 JWT,对于用户加载的几乎所有页面,都需要从缓存/数据库中加载用户信息,如果对于高流量的服务,你确定这个操作合适么?如果使用redis进行缓存,那么效率上也并不能比 session 更高效

  3. 无法在服务端注销,那么久很难解决劫持问题
  4. 性能问题

    JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。听着似乎很牛逼,但是没有任何优势,为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。

JWT

现在我们来说说今天的主角,JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用 户和服务器之间传递安全可靠的信息

分布式系统--感性认识JWT

组成

一个JWT实际上就是一个字符串,它由三部分组成,头部载荷签名

头部(header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以 被表示成一个JSON对象。

{
    "typ":"JWT",
    "alg":"HS256"
}

这就是头部的明文内容,第一部分说明他是一个jwt,第二部分则指出签名算法用的是HS256算法

然后将这个头部进行BASE64编码,编码后形成头部:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(payload)

载荷就是存放有效信息的地方,有效信息包含三个部分:

(1)标准中注册的声明(建议但不强制使用)

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

(2)公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息. 但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64 是对称解密的,意味着该部分信息可以归类为明文信息。

{
    "sub":"1234567890",
    "name":"tengshe789",
    "admin": true
}

上面就是一个简单的载荷的明文,接下来使用base64加密:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  1. header (base64后的)
  2. payload (base64后的)
  3. secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第 三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

合成

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I kpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7Hg Q

实现JWT

现在一般实现jwt,都使用Apache 的开源项目JJWT(一个提供端到端的JWT创建和验证的Java库)。

依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

创建token的demo

public class CreateJWT {
    public static void main(String[] args) throws Exception{
        JwtBuilder builder = Jwts.builder().setId("123")
                .setSubject("jwt所面向的用户")
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,"tengshe789");
        String s = builder.compact();
        System.out.println(s);
        //eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA
    }
}

结果如图:

分布式系统--感性认识JWT

(注意,jjwt不支持jdk11,0.9.1以后的jjwt必须实现signWith()方法才能实现)

解析Token的demo

public class ParseJWT {
    public static void main(String[] args) {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJqd3TmiYDpnaLlkJHnmoTnlKjmiLciLCJpYXQiOjE1NDM3NTk0MjJ9.1sIlEynqqZmA4PbKI6GgiP3ljk_aiypcsUxSN6-ATIA";

        Claims claims =
                Jwts.parser().setSigningKey("tengshe789").parseClaimsJws(token).getBody();
        
        System.out.println("id"+claims.getId());
        System.out.println("Subject"+claims.getSubject());
        System.out.println("IssuedAt"+claims.getIssuedAt());
    }
}

结果如图:

分布式系统--感性认识JWT

生产中的JWT

在企业级系统中,通常内部会有非常多的工具平台供大家使用,比如人力资源,代码管理,日志监控,预算申请等等。如果每一个平台都实现自己的用户体系的话无疑是巨大的浪费,所以公司内部会有一套公用的用户体系,用户只要登陆之后,就能够访问所有的系统。

这就是 单点登录(SSO: Single Sign-On)

SSO 是一类解决方案的统称,而在具体的实施方面,一般有两种策略可供选择:

  1. SAML 2.0
  2. OAuth 2.0

欲扬先抑,先说说几个重要的知识点。

Authentication VS Authorisation

  • Authentication: 身份鉴别,鉴权,以下简称认证

    认证 的作用在于认可你有权限访问系统,用于鉴别访问者是否是合法用户。负责认证的服务通常称为 Authorization Server 或者 Identity Provider,以下简称 IdP

  • Authorisation: 授权

    授权 用于决定你有访问哪些资源的权限。大多数人不会区分这两者的区别,因为站在用户的立场上。而作为系统的设计者来说,这两者是有差别的,这是不同的两个工作职责,我们可以只需要认证功能,而不需要授权功能,甚至不需要自己实现认证功能,而借助 Google 的认证系统,即用户可以用 Google 的账号进行登陆。负责提供资源(API调用)的服务称为 Resource Server 或者 Service Provider,以下简称 SP

SMAL 2.0

分布式系统--感性认识JWT

OAuth(JWT)

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在 某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

流程可以参考如下:

分布式系统--感性认识JWT

简单的来说,就是你要访问一个应用服务,先找它要一个request token(请求令牌),再把这个request token发到第三方认证服务器,此时第三方认证服务器会给你一个aceess token(通行令牌), 有了aceess token你就可以使用你的应用服务了。

注意图中第4步兑换 access token 的过程中,很多第三方系统,如Google ,并不会仅仅返回 access token,还会返回额外的信息,这其中和之后更新相关的就是 refresh token。一旦 access token 过期,你就可以通过 refresh token 再次请求 access token

分布式系统--感性认识JWT

当然了,流程是根据你的请求方式和访问的资源类型而定的,业务很多也是不一样的,我这是简单的聊聊。

现在这种方法比较常见,常见的譬如使用QQ快速登陆,用的基本的都是这种方法。

开源项目

我们用一个很火的开源项目Cloud-Admin为栗子,来分析一下jwt的应用。

Cloud-Admin是基于Spring Cloud微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关API管理等多个模块,支持多业务系统并行开发。

目录结构

分布式系统--感性认识JWT

鉴权中心功能在ace-authace-gate下。

模型

下面是官方提供的架构模型。

分布式系统--感性认识JWT

可以看到,AuthServer在架构的中心环节,要访问服务,必须需要鉴权中心的JWT鉴权。

鉴权中心服务端代码解读

实体类

先看实体类,这里鉴权中心定义了一组客户端实体,如下:

@Table(name = "auth_client")
@Getter
@Setter
public class Client {
    @Id
    private Integer id;

    private String code;

    private String secret;

    private String name;

    private String locked = "0";

    private String description;

    @Column(name = "crt_time")
    private Date crtTime;

    @Column(name = "crt_user")
    private String crtUser;

    @Column(name = "crt_name")
    private String crtName;

    @Column(name = "crt_host")
    private String crtHost;

    @Column(name = "upd_time")
    private Date updTime;

    @Column(name = "upd_user")
    private String updUser;

    @Column(name = "upd_name")
    private String updName;

    @Column(name = "upd_host")
    private String updHost;
    
    private String attr1;
    private String attr2;
    private String attr3;
    private String attr4;
    private String attr5;
    private String attr6;
    private String attr7;
    private String attr8;

对应数据库:

CREATE TABLE `auth_client` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL COMMENT '服务编码',
  `secret` varchar(255) DEFAULT NULL COMMENT '服务密钥',
  `name` varchar(255) DEFAULT NULL COMMENT '服务名',
  `locked` char(1) DEFAULT NULL COMMENT '是否锁定',
  `description` varchar(255) DEFAULT NULL COMMENT '描述',
  `crt_time` datetime DEFAULT NULL COMMENT '创建时间',
  `crt_user` varchar(255) DEFAULT NULL COMMENT '创建人',
  `crt_name` varchar(255) DEFAULT NULL COMMENT '创建人姓名',
  `crt_host` varchar(255) DEFAULT NULL COMMENT '创建主机',
  `upd_time` datetime DEFAULT NULL COMMENT '更新时间',
  `upd_user` varchar(255) DEFAULT NULL COMMENT '更新人',
  `upd_name` varchar(255) DEFAULT NULL COMMENT '更新姓名',
  `upd_host` varchar(255) DEFAULT NULL COMMENT '更新主机',
  `attr1` varchar(255) DEFAULT NULL,
  `attr2` varchar(255) DEFAULT NULL,
  `attr3` varchar(255) DEFAULT NULL,
  `attr4` varchar(255) DEFAULT NULL,
  `attr5` varchar(255) DEFAULT NULL,
  `attr6` varchar(255) DEFAULT NULL,
  `attr7` varchar(255) DEFAULT NULL,
  `attr8` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;

这些是每组微服务客户端的信息

第二个实体类,就是客户端_服务的实体,也就是对应着那些微服务客户端能调用哪些微服务客户端:

大概对应的就是微服务间调用权限关系。

@Table(name = "auth_client_service")
public class ClientService {
    @Id
    private Integer id;

    @Column(name = "service_id")
    private String serviceId;

    @Column(name = "client_id")
    private String clientId;

    private String description;

    @Column(name = "crt_time")
    private Date crtTime;

    @Column(name = "crt_user")
    private String crtUser;

    @Column(name = "crt_name")
    private String crtName;

    @Column(name = "crt_host")
    private String crtHost;}

接口层

我们跳着看,先看接口层

@RestController
@RequestMapping("jwt")
@Slf4j
public class AuthController {
    @Value("${jwt.token-header}")
    private String tokenHeader;

    @Autowired
    private AuthService authService;

    @RequestMapping(value = "token", method = RequestMethod.POST)
    public ObjectRestResponse<String> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception {
        log.info(authenticationRequest.getUsername()+" require logging...");
        final String token = authService.login(authenticationRequest);
        return new ObjectRestResponse<>().data(token);
    }

    @RequestMapping(value = "refresh", method = RequestMethod.GET)
    public ObjectRestResponse<String> refreshAndGetAuthenticationToken(
            HttpServletRequest request) throws Exception {
        String token = request.getHeader(tokenHeader);
        String refreshedToken = authService.refresh(token);
        return new ObjectRestResponse<>().data(refreshedToken);
    }

    @RequestMapping(value = "verify", method = RequestMethod.GET)
    public ObjectRestResponse<?> verify(String token) throws Exception {
        authService.validate(token);
        return new ObjectRestResponse<>();
    }
}

这里放出了三个接口

先说第一个接口,创建token

具体逻辑如下:
每一个用户登陆进来时,都会进入这个环节。根据request中用户的用户名和密码,利用feign客户端的拦截器拦截request,然后使用作者写的JwtTokenUtil里面的各种方法取出token中的key和密钥,验证token是否正确,正确则用authService.login(authenticationRequest);的方法返回出去一个新的token。

public String login(JwtAuthenticationRequest authenticationRequest) throws Exception {
        UserInfo info = userService.validate(authenticationRequest);
        if (!StringUtils.isEmpty(info.getId())) {
            return jwtTokenUtil.generateToken(new JWTInfo(info.getUsername(), info.getId() + "", info.getName()));
        }
        throw new UserInvalidException("用户不存在或账户密码错误!");
    }

下图是详细逻辑图:

分布式系统--感性认识JWT

鉴权中心客户端代码

入口

作者写了个注解的入口,使用@EnableAceAuthClient即自动开启微服务(客户端)的鉴权管理

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AutoConfiguration.class)
@Documented
@Inherited
public @interface EnableAceAuthClient {
}

配置

接着沿着注解的入口看

@Configuration
@ComponentScan({"com.github.wxiaoqi.security.auth.client","com.github.wxiaoqi.security.auth.common.event"})
public class AutoConfiguration {
    @Bean
    ServiceAuthConfig getServiceAuthConfig(){
        return new ServiceAuthConfig();
    }
    @Bean
    UserAuthConfig getUserAuthConfig(){
        return new UserAuthConfig();
    }
}

注解会自动的将客户端的用户token和服务token的关键信息加载到bean中

feigin拦截器

作者重写了okhttp3拦截器的方法,每一次微服务客户端请求的token都会被拦截下来,验证服务调用服务的token和用户调用服务的token是否过期,过期则返回新的token

@Override
    public Response intercept(Chain chain) throws IOException {
        Request newRequest = null;
        if (chain.request().url().toString().contains("client/token")) {
            newRequest = chain.request()
                    .newBuilder()
                    .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                    .build();
        } else {
            newRequest = chain.request()
                    .newBuilder()
                    .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                    .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken())
                    .build();
        }
        Response response = chain.proceed(newRequest);
        if (HttpStatus.FORBIDDEN.value() == response.code()) {
            if (response.body().string().contains(String.valueOf(CommonConstants.EX_CLIENT_INVALID_CODE))) {
                log.info("Client Token Expire,Retry to request...");
                serviceAuthUtil.refreshClientToken();
                newRequest = chain.request()
                        .newBuilder()
                        .header(userAuthConfig.getTokenHeader(), BaseContextHandler.getToken())
                        .header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken())
                        .build();
                response = chain.proceed(newRequest);
            }
        }
        return response;
    }

spring容器的拦截器

第二道拦截器是来自spring容器的,第一道feign拦截器只是验证了两个token是否过期,但token真实的权限却没验证。接下来就要验证两个token的权限问题了。

服务调用权限代码如下:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置该注解,说明不进行服务拦截
        IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class);
        if (annotation == null) {
            annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class);
        }
        if(annotation!=null) {
            return super.preHandle(request, response, handler);
        }

        String token = request.getHeader(serviceAuthConfig.getTokenHeader());
        IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token);
        String uniqueName = infoFromToken.getUniqueName();
        for(String client:serviceAuthUtil.getAllowedClient()){
            if(client.equals(uniqueName)){
                return super.preHandle(request, response, handler);
            }
        }
        throw new ClientForbiddenException("Client is Forbidden!");
    }

用户权限:

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置该注解,说明不进行用户拦截
        IgnoreUserToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreUserToken.class);
        if (annotation == null) {
            annotation = handlerMethod.getMethodAnnotation(IgnoreUserToken.class);
        }
        if (annotation != null) {
            return super.preHandle(request, response, handler);
        }
        String token = request.getHeader(userAuthConfig.getTokenHeader());
        if (StringUtils.isEmpty(token)) {
            if (request.getCookies() != null) {
                for (Cookie cookie : request.getCookies()) {
                    if (cookie.getName().equals(userAuthConfig.getTokenHeader())) {
                        token = cookie.getValue();
                    }
                }
            }
        }
        IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token);
        BaseContextHandler.setUsername(infoFromToken.getUniqueName());
        BaseContextHandler.setName(infoFromToken.getName());
        BaseContextHandler.setUserID(infoFromToken.getId());
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        BaseContextHandler.remove();
        super.afterCompletion(request, response, handler, ex);
    }

spring cloud gateway网关代码

该框架中所有的请求都会走网关服务(ace-gatev2),通过网关,来验证token是否过期异常,验证token是否不存在,验证token是否有权限进行服务。

下面是核心代码:

@Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, GatewayFilterChain gatewayFilterChain) {
        log.info("check token and user permission....");
        LinkedHashSet requiredAttribute = serverWebExchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        ServerHttpRequest request = serverWebExchange.getRequest();
        String requestUri = request.getPath().pathWithinApplication().value();
        if (requiredAttribute != null) {
            Iterator<URI> iterator = requiredAttribute.iterator();
            while (iterator.hasNext()){
                URI next = iterator.next();
                if(next.getPath().startsWith(GATE_WAY_PREFIX)){
                    requestUri = next.getPath().substring(GATE_WAY_PREFIX.length());
                }
            }
        }
        final String method = request.getMethod().toString();
        BaseContextHandler.setToken(null);
        ServerHttpRequest.Builder mutate = request.mutate();
        // 不进行拦截的地址
        if (isStartWith(requestUri)) {
            ServerHttpRequest build = mutate.build();
            return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());
        }
        IJWTInfo user = null;
        try {
            user = getJWTUser(request, mutate);
        } catch (Exception e) {
            log.error("用户Token过期异常", e);
            return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Token Forbidden or Expired!"));
        }
        List<PermissionInfo> permissionIfs = userService.getAllPermissionInfo();
        // 判断资源是否启用权限约束
        Stream<PermissionInfo> stream = getPermissionIfs(requestUri, method, permissionIfs);
        List<PermissionInfo> result = stream.collect(Collectors.toList());
        PermissionInfo[] permissions = result.toArray(new PermissionInfo[]{});
        if (permissions.length > 0) {
            if (checkUserPermission(permissions, serverWebExchange, user)) {
                return getVoidMono(serverWebExchange, new TokenForbiddenResponse("User Forbidden!Does not has Permission!"));
            }
        }
        // 申请客户端密钥头
        mutate.header(serviceAuthConfig.getTokenHeader(), serviceAuthUtil.getClientToken());
        ServerHttpRequest build = mutate.build();
        return gatewayFilterChain.filter(serverWebExchange.mutate().request(build).build());

    }

分布式系统--感性认识JWT

cloud admin总结

总的来说,鉴权和网关模块就说完了。作者代码构思极其精妙,使用在大型的权限系统中,可以巧妙的减少耦合性,让服务鉴权粒度细化,方便管理。

结束

此片完了~ 想要了解更多精彩新姿势?
请访问我的个人博客

本篇为原创内容,已在个人博客率先发表,随后看心情可能会在CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友,咱们两个想个号码, 买个彩票,先挣他个几百万

相关推荐