美文网首页
Angular利用客户端存储技术存取JWT

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

作者: 阿狸不歌 | 来源:发表于2019-04-13 14:55 被阅读0次

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

    相关文章

      网友评论

          本文标题:Angular利用客户端存储技术存取JWT

          本文链接:https://www.haomeiwen.com/subject/zemdiqtx.html