Angular利用客户端存储技术存取JWT

在所有的客户端存储技术中,Web Storage可能是学习周期最短的,也是最容易学会的。Web Storage 主要通过key设置和检索简单的值。本文在Angular框架下利用Web Storage来存储JWT,并实现身份认证。

《客户端存储技术》 封面

准备工作

本文的项目将在 《Angular初探PWA》的项目基础上添加用户登录功能,所以部分代码将在该文基础上修改。

1、进入项目根目录,安装jsonwebtoken

$ npm install --save-dev jsonwebtoken

2、在项目根目录添加auth.js文件,由于本demo并不涉及用户的创建与管理,所以写死了一个用户名与密码,千万不要在真实项目中这么干哦😄,用户在成功登录后,该中间件将返回给前端一个JWT

const jwt = require("jsonwebtoken");
const APP_SECRET = "myappsecret";  
const USERNAME = "admin";   // ⚠️ 在实际项目中不要这样写死
const PASSWORD = "secret";  // ⚠️ 在实际项目中不要这样写死
module.exports = function (req, res, next) {
    if ((req.url == "/api/login" || req.url == "/login") && req.method == "POST") {
        if (req.body != null && req.body.name == USERNAME && req.body.password == PASSWORD) {
            let token = jwt.sign({ data: USERNAME, expiresIn: "1h" }, APP_SECRET);
            res.json({ success: true, token: token });
        } else {
            res.json({ success: false });
        }
        res.end();
        return;
    } else if ((((req.url.startsWith("/api/rooms") || req.url.startsWith("/rooms"))) && req.method != "GET")) {
            let token = req.headers["authorization"];
            if (token != null && token.startsWith("Bearer<")) {
                token = token.substring(7, token.length - 1);
                try {
                    jwt.verify(token, APP_SECRET);
                    next();
                    return;
                } catch (err) { }
            }
            res.statusCode = 401;
            res.end();
            return;
    }
    next(); 
}

3、修改package.json 添加 auth中间件,这样前端对后端数据的访问就要通过auth中间件的检查

...
"scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "json": "json-server data.js -p 3500 -m auth.js"
}, 
...

登录服务,利用 Web Storage 存取JWT

创建auth service

$ ng g s services/auth

修改 auth.service.ts 文件如下, 在 auth 的不同环节分别使用了 localStorage.setItem、getItem、removeItem

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  loginUrl = `http://${location.hostname}:3500/login`;

  constructor(private http: HttpClient) {
  }

  login(name: string, password: string): Observable<boolean> {
    return this.http.post<any>(this.loginUrl, {name, password})
      .pipe(map(response => {
        // ⚠️ 此处 使用 localStorage setItem 存储 jwt
        if (response.success && response.token) {
          localStorage.setItem('access_token', response.token);
        }
        return response.success;
    }));
  }

  get loggedIn(): boolean {
        // ⚠️ 通过鉴定在 localStorage 是否存有 access_token 来判断是否已经登录
    return localStorage.getItem('access_token') !==  null;
  }

  logout() {
    // ⚠️ 退出登录 的时候抹掉 jwt
    localStorage.removeItem('access_token');
  }
}

Web 存储有两个版本:本地存储(Local Storage)和会话存储(Session Storage)。两者使用完全相同的 API,但本地存储会持久存在(比如本程序在登录后,我们可以先把页面关闭,再打开网址,会发现登录状态仍然存在,手动退出登录状态后,存在Local Storage中的JWT才会被清除),而会话存储只要浏览器关闭就会消失。在上面的代码中,我们也可以把 localStorage 替换成 sessionStorage 来体验两者的差别。


创建login组件

$ ng g c components/login

修改login.component.ts代码如下

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { first } from 'rxjs/operators';

import { AuthService } from '../../services/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  validateForm: FormGroup;
  errMsg: string;

  constructor(
    private fb: FormBuilder,
    private auth: AuthService,
    private router: Router,
  ) {}

  ngOnInit(): void {
    this.validateForm = this.fb.group({
      username: [null, [Validators.required]],
      password: [null, [Validators.required]],
      remember: [true]
    });
  }

  get f() { return this.validateForm.controls; }

  submitForm(): void {
    this.auth.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe(response => {
                  if (response) {
                    this.router.navigateByUrl('rooms');  // 登录成功则转到列表页
                  }
                  this.errMsg = '登录失败';
                });
  }
}

修改login.component.html代码如下

<form nz-form [formGroup]="validateForm" (ngSubmit)="submitForm()">
  <nz-form-item>
    <nz-form-control>
      <nz-input-group [nzPrefix]="prefixUser">
        <input type="text" nz-input formControlName="username" placeholder="用户名" />
      </nz-input-group>
      <nz-form-explain *ngIf="validateForm.get('userName')?.dirty && validateForm.get('userName')?.errors"
        >请输入用户名!</nz-form-explain
      >
    </nz-form-control>
  </nz-form-item>
  <nz-form-item>
    <nz-form-control>
      <nz-input-group [nzPrefix]="prefixLock">
        <input type="password" nz-input formControlName="password" placeholder="密码" />
      </nz-input-group>
      <nz-form-explain *ngIf="validateForm.get('password')?.dirty && validateForm.get('password')?.errors"
        >请输入密码!</nz-form-explain
      >
    </nz-form-control>
  </nz-form-item>
  <nz-form-item>
    <nz-form-control>
      <button nz-button [nzType]="'primary'" nzBlock>登录</button>
    </nz-form-control>
  </nz-form-item>
</form>
<nz-tag *ngIf='errMsg' nzColor='red'>{{errMsg}}</nz-tag>
<ng-template #prefixUser><i nz-icon type="user"></i></ng-template>
<ng-template #prefixLock><i nz-icon type="lock"></i></ng-template>

我们在登录页上点击“登录”按钮时,会调用AuthService的的login函数,将用户名、密码传入后端,后端校验成功后,会回传JWT,并通过Web Storage存储起来。


修改首页

修改home.component.ts如下,与原来相比,添加了AuthService的依赖注入,从而可以判断登录状态并显示不同的按钮。

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../services/auth.service';

@Component({
  selector: 'app-home',
  template: `
    <a nz-button nzType="primary" nzSize="large" nzBlock routerLink="rooms" *ngIf="auth.loggedIn">
      欢迎光临哥谭帝国酒店
    </a>
    <a nz-button nzType="dashed" nzSize="large" nzBlock routerLink="login" *ngIf="!auth.loggedIn">
      请先登录
    </a>
    <a nz-button nzType="dashed" nzSize="large" nzBlock *ngIf="auth.loggedIn" (click)="auth.logout()">
      退出登录
    </a>
  `,
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  constructor(
    private auth: AuthService
  ) { }

  ngOnInit() {
  }
}

修改路由

修改app.module.ts文件,将login的路由添加进去

...
RouterModule.forRoot([
      { path: '', component: HomeComponent},
      { path: 'rooms', component: RoomsComponent },
      { path: 'login', component: LoginComponent},
    ]),
...

测试

1、启动后端服务

$ npm run json

2、启动ng serve

$ ng serve --port 0 --open

未登录状态
登录成功后的状态

小结

本文探讨了利用客户端存储技术来保存JWT信息,在此基础上其实还可以轻松的实现Auth Guard、Http Interceptors等功能,留待以后讨论😄

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容