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) | - |