JWT认证系统尝鲜

August 1, 2019

3451 15 minutes and 41 seconds
JWT认证系统尝鲜

简介一下技术栈

What is jwt token

其实什么是 jwt 认证系统,这个已经不是什么新的技术了,但是由于之前一直在 toB 的企业中天天浑浑噩噩的,所以没有地方用到过这类的技术(不得不感慨,前端技术的话,还是 toC 的企业用的比较新比较全,毕竟开发效率是第一生产力 >.0),现在我介绍这个东西也算是老调重弹,或者说赶个晚集。 jwt 全称(JSON WEB TOKEN)是一套开放的认证系统标准,它的定义宗旨是使用一套简洁的且安全的方案,在客户端和服务端认证之间安全的传输 JSON 格式的认证信息。 介绍就是这么的简单,完全不需要任何高深度的理解,就是服务端和客户端把认证信息变成了 JSON 在传输而已,掌握起来并没有任何难度,但是什么时候使用它以及为什么使用这个就是在集成过程中特别需要注意的地方了,不过这部分内容,先按下不表,后面段落中再详细说明。

ng-alain

ng-alain 这个框架或者说是前端构建脚手架,相信使用的人还不是很多,目前在网上似乎我也搜不到什么帮助问答,不过我个人还是很看好这个框架的(其实是我个人倾向前端的 angular 技术,别问为什么,问就是 react 和 vue 不如它,PS:后期还是会好好学习 react 和 vue 的,期待’真香’时刻)。 首先它是基于 angular 的一套完善的中后台解决方案了,也算是我在学习 angular 过程中发现的一套不过的框架结构,不过我才使用几天就发现了一些令我头疼的问题,不过还是很值得期待后续它是如何发展的,毕竟出自阿里的 ant-design 系统的一套独立系统,并且更新还是很频繁的~ 其实,在 angluar 还有使用 ng-alain 上面我都是一个刚入门的小菜鸡,所以总结不出什么高大上的介绍语,所以还是引用一句官方的话来简单介绍它吧:NG-ALAIN 是一个企业级中后台前端/设计解决方案脚手架,我们秉承 Ant Design 的设计价值观,目标也非常简单,希望在Angular上面开发企业后台更简单、更快速。随着『设计者』的不断反馈,将持续迭代,逐步沉淀和总结出更多设计模式和相应的代码实现,阐述中后台产品模板/组件/业务场景的最佳实践,也十分期待你的参与和共建。 ng-alin地址: https://ng-alain.com/docs/getting-started/zh

Why choose jwt & ng-alain

选择一个东西,肯定得有挑选它的理由(PS:我在我的项目使用只是想用,就是这么任性~ 此外也是因为正好 ng-alain 中提供了 JWTInterceptor )。

JWT 的优点

  • 它有严谨的结构化数据体征,它自身存在 payload 字段可以包含用户的自定义验证信息,这里,在服务端进行校验时,就不需要再去进行查询来判断当前用户的有效性了,当然你还可以往里面塞一些其他信息,不过不建议塞过多的东西,毕竟他是用heard 头部信息(PS:之前的文章我里面提到过)进行传输的,体积小是它的特征,如果不遵守使用的太过了,就会导致一些不必要的问题。
  • 它在前后端分离和跨平台项目中可是利器,无论是之前的 cookie 还是 session 都是有状态 restful api 验证,而 JWT 可以进行无状态的 rest api 验证,说白了就是服务端不进行状态的保留,而把登录状态的保留工作交给了客户端,需要客户端将每次 api 的请求头都带上所需要的信息,这样每个 api 之间都是独立的。
  • 无需使用特定的身份验证方式,只要拥有公司制定的标准格式的验证信息,生成的的 token 值可以在所有的产品接口中通用,无需繁琐的耦合验证操作,一次生成,永久使用,是不是很爽?这个难道就是传说中的单点登录?
// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  // reserved claims
  "iss": "a.com",
  "exp": "1d",
  // public claims
  "http://a.com": true,
  // private claims
  "company": "A",
  "awesome": true
}

// $Signature
HS256(Base64(Header) + "." + Base64(Payload), secretKey)

// JWT
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature

JWT 在验证过程中前后端的交互过程和工作原理

image

选择 ng-alain 的原因

首先 angluar 我也是入门者,但是我是标准的后端转前端,所以没什么好说的就是 angular 看着顺眼,这种写法深得我心,然后官网撸一撸文档,看看视频,基本差不多可以入手了,写着写着就不太对劲了。因为之前我写过一段时间的 react,我 T 喵的怎么发现 angular 比 react 还难写,没道理啊,我感觉从概念和代码上看,完全适合后端这种拆分模块的思虑啊,特别像 python 的 class 和 moduel 啊,怎么写起来这么难。没办法啊,再硬的骨头,怎么也得自己啃吧,不能因为难就放弃了吧,毕竟自己打算做一个独立开发人员,这点实力还是得有的,于是在网上搜集成熟的框架进行实打实的实战学习,于是就发现了神器->ng-alain。

其实这里我也是经历了很久的筛选过程的,不过我自己有几个标准:

  • 一定是遵循某一特定的设计理念,这点对于独立开发人员很重要,因为你不是一个设计师(PS:你要是真的艺术细胞爆棚也无敌),所以不能够拿出惨不忍睹的东西出来面人,所以一整套完善的开源设计体系是你需要的
  • 一定是有严格的开发标准的,即使是对于独立开发者来说,我还是觉得规则比开发本身更重要,因为我真的是受了太多的这种坑了,有许许多多问题都是因为你东抄一点西抄一点最后导致哪里出了问题都找不到
  • 一定是有发展潜力的开源框架,独立开发者可能有很多是大牛,直接自己手撸框架了,但是自己还诚然不是那种人,所以要选择一个对自己有利的开源框架,这个框架能够辅助自己的开发效率,此外,这个框架一定要频繁的更新,并且能够快速反馈,当然,最好的是起步阶段的项目,为什么不是稳定的?因为只有在一直处理问题的时候你才能,更深入的去学习框架代码,才能自己写好框架,为以后自己手撸框架做铺垫

如何集成

说了这么多,终于要开始把上述的功能点集成到一起了~ 不过这里我并不是从零开始一步一步手把手复述一遍集成过程,而是把一些主要的地方或者自己觉得需要注意的点备注一下:

后端集成 jwt

首先是服务端集成 jwt 这个功能,这里使用的是 django-rest-framework-jwt 这个插件,将这个插件安装之后,可以按照文档直接集成它自己的接口,不过我自己之前写过 restful api 的 login 接口了于是采用了手工集成的方式

@api_view(["POST"])
@authentication_classes(())
@permission_classes(())
@excepts
def login_view(request):
    """
    用于账户登录接口
    :param request:
    :return:
    """
    res = {
        "code": "00000000",
        "data": {}
    }

    username = request.POST.get('username')
    password = request.POST.get('password')
    remember = request.POST.get('remember')

    jwt_payload_handler = token_settings.JWT_PAYLOAD_HANDLER # 关键代码
    jwt_encode_handler = token_settings.JWT_ENCODE_HANDLER # 关键代码

    user = authenticate(request, username=username, password=password)
    if user is not None:
        login(request, user)
        if not remember:
            token_settings.JWT_EXPIRATION_DELTA = datetime.timedelta(hours=3) # 暂不生效后续需要改动

        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        res['data']['token'] = token
        return JsonResponse(res)
    else:
        raise AuthenticateError("Username or Password is incorrect!")

然后按照官方的提示,将 django-rest-framework-jwt 的认证方式添加到 settings.py 里面就行了,但是这里有个非常重要的问题,就是这个插件自己自带的 jwt token heard 的前缀是错误的,需要自己进行修改,这个地方真的是要感慨自己当初不好好看文档,这里我折腾了好久,还把 ng-alain 的认证方式和源码撸了一遍,其中还是用过 SimpleInterceptor 做过 ng-alain 的认证拦截器!!!

其实只需要把官方的配置文件中梳理仔细读一遍就可以知道了,官方的配置项目里面有能够修改的地方的。

JWT_AUTH = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
    'rest_framework_jwt.utils.jwt_decode_handler',

    'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',

    'JWT_PAYLOAD_GET_USER_ID_HANDLER':
    'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

    'JWT_RESPONSE_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_response_payload_handler',

    'JWT_SECRET_KEY': SECRET_KEY,
    'JWT_GET_USER_SECRET_KEY': None,
    'JWT_PUBLIC_KEY': None,
    'JWT_PRIVATE_KEY': None,
    'JWT_ALGORITHM': 'HS256',
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,

    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

    'JWT_AUTH_HEADER_PREFIX': 'Bearer',  # 就是这个地方!!!!!!
    'JWT_AUTH_COOKIE': None,
}

前端集成 jwt

其实 ng-alain 本身就已经自带了 JWTInterceptor,这里只需要开启就可以了 首先是配置请求拦截器,将 JWT 认证拦截器注册进去

// app.module.ts
// #region Http Interceptors
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { JWTInterceptor } from '@delon/auth';
import { DefaultInterceptor } from '@core/net/default.interceptor';
const INTERCEPTOR_PROVIDES = [
  { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true },
];
// #endregion

然后是在启动过程中,进行 token 的校验,判断当前用户是否已经登录了,是否从后端请求用户的详细信息

// startup.service.ts
...
// 启动的时候判断当前有没有存储 token 值,如果存储则直接跳转到首页,否则跳转到登录页面
  const tokenData = this.tokenService.get(JWTTokenModel);
  if (tokenData.token) {
    this.httpClient.get('accounts/current/').subscribe(userData => {
      // User information: including name, avatar, email address
      const {
        data: { profile, authority, permission },
      } = userData as any;

      // 设置用户的认证信息
      this.sessionStorageService.store('authority', authority);
      this.sessionStorageService.store('permission', permission);

      this.settingService.setUser(profile);
    });
  }
...

最后在登录的代码段里面加上保存用户 jwt token 信息的代码就搞定了,是不是很 easy~

  submitLogin() {
    this.username.markAsDirty();
    this.username.updateValueAndValidity();
    this.password.markAsDirty();
    this.password.updateValueAndValidity();
    if (this.username.invalid || this.password.invalid) {
      return;
    }

    const formData = new FormData();
    formData.append('username', this.username.value);
    formData.append('password', this.password.value);
    formData.append('remember', this.remember.value);

    // 默认配置中对所有HTTP请求都会强制 [校验](https://ng-alain.com/auth/getting-started) 用户 Token
    // 然一般来说登录请求不需要校验,因此可以在请求URL加上:`/login?_allow_anonymous=true` 表示不触发用户 Token 校验
    this.http.post('accounts/login/?_allow_anonymous=true', formData).subscribe((res: any) => {
      // 保存用户的 token 信息在缓存里面
      this.tokenService.set(res.data);

      // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响
      this.startupService.load().then(() => {
        let url = this.tokenService.referrer!.url || '/';
        if (url.includes('/passport')) {
          url = '/';
        }
        this.router.navigateByUrl(url);
      });
    });
  }

这里要说一句 ng-alain 的 JWTInterceptor 还提供了判断是否过期的功能,解析 payload 的功能,这部分真的很实用!

总结

虽然只是简单的集成工作,但是其实我也摸索了不少时间,这里算是头一次使用这个认证系统,也算是弥补了之前的自己的遗憾,没有在公司项目中用上,不过这里也不算是完全使用了,因为在集成过程中还特意看了下标准的使用方法,以及集成第三方的方式,发现,一般返回给客户端的通常会包含两个 token 值,一个是 access_token 一个是 refresh_token,这两个光看名字就知道是该如何使用了,当 access_token 过期了,使用 refresh_token 进行刷新,就可以继续访问了,通常情况下 refresh_token 是长期保存的,可以根据用户的申请的过程中进行时间设定,然后是 access_token 通常是设置短一点的时间,这样可以帮助用户传输过程中的安全。 不过我发现 django-rest-framework-jwt 这个插件,并没有这个功能,或者说它定义的 refresh 概念并不是一样的,所以,后续还是得手撸一个插件比较靠谱,虽然会很慢,但是后续自己的项目中,这么使用会一本万利~