转载

感性认识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总结

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

原文  https://juejin.im/post/5c069649e51d45748f39badf
正文到此结束
Loading...