Angular利用客戶端存儲(chǔ)技術(shù)存取JWT

在所有的客戶端存儲(chǔ)技術(shù)中,Web Storage可能是學(xué)習(xí)周期最短的嗦玖,也是最容易學(xué)會(huì)的浩销。Web Storage 主要通過(guò)key設(shè)置和檢索簡(jiǎn)單的值。本文在Angular框架下利用Web Storage來(lái)存儲(chǔ)JWT芹敌,并實(shí)現(xiàn)身份認(rèn)證。

《客戶端存儲(chǔ)技術(shù)》 封面

準(zhǔn)備工作

本文的項(xiàng)目將在 《Angular初探PWA》的項(xiàng)目基礎(chǔ)上添加用戶登錄功能垮抗,所以部分代碼將在該文基礎(chǔ)上修改氏捞。

1、進(jìn)入項(xiàng)目根目錄冒版,安裝jsonwebtoken

$ npm install --save-dev jsonwebtoken

2液茎、在項(xiàng)目根目錄添加auth.js文件,由于本demo并不涉及用戶的創(chuàng)建與管理辞嗡,所以寫死了一個(gè)用戶名與密碼捆等,千萬(wàn)不要在真實(shí)項(xiàng)目中這么干哦??,用戶在成功登錄后续室,該中間件將返回給前端一個(gè)JWT

const jwt = require("jsonwebtoken");
const APP_SECRET = "myappsecret";  
const USERNAME = "admin";   // ?? 在實(shí)際項(xiàng)目中不要這樣寫死
const PASSWORD = "secret";  // ?? 在實(shí)際項(xiàng)目中不要這樣寫死
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中間件,這樣前端對(duì)后端數(shù)據(jù)的訪問(wèn)就要通過(guò)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"
}, 
...

登錄服務(wù)挺狰,利用 Web Storage 存取JWT

創(chuàng)建auth service

$ ng g s services/auth

修改 auth.service.ts 文件如下明郭, 在 auth 的不同環(huán)節(jié)分別使用了 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 存儲(chǔ) jwt
        if (response.success && response.token) {
          localStorage.setItem('access_token', response.token);
        }
        return response.success;
    }));
  }

  get loggedIn(): boolean {
        // ?? 通過(guò)鑒定在 localStorage 是否存有 access_token 來(lái)判斷是否已經(jīng)登錄
    return localStorage.getItem('access_token') !==  null;
  }

  logout() {
    // ?? 退出登錄 的時(shí)候抹掉 jwt
    localStorage.removeItem('access_token');
  }
}

Web 存儲(chǔ)有兩個(gè)版本:本地存儲(chǔ)(Local Storage)和會(huì)話存儲(chǔ)(Session Storage)薯定。兩者使用完全相同的 API,但本地存儲(chǔ)會(huì)持久存在(比如本程序在登錄后瞳购,我們可以先把頁(yè)面關(guān)閉话侄,再打開網(wǎng)址,會(huì)發(fā)現(xiàn)登錄狀態(tài)仍然存在学赛,手動(dòng)退出登錄狀態(tài)后年堆,存在Local Storage中的JWT才會(huì)被清除),而會(huì)話存儲(chǔ)只要瀏覽器關(guān)閉就會(huì)消失罢屈。在上面的代碼中嘀韧,我們也可以把 localStorage 替換成 sessionStorage 來(lái)體驗(yàn)兩者的差別。


創(chuàng)建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');  // 登錄成功則轉(zhuǎn)到列表頁(yè)
                  }
                  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"
        >請(qǐng)輸入用戶名!</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"
        >請(qǐng)輸入密碼!</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>

我們?cè)诘卿涰?yè)上點(diǎn)擊“登錄”按鈕時(shí)缠捌,會(huì)調(diào)用AuthService的的login函數(shù)锄贷,將用戶名译蒂、密碼傳入后端,后端校驗(yàn)成功后谊却,會(huì)回傳JWT柔昼,并通過(guò)Web Storage存儲(chǔ)起來(lái)。


修改首頁(yè)

修改home.component.ts如下炎辨,與原來(lái)相比捕透,添加了AuthService的依賴注入,從而可以判斷登錄狀態(tài)并顯示不同的按鈕碴萧。

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">
      歡迎光臨哥譚帝國(guó)酒店
    </a>
    <a nz-button nzType="dashed" nzSize="large" nzBlock routerLink="login" *ngIf="!auth.loggedIn">
      請(qǐng)先登錄
    </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的路由添加進(jìn)去

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

測(cè)試

1、啟動(dòng)后端服務(wù)

$ npm run json

2破喻、啟動(dòng)ng serve

$ ng serve --port 0 --open

未登錄狀態(tài)
登錄成功后的狀態(tài)

小結(jié)

本文探討了利用客戶端存儲(chǔ)技術(shù)來(lái)保存JWT信息虎谢,在此基礎(chǔ)上其實(shí)還可以輕松的實(shí)現(xiàn)Auth Guard、Http Interceptors等功能曹质,留待以后討論??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末婴噩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子羽德,更是在濱河造成了極大的恐慌几莽,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宅静,死亡現(xiàn)場(chǎng)離奇詭異章蚣,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)坏为,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門究驴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匀伏,你說(shuō)我怎么就攤上這事『拢” “怎么了够颠?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)榄鉴。 經(jīng)常有香客問(wèn)我履磨,道長(zhǎng),這世上最難降的妖魔是什么庆尘? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任剃诅,我火速辦了婚禮,結(jié)果婚禮上驶忌,老公的妹妹穿的比我還像新娘矛辕。我一直安慰自己笑跛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布聊品。 她就那樣靜靜地躺著飞蹂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翻屈。 梳的紋絲不亂的頭發(fā)上陈哑,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音伸眶,去河邊找鬼惊窖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛厘贼,可吹牛的內(nèi)容都是我干的爬坑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涂臣,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼盾计!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起赁遗,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤署辉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后岩四,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哭尝,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年剖煌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了材鹦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耕姊,死狀恐怖桶唐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茉兰,我是刑警寧澤尤泽,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站规脸,受9級(jí)特大地震影響坯约,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莫鸭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一闹丐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧被因,春花似錦卿拴、人聲如沸衫仑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惑畴。三九已至,卻和暖如春航徙,著一層夾襖步出監(jiān)牢的瞬間如贷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工到踏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杠袱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓窝稿,卻偏偏與公主長(zhǎng)得像楣富,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伴榔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容