在继续学习如何在前台获取后台返回的token以前,先带领大家了解下RxJS操作符。 可能你还不清楚什么是RxJS,但实际上你早早的就已经用上它了。每写一次`this.httpClient().subscribe()`,你便使用了一次RxJS。 至于RxJS具体是什么,又有着什么样的功能。怕是需要一个单独的教程来使用较长的篇幅来介绍它。而我们在此需要清楚的是RxJS是一种编程的模式(思想),具体来讲是一种面向数据流的编程思想。 # 生活中的数据流 笔者所在的院校至今还保持着订阅报刊的习惯,如果对某些报刊有需求可以选择在开学初向教务员提出订阅申请,在预算允许的情况下。此后每个月(月刊)或每半个月(半月刊)都会收到在开学初订阅的报刊。不仅如何,有时候还会收到一些意想不到的增刊(临时增加印刷的)。而这个报刊订阅便像极了RxJS的数据流。 对笔者而言,教务员便是数据流的来源;而教务员是不提供发行报刊的服务的,所以其订阅报刊时需要找到校级部门,而报刊最终也是由校级部门进行分发;同样的,校级部门向上可以需要找邮局,依次累推最终达到杂志社。下发报刊的过程也相同,杂志社将报刊向下交付,然后逐级的交到笔者的手中。而最终交付的报刊就是刚刚提到的**数据**,多份报刊不定期的由杂志社向读者传递,便是**数据流**。 如果RxJS中的数据流和现实生活中的报刊订阅是一个概念的话,那么我猜想它一定具有以下的性质: 1. 在进行报刊订阅时,由于是逐级向上订阅的,所以订阅过程可能失败(比如领导自认为该报刊不适合订阅或经费不足);那么RxJS的数据订阅过程也必然会面临失败的情况。 2. 报刊发行后,可能由于其内容过于吸引人,可能会被校级或院级教务员扣留,待其阅读后再分发给笔者;那么RxJS的数据发行,也必然可能出现被中间的转发者暂缓扣留而未得到及时转发的可能。 3. 报刊发行后,由于校级教务员的疏忽,可能导致忘记分发给笔者了;那么RxJS也必然存在在发行过程中,数据中断的情况。 4. 基于3,当教务员收到下一期的报刊时,突然发现上一期的还在这呢,此时一并发给笔者;那么RxJS也必然存在将两份数据员合并后发送给读者的情况。 5. 笔者订阅的两份报刊总是先后隔一天送达,教务员感觉两天送两次太难受,所以每次都是等两份报刊到齐了以后再统一发送给笔者;那么RxJS也必然存在订阅了两个数据源后,只有当满足两个数据源全部返回后才下发数据的情况。 6. 报刊发行后,教务员提前浏览的时候,不小心将报刊损坏或不经意的在报刊的内容上进行了批注,然后将损坏或批注的报刊给了笔者;那么RxJS中在进行数据传递的过程中,必然会出现数据被修改后才下发的情况。 7. 报刊发行后,教务员对自己感兴趣的报刊进行阅读后再下发给笔者,而对不感兴趣的报刊选择直接下发给笔者;那么RxJS中必然可以设置某些条件,当满足条件时对数据进行处理,而当不满足条件时直接下发数据。 没错,只要在现实生活中能想到的报刊订阅的情况RxJS均可满足!RxJS中提供了丰富的**操作符**来操作返回的数据流,每个操作符都有着其特殊的功能。此外如果RxJS中提供的操作符不能满足一些对数据流处理的要求,我们还可以自定义自己的操作符。 # 在响应header中获取auth-token 拦截器的本质正是生活中的"教务员",它接收其它订阅者的订阅,并将此后的数据按订阅的情况分发给对应的订阅者,对于"院教务员"而言,报刊订阅反馈到代码中如下: src/app/core/auth-token-interceptor.ts ```javascript intercept(req: HttpRequest<any> ➊, next: HttpHandler ➋): Observable<HttpEvent<any>> ➌ { return➎ next.handle(reqClone) ➍ } ``` * ➊ 向其发起订阅的笔者 * ➋ 用以发起订阅的校教务员 * ➌ 订阅的报刊种类 * ➍ 向校教务员发起订阅 * ➎ 将校教务员送达的报刊发送给笔者 # pipe() 在RxJS中,可以使用pipe()方法处理一些分发的数据。pipe方法接收一个或多个参数,每个参数都是一个**操作符**,被分发的数据在转发以前,依次通过pipe中规定的几个**操作符**。比如: ```javascript return next.handle(reqClone).pipe(操作符1(传入的数据0) => {处理后的数据1}, 操作符2(传入的数据1) => {处理后的数据2}) ``` ![](https://img.kancloud.cn/e9/bf/e9bfd21cd06593527c8a110d8d72bfca_558x90.png) ## demo 订阅、发送报刊与订阅、发送数字是一个道理,在此简单展示下RxJS是如何发送由数字组成的数据流以及如何使用pipe操作即将下发的数据流的。 情景:每1秒种发送一个0-100的随机数字,如果生成的是数字尾数是1则终止发送。有三个操作符来影响它,第一个操作符:将得到的数字进行平方,再将平方以后的数字向后发送;第二个操作符:对得到的数据与10进行模运算,将取模后的值向后发送;第三个操作符:如果得到的数据是奇数,则向后发送,否则不发送。则示例代码如下: src/app/core/auth-token-interceptor.ts ```javascript import {Observable, Subject} from 'rxjs'; import {filter, map} from 'rxjs/operators'; ... intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const sendNumbers = new Subject<number>(); ➊ const pipeNumber = sendNumbers.pipe➋(map➌((x) => { console.log('1接收到的数字为 ' + x); return x * x; }), map(x => { console.log('2接收到的数字为 ' + x); return x % 10; }), filter➍(x => { console.log('3接收到的数字为 ' + x); return x % 2 === 0; ➎ })); pipeNumber.subscribe((value) => { console.log('接收到了 ' + value); }); const sendNumber = () => setTimeout(() => { ➏ const n = Math.floor(Math.random() * 100); console.log('生成的数字是' + n.toString()); sendNumbers.next(n); ➐ if (n % 10 !== 1) { sendNumber(); } }, 1000); sendNumber(); ➑ const reqClone = req.clone({ setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'} }); return next.handle(reqClone); } ``` * ➊ 定义了一个可以发送数字的数据源 * ➋ 使用pipe对接收到的数据进行**操作** * ➌ map是一个**操作符**,它的作用时:接收数据、处理数字、转发处理后的数据 * ➍ filter也是一个**操作符**,它的作用时:当接收到的值满足其中规定的条件时,则转发数据。否则不转发。 * ➎ 此条件为true时➍转发数据,为false时不转发数据。 * ➏ 每1秒钟发送1个数据 * ➐ 发送数据 运行结果如下: ``` auth-token-interceptor.ts:28 生成的数字是42 auth-token-interceptor.ts:12 1接收到的数字为 42 auth-token-interceptor.ts:15 2接收到的数字为 1764 auth-token-interceptor.ts:18 3接收到的数字为 4 auth-token-interceptor.ts:23 接收到了 4 auth-token-interceptor.ts:28 生成的数字是65 auth-token-interceptor.ts:12 1接收到的数字为 65 auth-token-interceptor.ts:15 2接收到的数字为 4225 auth-token-interceptor.ts:18 3接收到的数字为 5 auth-token-interceptor.ts:28 生成的数字是74 auth-token-interceptor.ts:12 1接收到的数字为 74 auth-token-interceptor.ts:15 2接收到的数字为 5476 auth-token-interceptor.ts:18 3接收到的数字为 6 auth-token-interceptor.ts:23 接收到了 6 auth-token-interceptor.ts:28 生成的数字是95 auth-token-interceptor.ts:12 1接收到的数字为 95 auth-token-interceptor.ts:15 2接收到的数字为 9025 auth-token-interceptor.ts:18 3接收到的数字为 5 auth-token-interceptor.ts:28 生成的数字是91 auth-token-interceptor.ts:12 1接收到的数字为 91 auth-token-interceptor.ts:15 2接收到的数字为 8281 auth-token-interceptor.ts:18 3接收到的数字为 1 ``` # 获取auth-token 终于到了使用pipe来获取token的时候了: src/app/core/auth-token-interceptor.ts ```javascript intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const reqClone = req.clone({ setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'} }); return next.handle(reqClone).pipe➊(map➋((httpEvent) => { console.log(httpEvent); ➌ return httpEvent; })); } ``` * ➊ 使用pipe设置操作数据的操作符 * ➋ map操作符的作用的是:将return的结果进行转发 * ➌ 打印个日志看看 控制台显示该变量的值类型为HttpResponse,对应的基本信息如下: ``` { "headers": { "normalizedNames": {}, "lazyUpdate": null }, "status": 200, "statusText": "OK", "url": "http://localhost:8080/Teacher/", "ok": true, "type": 4, "body": [ { "id": 1, "name": "panjie", "sex": false, "username": "yqac", "email": "3792535@qq.com", "createTime": 0, "updateTime": 0 } ] } ``` 近一步进行类型转化来获取auth-token的值。 src/app/core/auth-token-interceptor.ts ```javascript intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const reqClone = req.clone({ setHeaders: {'auth-token': 'af1c0c77-67d0-4ec2-8321-2f88e32f76af'} }); return next.handle(reqClone).pipe(map((httpEvent) => { if (httpEvent instanceof HttpResponse) { ➊ const httpResponse = httpEvent as HttpResponse<any>; ➋ const authToken = httpResponse.headers.get('auth-token'); console.log('获取到的authToken为' + authToken); } return httpEvent; })); } ``` * ➊ 使用if进行类型判断,以防止异常(HttpEvent不止HttpResponse一种类型) * ➋ 类型转换 * ➌ 获取authToken 测试: ![](https://img.kancloud.cn/ec/aa/ecaa06331f1e34cd57c4e0109eb0423c_313x71.png) 很失望,竟然获取到了null,查看网络确认是否真的返回了auth-token呢? ![](https://img.kancloud.cn/c0/21/c02115ded23e1568ffb0809c9d9070f9_468x312.png) 诡异的事情发生了,在控制台中明明返回了auth-token,但为什么获取不到呢?莫非`httpResponse.headers.get('auth-token');`这个写法不正确吗?答案并不是这样的。`httpResponse.headers.get('auth-token');`这个写法完全正确,之所以在此时获取不到`auth-token`是由浏览器的安全策略决定的。为了某些安全方面的问题,浏览器仅允许js获`Cache-Control`,`Content-Language`,`Content-Type`,`Expires`,`Last-Modified`以及`Pragma`几个header信息。而若要获取其它的header的信息,则需要由在header中返回`Access-Control-Expose-Headers 访问控制允许暴露的headers`来指定。但当前的后台并未指定允许暴露的任何header信息,所以此时获取`auth-token`是不被浏览器所允许的。 解决的方法是打开后台,设置`Access-Control-Expose-Headers`并将`auth-token`加入其中。 config/WebConfig.java ```java public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:4200") .allowedMethods("PUT", "DELETE", "POST", "GET", "PATCH") .exposedHeaders("auth-token"); ➊ } ``` * ➊ 设置允许前台获取的额外header信息 重新启动后台后再次测试: ![](https://img.kancloud.cn/c1/c0/c1c0800f5fe22144e67d105dcfa7f183_529x110.png) # 缓存auth-token 行百里者,半九十。最后建立缓存服务把获取到的token信息缓存起来,以便在下起发起请求的时候,加入此auth-token信息。这样便能够达到在非首次请求中使用首次请求获取到的auth-token的目的。 ``` panjiedeMac-Pro:web-app panjie$ cd src/app/service/ panjiedeMac-Pro:service panjie$ ng g s cache CREATE src/app/service/cache.service.spec.ts (328 bytes) CREATE src/app/service/cache.service.ts (134 bytes) ``` 初始化如下: src/app/service/cache.service.ts ```javascript import {Injectable} from '@angular/core'; /** * 缓存 */ @Injectable({ providedIn: 'root' }) export class CacheService { /** 认证令牌 */ private static authToken: string = undefined; constructor() { } static setAuthToken(token: string) { CacheService.authToken = token; } static getAuthToken() { if (CacheService.authToken === undefined) { return ''; } return CacheService.authToken; } } ``` 在拦截器中使用缓存 src/app/core/auth-token-interceptor.ts ```javascript intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const reqClone = req.clone({ setHeaders: {'auth-token': CacheService.getAuthToken()➊} }); return next.handle(reqClone).pipe(map((httpEvent) => { if (httpEvent instanceof HttpResponse) { const httpResponse = httpEvent as HttpResponse<any>; const authToken = httpResponse.headers.get('auth-token'); CacheService.setAuthToken(authToken)➋; } return httpEvent; })); } ``` 先后点击次教师管理进行测试: ![](https://img.kancloud.cn/fc/df/fcdfc9685189147b27eea0824cf1952d_765x561.gif) 如上图所示: 首次访问时传入了空的auth-token,接收到了后台分发的auth-token: `70fb8229-7cf9-4326-b5a6-dbe911bf8e51`。进行第二次访问时,前台自动携带了该auth-token向后台发起了请求,同时后台在响应将发起的请示的token原值返回。这符合我们设定的令牌认证逻辑: ![](https://img.kancloud.cn/2a/cd/2acdc94ee4440132f4a062dbf7232b9b_809x471.png) # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.5) | - | | Access-Control-Expose-Headers | [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) | - | |RxJS 库 | [https://angular.cn/guide/rx-library](https://angular.cn/guide/rx-library) | 15 |