在Angular中使用JWT认证方法示例


Posted in Javascript onSeptember 10, 2018

本文介绍了在Angular中使用JWT认证方法示例,分享给大家,具体如下:

在Angular中使用JWT认证方法示例

项目地址: grading-system

基于session的认证和基于token的认证的方式已经被广泛使用。在session认证中,服务端会存储一份用户登录信息,这份登录信息会在响应时传递给浏览器并保存为Cookie,在下次请求时,会带上这份登录信息,这样就能识别请求来自哪个用户。

在基于session的认证中,每个用户都要生成一份session,这份session通常保存在内存中,随着用户量的增加,服务端的开销会增大,而且对分布式应用不是很友好。

在token认证中,服务端不需要保留用户认证信息。当用户登录时,服务器验证用户信息后会返回一个token,这个token存储在客户端,并且在每次请求的请求头中都带上这个token,这样服务端验证token后就可以返回数据。

JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。 此信息可以通过数字签名进行验证和信任。特别适用于分布式站点的单点登录(SSO)场景。

JWT 是什么,为何要使用 JWT?

JWT 是 JSON Web Tokens 的简称,对于这个问题最精简的回答是,JWT 具有简便、紧凑、安全的特点,具体来看:

简便:只要用户登陆后,使用 JWT 认证仅需要添加一个 http header 认证信息,这可以用一个函数简单实现,我们会在后面的例子中看到这一点。

紧凑:JWT token 是一个 base 64 编码的字符串,包含若干头部信息及一些必要的数据,非常简单。签名后的 JWT 字符串通常不超过 200 字节。

安全:JWT 可以使用 RSA 或 HMAC 加密算法进行加密,确保 token 有效且防止篡改。

总之你可以有一种安全有效的方式来认证用户,并且对所有 api 调用都进行认证,而不需要解析复杂的数据结构或者实现自己的加密算法。

JWT的构成

JWT由 . 分隔的三个部分组成,它们是:

  • 头部(Header)
  • 荷载(Playload)
  • 签名(Signature)

也就是说,JWT只是一个具有以下格式的字符串:

header.payload.signature

头部

头部通常由两部分组成:令牌的类型(即JWT)以及正在使用的散列算法,例如HMAC SHA256或RSA。

header.payload.signature

然后,对这个JSON进行Base64编码,形成JWT的第一部分。

ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9

荷载

JWT的第二部分是荷载,其中包含声明。 声明是关于实体(通常是用户)和其他数据的声明。声明有三种:注册的声明、公开的声明和私有的声明。

JWT规范定义了七个在标准中注册的声明名称,它们是:

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

对于特定情况,可以使用公共的声明名称。 这些包括:

  • auth_time:身份验证发生的时间
  • acr:认证上下文类的引用
  • nonce:用于将客户端会话与ID Token关联的值

最后,还有私有的声明名称,可以使用它们来传达与身份相关的信息,例如姓名或部门。

由于公共和私人的声明未注册,请注意避免名称冲突。

比如,我们定义一个palyload:

{
 "sub": "1234567890",
 "name": "tc9011",
 "admin": true,
 "exp": 1441594722
}

然后将其进行base64加密,得到JWT的第二部分:

ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAidGM5MDExIiwKICAiYWRtaW4iOiB0cnVlLAogICJleHAiOiAxNDQxNTk0NzIyCn0=

签名

签名由base64编码后的头、base64编码后的荷载和secret组成。

例如,将上面的两个编码后的字符串都用句号 . 连接在一起(头部在前),就形成了:

ewogICJhbGciOiAiSFMyNTYiLAogICJ0eXAiOiAiSldUIgp9.ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAidGM5MDExIiwKICAiYWRtaW4iOiB0cnVlLAogICJleHAiOiAxNDQxNTk0NzIyCn0=

然后,将上面拼接完的字符串用secret作为秘钥进行HS256加密。

HMACSHA256(
 base64UrlEncode(header) + "." +
 base64UrlEncode(payload),
 secret)

使用JWT

一般在会在请求头中加入 Authorization ,并加上 Bearer 进行标注:

在Angular中使用JWT认证方法示例

fetch('api/v1/user/1', {
 headers: {
  'Authorization': 'Bearer ' + token
 }
})

服务端会验证token,如果验证通过就会返回相应的资源。

不过要注意,因为荷载是base64编码,这种编码可以对称解密,所以在荷载中不应该存放用户的敏感信息,比如密码。所以一般JWT用来向Web传递一些非敏感信息,例如用户名、所属部门等。

在Angular中使用JWT

这里我们以Angular6和koa2(使用TypeScript)为例,介绍一下如何在你的Angular应用中使用JWT。

服务端

首先在jwt.io 官网上找到node的JWT的库: jsonwebtoken 。

在Angular中使用JWT认证方法示例

可以看到官网把这个库对标准注册声明字段的支持情况以及加密方式的支持情况都列出来了。除了这个库,还需要使用koa一个中间件: koa-jwt ,用来对HTTP请求进行JWT认证。你可以通过下面命令安装这两个库:

npm i koa-jwt jsonwebtoken --save

app.ts 中:

import * as jwt from 'koa-jwt';

app.use(jwt({
   secret: Secret
  }).unless({
   path: [/\/register/, /\/login/, /\/groups/],
  }));

这里的secret就是你自己定义的秘钥, unless 方法用来排除一些不需要进行JWT认证的api。koa-jwt中间件需要放在路由中间件之前,这样就可以对所有路由(除了 unless 中设置的路由外)进行JWT的检查。只有正确之后才能正确的访问。

除此之外,你还要自定义一个401错误处理的中间件,如果没有token,或者token失效,该中间件会给出对应的错误信息。如果没有自定义中间件的话,会直接将 koa-jwt 暴露的错误信息直接返回给用户。

export const errorHandle = (ctx, next) => {
 return next().catch((err) => {
  if (err.status === 401) {
   ctx.status = 401;
   handleError({ctx, message: '登录过期,请重新登录', err: err.originalError ? err.originalError.message : err.message});
  } else {
   throw err;
  }
 });
};

然后把这个中间件放在koa-jwt之前:

app.use(errorHandle);

app.use(jwt({
  secret: Secret
}).unless({
  path: [/\/register/, /\/login/, /\/groups/],
}));

在用户登陆时候,生成token,返回给客户端:

// 生成 token 返回给客户端
const token = jsonwebtoken.sign({
  user: {
    workNumber: user.workNumber,
    realName: user.realName,
    group: user.group,
    role: user.role
  },

  // 设置 token 过期时间
  exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24),  // 1天
}, Secret);

handleSuccess({
  ctx,
  message: '登陆成功!',
  response: {
    token,
    lifeTime: Math.floor(Date.now() / 1000) + (60 * 60 * 24)  // 1天
  }
});

需要注意的是,在使用 jsonwebtoken.sign() 时,需要传入的 secret 参数,这里的 secret 必须要与 前面设置 jwt() 中的 secret 一致。

客户端

在Angular中,我们需要使用 @auth0/angular2-jwt 这个库来帮助我们在Angular中处理JWT:

npm install @auth0/angular-jwt --save

app.module.ts 中引入 JwtModule 这个模块(注意,引入该模块的同时也要引入 HttpClientModule 模块):

import { JwtModule } from '@auth0/angular-jwt';
import { HttpClientModule } from '@angular/common/http';

export function tokenGetter(){
 return localStorage.getItem('token');
}

@NgModule({
 bootstrap: [AppComponent],
 imports: [
  // ...
  HttpClientModule,
  JwtModule.forRoot({
   config: {
    tokenGetter: tokenGetter,
    whitelistedDomains: ['localhost:3001'],
    blacklistedRoutes: ['localhost:3001/auth/']
   }
  })
 ]
})
export class AppModule {}

JwtModuleconfig 中:

tokenGetter :从localStorage中获取token;

whitelistedDomains :允许发送认证的请求的域名;

blacklistedRoutes :你不希望替换header中 Authorization 信息的api列表。

接着创建一个全局的 auth.service.ts 服务,方便在登陆的时候获取用户相关信息及权限,这个服务中有个 login 方法,用来处理登陆后返回的token信息,并把token存到LocalStorage中,这样在token失效前,下次用户登陆时就不需要输入用户名和密码:

login(loginInfo: LoginInfo): Observable<boolean> {
  return this.passportService.postLogin(loginInfo).pipe(map(
    (res: LoginRes) => {
      // 登陆成功后获取token,并存到localStorage
      this.storageService.setLocalStorage('token', res.token);
      const decodedUser = this.decodeUserFromToken(res.token);
      this.setCurrentUser(decodedUser);
      this.msg.success('登录成功!');
      return this.loggedIn;
    }
  )
                             );
}

在这个 login 方法中, decodeUserFromToken 封装了 @auth0/angular2-jwt 中提供的 decodeToken 方法,注意 decodeToken 方法解析出来的只是服务端 jsonwebtoken.sign() 中的JSON对象,所以需要通过 . 操作获取 jsonwebtoken.sign() 中定义的 user

decodeUserFromToken(token): User {
  return this.jwtHelperService.decodeToken(token).user;
}

在这个服务中,定义了两个变量 loggedInisAdmin ,用来标识用户是否登录和其相应的权限,方便在Angular路由中控制可以访问的视图。

有登录当然就有登出,登出时只需把token从LocalStorage中移除,并把几个变量重置即可:

logout(): void {
  this.storageService.removeLocalStorage('token');
  this.loggedIn = false;
  this.isAdmin = false;
  this.currentUser = new User();
}

AuthService 的完整代码如下:

import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';

import { JwtHelperService } from '@auth0/angular-jwt';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { LoginInfo, LoginRes, User } from '../../views/passport/interfaces/passport';
import { PassportService } from '../../views/passport/services/passport.service';
import { StorageService } from '../storage/storage.service';
import { NzMessageService } from 'ng-zorro-antd';


@Injectable()
export class AuthService {
 public loggedIn = false;
 public isAdmin = false;
 public currentUser: User = new User();

 constructor(private jwtHelperService: JwtHelperService,
       private router: Router,
       private injector: Injector,
       private passportService: PassportService,
       private storageService: StorageService) {
  const token = localStorage.getItem('token');
  if (token) {
   const decodedUser = this.decodeUserFromToken(token);
   this.setCurrentUser(decodedUser);
  }
 }

 get msg(): NzMessageService {
  return this.injector.get(NzMessageService);
 }

 login(loginInfo: LoginInfo): Observable<boolean> {
  return this.passportService.postLogin(loginInfo).pipe(map(
   (res: LoginRes) => {
     this.storageService.setLocalStorage('token', res.token);
     const decodedUser = this.decodeUserFromToken(res.token);
     this.setCurrentUser(decodedUser);
     this.msg.success('登录成功!');
     return this.loggedIn;
    }
   )
  );
 }

 logout(): void {
  this.storageService.removeLocalStorage('token');
  this.loggedIn = false;
  this.isAdmin = false;
  this.currentUser = new User();
 }

 decodeUserFromToken(token): User {
  return this.jwtHelperService.decodeToken(token).user;
 }

 setCurrentUser(decodedUser): void {
  this.loggedIn = true;
  this.currentUser.workNumber = decodedUser.workNumber;
  this.currentUser.realName = decodedUser.realName;
  this.currentUser.group = decodedUser.group;
  this.currentUser.role = decodedUser.role;
  this.isAdmin = decodedUser.role > 10;
  delete decodedUser.role;
 }
}

至此,在你的Angular应用中就引入了JWT认证,当然,你也可以不使用 @auth0/angular2-jwt ,自己手写一个HTTP拦截器,手动设置每次请求的header:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>,
       next: HttpHandler): Observable<HttpEvent<any>> {

    const token = localStorage.getItem("token");

    if (token) {
      const cloned = req.clone({
        headers: req.headers.set("Authorization",
          "Bearer " + token)
      });

      return next.handle(cloned);
    }
    else {
      return next.handle(req);
    }
  }
}

不过这样的话,token Base64解码也需要自己手写,稍微麻烦一点。

总结

JWT因为是基于JSON的,所以通用性很强,很多语言已经存在jwt相关的库。不过使用JWT的时候需要注意以下几点:

  • 保存好secret秘钥,这个秘钥只能在服务端存在
  • 给token设置一个过期时间,因为一旦token生成,它就永远有效,除非token密钥被更改或过期
  • 在payload中只能存储一些业务逻辑所必要的非敏感信息

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery的实现原理的模拟代码 -4 重要的扩展函数 extend
Aug 03 Javascript
Asp.net下使用Jquery Ajax传送和接收DataTable的代码
Sep 12 Javascript
dwz 如何去掉ajaxloading具体代码
May 22 Javascript
Node.js的特点和应用场景介绍
Nov 04 Javascript
javascript获得当前的信息的一些常用命令
Feb 25 Javascript
JavaScript中匿名函数用法实例
Mar 23 Javascript
JavaScript用select实现日期控件
Jul 17 Javascript
基于jQuery实现音乐播放试听列表
Apr 14 Javascript
微信小程序中input标签详解及简单实例
May 18 Javascript
浅谈webpack对样式的处理
Jan 05 Javascript
javascript合并两个数组最简单的实现方法
Sep 14 Javascript
vue npm install 安装某个指定的版本操作
Aug 11 Javascript
详解vue-router传参的两种方式
Sep 10 #Javascript
详解Vue.js使用Swiper.js在iOS
Sep 10 #Javascript
webuploader分片上传的实现代码(前后端分离)
Sep 10 #Javascript
在Vue 中使用Typescript的示例代码
Sep 10 #Javascript
ES6使用export和import实现模块化的方法
Sep 10 #Javascript
Vuejs 实现简易 todoList 功能 与 组件实例代码
Sep 10 #Javascript
浅谈微信小程序flex布局基础
Sep 10 #Javascript
You might like
基于header的一些常用指令详解
2013/06/06 PHP
根据中文裁减字符串函数的php代码
2013/12/03 PHP
PHP中使用正则表达式提取中文实现笔记
2015/01/20 PHP
PHP多进程简单实例小结
2019/11/09 PHP
Laravel 框架控制器 Controller原理与用法实例分析
2020/04/14 PHP
JavaScript 高级语法介绍
2009/06/15 Javascript
JavaScript文本框脚本编写的注意事项
2016/01/25 Javascript
微信小程序 常见问题总结(4058,40013)及解决办法
2017/01/11 Javascript
Angualrjs和bootstrap相结合实现数据表格table
2017/03/30 Javascript
Angular父组件调用子组件的方法
2018/04/02 Javascript
简单易扩展可控性强的Jquery转盘抽奖程序
2019/03/16 jQuery
微信小游戏之使用three.js 绘制一个旋转的三角形
2019/06/10 Javascript
原生js实现表格翻页和跳转
2020/09/29 Javascript
[15:07]lgd_OG_m2_BP
2019/09/10 DOTA
python两种遍历字典(dict)的方法比较
2014/05/29 Python
python脚本爬取字体文件的实现方法
2017/04/29 Python
详解python中executemany和序列的使用方法
2017/08/12 Python
Python通过matplotlib画双层饼图及环形图简单示例
2017/12/15 Python
对python中使用requests模块参数编码的不同处理方法
2018/05/18 Python
基于Django框架利用Ajax实现点赞功能实例代码
2018/08/19 Python
python3.7.0的安装步骤
2018/08/27 Python
解决Pycharm运行时找不到文件的问题
2018/10/29 Python
配置 Pycharm 默认 Test runner 的图文教程
2018/11/30 Python
Python并发:多线程与多进程的详解
2019/01/24 Python
Python3多线程基础知识点
2019/02/19 Python
python使用协程实现并发操作的方法详解
2019/12/27 Python
python opencv肤色检测的实现示例
2020/12/21 Python
html5 canvas 使用示例
2010/10/22 HTML / CSS
莫斯科绝对前卫最秘密的商店:SVMoscow
2017/10/23 全球购物
美国男士内衣品牌:Tommy John
2017/12/22 全球购物
什么是Smart Navigation?
2016/07/03 面试题
给海归自荐信的建议
2013/12/13 职场文书
2014年语文教研组工作总结
2014/12/06 职场文书
python Django框架快速入门教程(后台管理)
2021/07/21 Python
Windows11里微软已经将驱动程序安装位置A盘删除
2021/11/21 数码科技
浅谈Node的内存泄露问题
2022/05/06 NodeJs