token的问题解决后本节展示如何将新令牌添加到请求的header中。
上节中学习过了使用`request.getHeader(String key);`来获取header中某个key的值。通常来说则还会有个类似的`request.setHeader(String key, String value);`的方法用于设置header中的某个key的值。
很遗憾,出于某些方面的考虑spring中(确认说是servlet)中并没有提供设置header的方法。
![](https://img.kancloud.cn/4e/85/4e85bad48aad708677444b4b6a22b5f9_653x172.png)
request只提供了上述两个set方法,也就是说我们没有办法通过request来直接设置其`header`信息。既然无法设置其值,那么是否可以在获取`header`的值的方法上下功夫呢?获取某个`header`时则必然调用`getHeader`方法;其它人获取令牌时,则必然调用`getHeader("auth-token")`。在编程的世界中,有一种叫做装饰器模式的方法恰好能够做到变更`getHeader("auth-token")`的返回值的目的。被改变后的`getHeader("auth-token")`可以指定的返回我们为其指定的`auth-token`信息,从而间接的**达到**设置header信息的目的。
# 装饰器模式
alphago前几年风靡全球,但你一定想不到的是:早上18世纪晚期的1770年,土耳其便已经拥有了一台自动下棋(应该是国际象棋)装置,比2016年3月alphago成名足足早了240年!该下棋装置在当时击败了大多数挑战者,这其中还包括了拿破仑。此装置不止能够计算出下一个落棋点,而且还可以自动拿着棋子下棋,甚至于还会与人交流说"将军"。
![](https://img.kancloud.cn/3d/95/3d9562f35c3aef4018c46dd47d643b26_668x572.png)
在那个只有机器装置的年代,完成这项工作简直不可思议。那么当时人们是怎么做到的呢?
![](https://img.kancloud.cn/84/d7/84d776b3d270eaad005f26ec7e913d31_347x313.png)
重点就在下棋装置里面的"人",实际上在下棋装置中偷偷藏了一个下棋的高手。而下棋装置走的每一步棋,都是由该下棋高手在内容操作的(当然这种操作本身使用机器装置传递到机器臂也很复杂)。
但笔者想说的是:这种在下棋装置中将下棋高手包装(装饰)起来,以使得下棋傀儡拥有下棋高手的下棋能力的模式,被称为"装饰器模式"。
## DEMO
下面以一个小的demo来讲下如何使用"装饰器模式"达到设置header中的auth-token的目的。
```java
class Request {
String getHeader(String key) {
// 获取header值的真实代码略
}
}
class TokenFilterTest {
/**在此传入auth-token的值*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
this.getAuthUser(request);
}
/** 在此获取auth-token的值 */
void getAuthUser(Request request) {
// 获取auth-token
request.getHeader("auth-token");
// 根据auth-token获取当前登录用户
}
}
```
上述代码首先执行doFilter方法,然后再执行`getAuthUser`方法,在`getAuthUser`方法中调用了`request`的`getHeader`方法近而获取到`auth-token`的值。
接下来使用装饰器模式来简单处理一下:
```
class Request {
...
}
class TokenFilterTest {
...
}
/**
* Request傀儡
*/
class RequestWrapper extends Request➊ {
Request request; ➋
private RequestWrapper() { ➎
}
public RequestWrapper(Request request) { ➎
this.request = request;
}
@Override
String getHeader(String key) { ➌
return this.request.getHeader(key); ➍
}
}
```
* ➊ 继承了Request,说明Request有的方法本傀儡都有。指定需要Request的地方,本傀儡都能代替。
* ➋ 本傀儡之所有这么有底气,是由于本傀儡内部拥有一个真正的Request(下棋高手)
* ➌ 本傀儡也会下棋
* ➍ 实际上下的每一步棋都是真正的Request来完成的
* ➎ 若想启动本傀儡,**必须**给我装入一个下棋高手
有了这个傀儡以后,doFilter方法便可以改成这样:
```
/**在此传入auth-token的值*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request); ➊
this.getAuthUser(request); ✘ ➋
this.getAuthUser(requestWrapper); ➋
}
```
* ➊ 将下棋高手装入
* ➋ 由于下棋傀儡requestWrapper拥有下棋高手request的全部功能,所以此时将下棋傀儡requestWrapper传入getAuthUser也是完全符合要求的。
此时程序执行的时序图如下:
![](https://img.kancloud.cn/00/20/00202f24bcc24e7c4cec627564ce8332_627x221.png)
## 重写装饰器中的方法
此时只要稍稍的对装饰器的getHeader方法进行改行,便能达到:如果传入的key为`auth-token`便返回新的令牌的目的:
```java
/**
* Request傀儡
*/
class RequestWrapper extends Request {
...
@Override
String getHeader(String key) {
if ("auth-token".equals(key)) {
// 在此返回新的auth-token值
return "456789";
}
return this.request.getHeader(key);
}
}
```
对应实序图如下:
![](https://img.kancloud.cn/f2/5e/f25e23e9f1c809c4365e15e8ab126c90_613x361.png)
## 在外部向装饰器中设置token
有了装饰器,也能够在装饰器中按特定的思想传值了,但这仍然满足不了初始的要求,即:
```java
class TokenFilterTest {
/**在此传入auth-token的值 ➊*/
@Test
void doFilter() {
String authToken = "654321";
Request request = new Request();
this.getAuthUser(request);
}
/** 在此获取auth-token的值 ➋*/
void getAuthUser(Request request) {
// 获取auth-token
request.getHeader("auth-token");
// 根据auth-token获取当前登录用户
}
}
```
将方法➊中设置的authToken的值通过request➋带入getAuthUser中。刚刚改写getHeader的方法却是传回了固定的"456789",而非"654321"。继续如下改造RequestWrapper
```java
class RequestWrapper extends Request {
Request request;
String token; ➊
private RequestWrapper() {
}
private RequestWrapper(Request request) { ➋
this.request = request;
}
public RequestWrapper(Request request, String token) {
this(request); ➋
this.token = token; ➊
}
@Override
String getHeader(String key) {
if ("auth-token".equals(key)) {
return this.token; ➊
}
return this.request.getHeader(key);
}
...
```
* ➊ 增加token,并做为auth-token返回
* ➋ 将原构造函数设置为私有,并在内部进行调用
此时doFilter随之改造为:
```
@Test
public void doFilter() {
String authToken = "654321";
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request, authToken);
this.getAuthUser(requestWrapper);
}
```
加入测试信息后的最终代码如下:
filter/TokenFilterTest.java
```java
public class TokenFilterTest {
@Test
public void doFilter() {
String authToken = new RandomString(6).nextString();
System.out.println("authToken传入值为" + authToken);
Request request = new Request();
RequestWrapper requestWrapper = new RequestWrapper(request, authToken);
this.getAuthUser(requestWrapper);
}
/** 在此获取auth-token的值 */
void getAuthUser(Request request) {
// 获取auth-token
System.out.println("获取到的auth-token值为:" + request.getHeader("auth-token"));
// 根据auth-token获取当前登录用户
}
}
class Request {
private String authToken = "123456";
String getHeader(String key) {
System.out.println("调用了Request中的getHeader方法");
if ("auth-token".equals(key)) {
return this.authToken;
}
// 其它的IF条件
return null;
}
}
/**
* Request傀儡
*/
class RequestWrapper extends Request {
Request request;
String token;
private RequestWrapper() {
}
private RequestWrapper(Request request) {
this.request = request;
}
public RequestWrapper(Request request, String token) {
this(request);
this.token = token;
}
@Override
String getHeader(String key) {
System.out.println("调用了RequestWrapper中的getHeader方法");
if ("auth-token".equals(key)) {
return this.token;
}
return this.request.getHeader(key);
}
}
```
执行结果如下:
```
authToken传入值为X0QR0u
调用了RequestWrapper中的getHeader方法
获取到的auth-token值为:X0QR0u
```
结果符合预期。达到了在不改变request中的header值的前提下,将token在A方法中传入,然后在B方法中获取的目的。
# HttpServletRequestWrapper
spring已经提供了用于装饰request的类 ---- HttpServletRequestWrapper。
![](https://img.kancloud.cn/d8/5e/d85ee21915a15b076bb0da0997bbdfb8_299x164.png)
此类提供了构造函数:`HttpServletRequestWrapper(request)`方法,但未提供所需要的`HttpServletRequestWrapper(request, token)`构造函数。为此,在原有HttpServletRequestWrapper的基础上,做一下**继承**并将其命名为HttpServletRequestTokenWrapper:
![](https://img.kancloud.cn/59/6e/596ed0045d77082e4696ea1fc18900dc_322x390.png)
代码如下:
```java
@WebFilter
public class TokenFilter extends HttpFilter {
...
/**
* 带有请求token的Http请求
*/
class HttpServletRequestTokenWrapper extends HttpServletRequestWrapper { ➊
HttpServletRequestWrapper httpServletRequestWrapper;
String token;
private HttpServletRequestTokenWrapper(HttpServletRequest request) { ➋
super(request);
}
public HttpServletRequestTokenWrapper(HttpServletRequest request, String token) { ➌
this(request);
this.token = token;
}
@Override
public String getHeader(String name) { ➍
if (TOKEN_KEY.equals(name)) {
return this.token;
}
return super.getHeader(name);
}
}
}
```
* ➊ 该类定义于TokenFilter内部
* ➋ 实现父类的构造函数,为防止其被外部使用从而声明为私有
* ➌ 新增构造函数,接收request及token
* ➍ 重写getHeader方法
使用该HttpServletRequestTokenWrapper在header中设置auth-token的代码如下:
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 有效性判断
if (!this.validateToken(token)) {
// 如果无效则分发送的token
token = this.makeToken();
logger.info("原token无效,发布的新的token为" + token);
// 设置header中的auth-token
request = new HttpServletRequestTokenWrapper(request, token); ➊
}
```
* ➊ 此时的request已经是那个被重写过getHeader方法的HttpServletRequestTokenWrapper了。
# 设置响应的令牌
在响应令牌的header中设置令牌非常简单:只需要调用response中的setHeader方法即可。
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 转发数据。spring开始调用控制器中的特定方法
chain.doFilter(request, response);
logger.info("在控制器被调用以后执行");
// 为http响应加入新token后返回
response.setHeader(TOKEN_KEY, token);
```
至此,使用过滤器在请求的header信息中传递令牌的过程便完整的实现了。虽然此方法没有考虑到生产环境下令牌过期以及令牌更新的问题,但在当前系统中已经完全够用了。
>[info] 在生产环境中会采用更加优秀、集成度更高的spring security来替代令牌交互及后面章节中将到学习到的用户认证过程。
# 测试
一切都不能想当然,一切都不能"我认为"。保证代码质量最好的方法便是使用单元测试,如果单元测试不适用则么最少也要看看日志及返回值。再次新建http request测试如下:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8 ✓
Transfer-Encoding: chunked ✓
Date: Tue, 11 Feb 2020 05:22:36 GMT ✓
[]
Response code: 200; Time: 121ms; Content length: 2 bytes
```
* ✓ 很遗憾:在响应的header并未获取到令牌auth-token
笔者猜想原因如下
1. 之所以没有在request上提供setHeader方法,官方是这么考虑的:一旦请求信息确立但不能够被再次修改以保证数据在传输过程中不失真。也就是说过滤器的设计之初并不是用来变更请求信息的(比如进行访问日志的记录)。
2. `response.setHeader(TOKEN_KEY, token);`未生效的原因也是如此:一旦响应信息确立则不能够对其进行修改,所以即使调用了setHeader也不会生效。
3. 确认request信息取决于前台,前台请求时带有什么值request就有什么值。所以在request不提供setHeader方法。而response信息取决于后台,在没有为response定稿前,是需要setHeader方法来设置header信息的。所以response中提供了setHeader方法供调用。
基于此,若想改变response响应中的header信息,那么应该在响应信息被确立以前。所以代码最终应该被修正为:
filter/TokenFilter.java
```java
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
...
// 在确立响应信息前,设置响应的header值 ✚
response.setHeader(TOKEN_KEY, token); ✚
// 转发数据。spring开始调用控制器中的特定方法
chain.doFilter(request, response);
logger.info("在控制器被调用以后执行");
// 为http响应加入新token后返回 ✘
response.setHeader(TOKEN_KEY, token); ✘
```
重新启动应用后再次测试:
<hr>
请求时未传入token:
```
GET http://localhost:8080/Teacher
```
测试结果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: e2047302-2c07-474a-9066-6404bc3b4534 ➊
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:34:44 GMT
[]
Response code: 200; Time: 122ms; Content length: 2 bytes
```
* ➊ 未传入token时,系统分发新token
<hr>
请求时传入有效token:
```
GET http://localhost:8080/Teacher
auth-token: d31e0d8a-470d-413a-beda-23885f4551f2 ➊
```
测试结果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: d31e0d8a-470d-413a-beda-23885f4551f2 ➊
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:36:46 GMT
[]
Response code: 200; Time: 12ms; Content length: 2 bytes
```
* ➊ 传入有效token时,系统回传有效token
<hr>
请求时传入无效token
```
GET http://localhost:8080/Teacher
auth-token: d31e0d8a-470d-413a-beda ➊
```
测试结果:
```
GET http://localhost:8080/Teacher
HTTP/1.1 200
auth-token: e71dd873-0f0b-428b-b202-aa1b359fe3ff ➊
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 11 Feb 2020 05:38:04 GMT
[]
Response code: 200; Time: 30ms; Content length: 2 bytes
```
* ➊ 传入无效token时,系统分发新的有效token
# 参考文档
| 名称 | 链接 | 预计学习时长(分) |
| --- | --- | --- |
| 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.2.3) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用