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等功能,留待以后讨论😄