在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 相关文章推荐
Javascript客户端将指定区域导出到Word、Excel的代码
Oct 22 Javascript
利用javascript/jquery对上传文件格式过滤的方法
Jul 25 Javascript
JavaScript中使用正则匹配多条,且获取每条中的分组数据
Nov 30 Javascript
new Date()问题在ie8下面的处理方法
Jul 31 Javascript
Jquery ajax基础教程
Nov 20 Javascript
jQuery中的siblings用法实例分析
Dec 24 Javascript
lhgcalendar时间插件限制只能选择三个月的实现方法
Jul 03 Javascript
AngularJS实现进度条功能示例
Jul 05 Javascript
基于bootstrop常用类总结(推荐)
Sep 11 Javascript
vue draggable resizable gorkys与v-chart使用与总结
Sep 05 Javascript
微信小程序 多行文本显示...+显示更多按钮和收起更多按钮功能
Sep 26 Javascript
基于脚手架创建Vue项目实现步骤详解
Aug 03 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
欧美媒体选出10年前最流行的17部动画
2017/01/18 日漫
php笔记之:初探PHPcms模块开发介绍
2013/04/26 PHP
C#静态方法与非静态方法实例分析
2014/09/22 PHP
php获取QQ头像并显示的方法
2014/12/23 PHP
PHP简单选择排序算法实例
2015/01/26 PHP
php实现通过ftp上传文件
2015/06/19 PHP
PHP图片添加水印功能示例小结
2016/10/03 PHP
php防止sql注入的方法详解
2017/02/20 PHP
js下通过getList函数实现分页效果的代码
2010/09/17 Javascript
jquery 年会抽奖程序
2011/12/22 Javascript
JavaScript的继承的封装介绍
2013/10/15 Javascript
jquery ajax属性async(同步异步)示例
2013/11/05 Javascript
28个常用JavaScript方法集锦
2015/01/14 Javascript
解决bootstrap中modal遇到Esc键无法关闭页面
2015/03/09 Javascript
jquery悬浮提示框完整实例
2016/01/13 Javascript
javascript获取以及设置光标位置
2017/02/16 Javascript
代码详解Vuejs响应式原理
2017/12/20 Javascript
vue中设置height:100%无效的问题及解决方法
2018/07/27 Javascript
vue实现重置表单信息为空的方法
2018/09/29 Javascript
灵活使用console让js调试更简单的方法步骤
2019/04/23 Javascript
layui的表单提交以及验证和修改弹框的实例
2019/09/09 Javascript
Python自动化测试工具Splinter简介和使用实例
2014/05/13 Python
Python中利用原始套接字进行网络编程的示例
2015/05/04 Python
python脚本替换指定行实现步骤
2017/07/11 Python
浅谈python中的数字类型与处理工具
2017/08/02 Python
用pyqt5 给按钮设置图标和css样式的方法
2019/06/24 Python
详解Python3迁移接口变化采坑记
2019/10/11 Python
python中的split()函数和os.path.split()函数使用详解
2019/12/21 Python
python中JWT用户认证的实现
2020/05/18 Python
HTML5网页录音和上传到服务器支持PC、Android,支持IOS微信功能
2019/04/26 HTML / CSS
美国大尺码女装零售商:TORRID
2016/10/01 全球购物
市场营销专业个人自荐信格式
2013/09/21 职场文书
机械化及自动化毕业生的自我评价分享
2013/11/06 职场文书
公司劳动纪律管理制度
2015/08/04 职场文书
如何在向量化NumPy数组上进行移动窗口
2021/05/18 Python
vue本地构建热更新卡顿的问题“75 advanced module optimization”完美解决方案
2022/08/05 Vue.js