angular8教程(4)-登录拦截

上一篇教程我们讲到了要做一个后台管理系统的项目,任何要使用这个系统的人都必须先登录,所以本篇就来说明一下如何做登录拦截。从本篇教程开始,前端的代码会和后台做通信,后台使用的是我在另一个系列教程Springboot入门教程中的后台代码,建议使用我在github上面的代码https://github.com/ahuadoreen/studentmanager搭建后台。

  1. 首先我们需要新建一个login组件,为了方便,我们可以直接利用ng-zorro官方的login示例组件来生成之后再做修改。先到https://ng.ant.design/components/form/zh,找到login示例组件如图
    login示例组件

    点击下方四个图标按钮中的第三个“复制生成代码命令”,之后去idea的最下方找Terminal那一栏,粘贴命令,需要修改的是<name>要输入组件名称login,完整的命令是ng g ng-zorro-antd:form-normal-login login,然后回车运行。
    有可能你会遇到“More than one module matches. Use skip-import option to skip importing the component into the closest module.”的错误,那是因为我们之前在引入ng-zorro的时候多生成了一个IconProvideModule,这里我们可以把它删掉,把app.module.ts中引入的代码也删掉,详情可以参考angular8教程(2)-引入ng-zorro。之后再运行应该就能成功了。自动生成的组件会被直接导入到app.module.ts中,不需要我们手动导入。
  2. 接着需要新建一个路由守卫,它的功能是使未登录的用户强制跳转到登录页面,对于这样的功能,我们需要实现的是一个canActive的守卫(关于路由守卫的详细介绍可以查看官方文档路由守卫)。
    在app文件夹上右键,依次选择New->Angular Schematic->guard,输入login,点击OK。之后在控制台需要选择继承的类,按空格键后选中canActive再按回车,就生成了一个login.guard.ts的文件 。
    选择guard继承的类

    修改代码,写入拦截未登录的逻辑
import { Injectable } from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class LoginGuard implements CanActivate {
  constructor(private router: Router) {
  }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean {
    let isLogin: boolean;
    // 判断用户是否登录
    const token = sessionStorage.getItem('token');
    if (!token) {
      isLogin = false;
      // 未登录跳转到登录界面
      this.router.navigateByUrl('/login');
    } else {
      isLogin = true;
    }
    return isLogin;
  }
}

这里主要是添加了一个构造函数constructor,利用依赖注入的特性声明了一个router的路由变量(关于依赖注入的概念见官方文档依赖注入)。这是angular中一个非常重要且有用的特性,如果学习过java后台的spring框架的朋友相信对这个概念已非常熟悉,如果没有学过,那也不要紧,在后面的使用中可以慢慢熟悉起来。
我们设定用户登录后会获取后台返回的一个token保存在sessionStorage中,所以这里我们判断如果sessionStorage中没有token,那说明用户没有登录过,则跳转到登录页面,否则返回true,允许直接跳转到目标页面。

  1. 修改路由配置文件app-routing.module.ts。
const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: '/home' },
  { path: 'home', component: HomeComponent, canActivate: [LoginGuard] },
  { path: 'login', component: LoginComponent}
];

这里我们给home组件的路由添加一个canActivate指向LoginGuard,再添加一个login的路由配置。
接着我们可以运行项目,访问localhost:4200,可以看到这次不会直接跳转到home页面,而是跳转到了login页面。

接下来我们要修改login组件来完成登录逻辑。

  1. 先修改一下界面,修改样式文件login.component.css中的login-form样式
.login-form {
        -webkit-border-radius: 5px;
        border-radius: 5px;
        -moz-border-radius: 5px;
        background-clip: padding-box;
        width: 350px;
        padding: 35px 35px 15px 35px;
        background: #fff;
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: -200px;
        margin-top: -200px;
        border: 1px solid #eaeaea;
        box-shadow: 0 0 25px #cac6c6;
      }

修改布局文件login.component.html

<form nz-form [formGroup]="validateForm" class="login-form" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-control nzErrorTip="请输入用户名!">
          <nz-input-group nzPrefixIcon="user">
            <input type="text" nz-input formControlName="username" placeholder="用户名" />
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control nzErrorTip="请输入密码!">
          <nz-input-group nzPrefixIcon="lock">
            <input type="password" nz-input formControlName="password" placeholder="密码" />
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item>
        <nz-form-control>
          <label nz-checkbox formControlName="remember">
            <span>记住我</span>
          </label>
          <a class="login-form-forgot" class="login-form-forgot">忘记密码</a>
          <button nz-button class="login-form-button" [nzType]="'primary'">登录</button>
        </nz-form-control>
      </nz-form-item>
    </form>
  1. 我们需要创建一个service层用来处理各种http请求,先新建一个service路径,再在里面新建一个login.service.ts文件,代码如下:
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {catchError} from 'rxjs/operators';
import {of} from 'rxjs/internal/observable/of';
import qs from 'qs';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' })
};
@Injectable({
  providedIn: 'root'
})
export class LoginService {
  private loginUrl = '/studentmanage/login';

  constructor(private http: HttpClient) { }
  login(username: string, password: string): Observable<string> {
    const options = { username, password };
    return this.http.post<string>(this.loginUrl, qs.stringify(options), httpOptions)
      .pipe(
        catchError(this.handleError('login', username))
      );
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {

      // TODO: send the error to remote logging infrastructure
      console.error(error); // log to console instead

      // Let the app keep running by returning an empty result.
      return of(result as T);
    };
  }
}

这里我们给component层提供了一个login的接口,用来发送login的post请求。angular给我们提供了现成的http请求的类HttpClient用来处理各种http请求,具体参见官方文档HttpClient。这里需要注意的是请求参数格式的问题,由于我的后台接收的是Content-Type为application/x-www-form-urlencoded格式的参数,而angular默认的Content-Type为application/json,也因为如此,我用了qs格式化了请求参数。

  1. 修改login.component.ts的逻辑代码
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {Router} from '@angular/router';
import {LoginService} from '../service/login.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  validateForm: FormGroup;

  submitForm(): void {
    for (const i in this.validateForm.controls) {
      this.validateForm.controls[i].markAsDirty();
      this.validateForm.controls[i].updateValueAndValidity();
      if (this.validateForm.controls[i].invalid) {
        return;
      }
    }
    this.loginService.login(this.validateForm.value.username, this.validateForm.value.password)
      .subscribe(result => this.loginSuccess(result));
  }
  loginSuccess(result: any) {
    console.log('result: ' + JSON.stringify(result));
    const data = result.data;
    const token = data.token;
    const username = data.username;
    sessionStorage.setItem('token', token);
    sessionStorage.setItem('username', username);
    this.router.navigate(['/home']);
  }

  constructor(private fb: FormBuilder, private router: Router, private loginService: LoginService) {}

  ngOnInit(): void {
    sessionStorage.removeItem('token');
    sessionStorage.removeItem('username');
    this.validateForm = this.fb.group({
      username: ['admin', [Validators.required]],
      password: ['qwertyuiop', [Validators.required]],
      remember: [true]
    });
  }
}

这里我们利用依赖注入的特性调用创建的login service来完成login请求,请求之前的默认数据绑定和数据校验angular的form模块也提供了现成的api来完成。
到这里登录的逻辑已经完成了,但是我们还无法访问后台,这是因为根据同源策略,我们的http请求必然会发生跨域的问题,那么怎么解决呢。在开发阶段,我们可以用以下方式:

  1. 在项目根目录下创建一个proxy.conf.json的文件,添加如下配置
{
  "/studentmanage": {
    "target": "http://localhost:8080",
    "secure": false
  }
}

这里的配置代表匹配"/studentmanage"这个路径的请求都会转到target的路径下,所以我们在login.service的请求路径中只写了"/studentmanage/login",实际最终请求会被转发到"http://localhost:8080/studentmanage/login"这个完整的路径下。

  1. 修改package.json中的启动配置,将scripts中的start配置改为
"start": "ng serve --proxy-config proxy.conf.json"

完成后重新启动项目,如果你的后台已经正常运行了, 那么点击登录按钮就能成功登录进入home界面。

到这里还没有结束,我们还有一项工作要做。完成登录逻辑之后,我们拿到了后台返回的token并保存到了sessionStorage中,后面的其他请求我们都需要带着这个token,因此我们需要给除了login请求之外的其他请求做一个拦截,让它们统一带上这个token。

  1. 新建一个http请求的拦截器MyInterceptor,它需要继承HttpInterceptor。
import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
  HttpResponse,
  HttpResponseBase
} from '@angular/common/http';
import {Observable, throwError} from 'rxjs';
import {Router} from '@angular/router';
import {of} from 'rxjs/internal/observable/of';
import {mergeMap} from 'rxjs/internal/operators/mergeMap';
import {catchError, retry} from 'rxjs/operators';

export class MyInterceptor implements HttpInterceptor {
  constructor(private router: Router) {}
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let authReq: any;
    // 实现不拦截的方式:1. 指定接口不拦截  2. 判断本地sessionStorage
    if (!req.url.includes('login')) {
      const token = sessionStorage.getItem('token');
      const username = sessionStorage.getItem('username');
      console.log(token + username);
      authReq = req.clone({ setHeaders: {  token, username } });
      return next.handle(authReq).pipe(
        mergeMap((event: any) => {
          // 允许统一对请求错误处理
            if (event instanceof HttpResponse) {
                  const body: any = event.body;
                  console.log(body);
                  if (body.code !== 200) {
                      // 继续抛出错误中断后续所有 Pipe、subscribe 操作,因此:
                      // this.http.get('/').subscribe() 并不会触发
                    if (body.code === 401) {// token过期,并且无法刷新,需要重新登录
                      this.router.navigate(['/login']);
                    } else if (body.code === 100) {// token过期,服务器端自动刷新了token,需要用新的token重新发送请求
                      const newToken = body.token;
                      sessionStorage.setItem('token', newToken);
                      console.log('new token: ' + newToken);
                      authReq = req.clone({ setHeaders: {  token: newToken, username } });
                      return next.handle(authReq).pipe();
                    }
                    return throwError({});
                  } else {
                      // 重新修改 `body` 内容为 `response` 内容,对于绝大多数场景已经无须再关心业务状态码
                      // return of(new HttpResponse(Object.assign(event, { body: body.response })));
                      // 或者依然保持完整的格式
                      return of(event);
                  }
              } else {
              return of(event);
            }
        }),
        catchError((err: HttpErrorResponse) => this.handleData(err))
      );
    }
    authReq = req.clone();
    return next.handle(authReq);
  }

  private handleData(ev: HttpResponseBase): Observable<any> {
    // 可能会因为 `throw` 导出无法执行 `_HttpClient` 的 `end()` 操作
    console.log(ev.status);
    return of(ev);
  }
}

这里我们判断如果是login请求便不需要拦截,其他请求则将username和token放到header中发送。并且也统一拦截了返回的数据先统一处理,这里重点处理了两个状态,一个是401需要重新登录,另一个是100,服务器端返回了新的token,这个拦截器保存了前一次请求的数据在req参数中,所以我们只需要把新的token重新放入header中再次做req.clone就可以了。

  1. 在app.module.ts中配置,这个拦截器需要配置到providers中
providers: [{ provide: NZ_I18N, useValue: zh_CN }, { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }],

至此,登录拦截的全部功能才算完成。
代码依然可以参考https://github.com/ahuadoreen/studentmanager-cli

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容