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

96
阿狸不歌
0.1 2019.04.13 14:55* 字数 698

在所有的客户端存储技术中,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等功能,留待以后讨论😄

前端、移动端