在所有的客戶端存儲(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)證。
準(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
小結(jié)
本文探討了利用客戶端存儲(chǔ)技術(shù)來(lái)保存JWT信息虎谢,在此基礎(chǔ)上其實(shí)還可以輕松的實(shí)現(xiàn)Auth Guard、Http Interceptors等功能曹质,留待以后討論??