V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rikka
V2EX  ›  Node.js

nestjs 如何优雅地给 response 设置 header?

  •  
  •   rikka · 2020-11-15 19:28:10 +08:00 · 7153 次点击
    这是一个创建于 1472 天前的主题,其中的信息可能已经有所发展或是发生改变。

    很常见的需求:一个登录请求过来后验证通过后要给 response 的 header 设置 token

    我找到了 2 种方法但都不满意不优雅

    方法一

    按照文档来

    @Post('login')
    async login (@Body() param,@Res res) {
     const data={}
      res.set('token','')//这里设置 response 没问题
      //但是啊下面的 return 就无效了!!
      //你必须自己手动操作 res.json().send()去给客户端返回数据
      //还有副作用是拦截器不正常了
     return {data}
    }
    

    很难受,这种方法太怪胎了,还有副作用,弃之

    方法二

    用拦截器来帮忙设置 header

    Injectable()
    export class SetTokenInterceptor implements NestInterceptor {
      intercept (context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(tap(result => {
          if (result?.token) {
            const http = context.switchToHttp()
            const res = http.getResponse()
            res.set('token', result.token)
            delete result.token
            //这里要删掉 token,因为我不希望 token 返回到 respoonse 的 body 中
          }
        }))
      }
    }
    
    @Post('login')
    @UseInterceptors(SetTokenInterceptor)
    async login (@Body() param) {
     const data={}
     return {data,token:''}
     //问题在于怎么把 token 传给拦截器?
     //只能带在返回数据里,然后拦截器拿到 token 再删掉
    }
    

    这种方法也很难受,说不上具体,总之就是非常难受

    所以还有其他方法吗

    第 1 条附言  ·  2020-11-16 00:20:16 +08:00
    完美的解决方法在 10L,这问题苦恼了我一天😭️
    31 条回复    2024-01-24 10:36:10 +08:00
    zengming00
        1
    zengming00  
       2020-11-15 19:34:56 +08:00
    老老实实用 express/koa,这风格一看就是 java 抄过来的,为什么要用 java 那套东西来复杂化 node.js
    rikka
        2
    rikka  
    OP
       2020-11-15 19:37:31 +08:00
    @zengming00 #1 我在这里被众人安利 nestjs 😂
    gouflv
        3
    gouflv  
       2020-11-15 21:50:49 +08:00 via iPhone
    token 直接放 json 返回咯
    luob
        4
    luob  
       2020-11-15 22:21:19 +08:00
    在后端给 response header 设置 token 没有任何意义

    供参考:

    @Post('login')
    async login (@Body() param,@Res res) {
    const {username, password} = param
    const token =this.authService.validateUser(username, password)
    return { token }
    }

    @Get('/profile')
    @UserRequired()
    async user (@CurrentUser() user) {
    return this.userService.getUser(user.id)
    }

    实现一下 @UserRequired()和 @CurrentUser()这两个注解即可
    rikka
        5
    rikka  
    OP
       2020-11-15 22:32:30 +08:00
    @luob #4 怎么会没意义呢,我鉴权用 jwt 来做,就想 token 放在 response header 中给客户端去拿,极其常见的情况

    另外你第一段代码试过了吗,你一旦用 @Res 这个装饰器后面 return 就没用了
    luob
        6
    luob  
       2020-11-15 22:56:53 +08:00
    @rikka 那里是忘了删除,显然不需要 @Req

    大家都是
    客户端从 body 里拿到 token 保存到状态然后把 token 塞进每个请求里
    你为什么要
    客户端从 header 里挖 token 保存到状态然后把 token 塞进每个请求里


    你自己造了个 token/set-token 方案?你都这样玩了为什么不用 set-cookie 呢?
    rikka
        7
    rikka  
    OP
       2020-11-15 23:02:35 +08:00
    @luob #6

    我也没啥特别的啊
    仅仅仅就想 token 放 header 里面给客户端拿而已
    为什么呢?因为客户端请求的时候会把 token 放在 header,所以为了对称美(狗头),下发 token 也想放 header 里面
    rikka
        8
    rikka  
    OP
       2020-11-15 23:05:35 +08:00
    @luob #6 还有个好处忘了讲,比方 token 还剩 10 分种就过期了,客户端做其他业务请求的时候我想重新下发一个新 token 给他啊,放 header 里面就就非常合适
    rikka
        9
    rikka  
    OP
       2020-11-15 23:13:46 +08:00
    业务交互数据放 body
    鉴权这种相对特殊的数据放 header

    难道不是更常见?
    rikka
        10
    rikka  
    OP
       2020-11-15 23:43:33 +08:00
    @luob #6 卧槽,还在想怎么实现这个 @CurrentUser,然后就受到启发,我可以自己写个装饰器自己拿到 response 啊

    ```
    export const resp = createParamDecorator((data, ctx: ExecutionContext) => {
    return ctx.switchToHttp().getResponse()
    })

    @Post('login')
    async login (@Body() param,@resp() resp:any) {
    const data={}
    resp.set('token','')
    return {data}
    }
    ```

    非常非常完美!!!
    zy445566
        11
    zy445566  
       2020-11-16 10:21:32 +08:00
    10L 这样不也一样有副作用,要想最没副作用,那就这个方法就单独做参数处理器,return 出来后,用一个出口方法了 merge 到 resp 上,这样副作用才最小
    namelosw
        12
    namelosw  
       2020-11-16 11:19:12 +08:00 via iPhone
    Nest 的设计就不是无副作用的
    rikka
        13
    rikka  
    OP
       2020-11-16 14:11:48 +08:00
    @zy445566 #11 我说的副作用是指:只要用了 nestjs 提供的 @Res 装饰器后就不能直接 return 数据了,必须操作 res.json/send 去返回数据,然后这又导致拦截器不能正常工作

    你说副作用具体是什么,还是说是指函数式编程中的那种副作用啊
    zy445566
        14
    zy445566  
       2020-11-16 14:21:36 +08:00
    @rikka 嗯,我以为你说的是函数式编程的副作用
    gxm44
        15
    gxm44  
       2020-11-16 16:23:55 +08:00
    @zengming00 赞同
    Kasumi20
        16
    Kasumi20  
       2020-11-20 22:19:51 +08:00
    你用了 @Res 注解就会被忽略返回值啊, 这样避免重复操作流....
    res.send 不就好了? 非要 return ...
    rikka
        17
    rikka  
    OP
       2020-11-20 22:25:24 +08:00
    @Kasumi20 #16 我所有的控制器方法都是直接 return 数据,唯独这里不能 return,代码看起来我那个难受啊。。。
    hongweiliuruige
        18
    hongweiliuruige  
       2020-11-24 18:44:55 +08:00
    写个装饰器,装饰器可以拿到 req res,res 里设置下就行了,例如 @SetToken(),,然后装饰器里面写逻辑,token 从哪里来
    hongweiliuruige
        19
    hongweiliuruige  
       2020-11-24 18:59:52 +08:00
    ```javascript
    export const SetCookie = createParamDecorator(
    (data: unknown, ctx: ExecutionContext) => {
    const response: Response = ctx.switchToHttp().getResponse();
    return function(data){
    response.cookie('auth',data);
    }
    },
    );
    ```
    参考
    BoringTu
        20
    BoringTu  
       2020-11-25 11:46:06 +08:00
    @rikka 你这所谓“对称美”本来就是不应该的啊,我是有强迫症的人,我都不会这么考虑,因为不应该

    登录接口响应的 token 是作为响应数据存在的,理应放在 body 里
    需要鉴权的接口请求的 token 是作为鉴权依据存在的,理应放在 header 里
    rikka
        21
    rikka  
    OP
       2020-11-25 14:20:27 +08:00
    @BoringTu #20 你说的这个理我也认同

    但你考虑下这种情况:客户端做其他业务请求时,服务端发现 token 快过期了,要发个新的给客户端,新 token 放哪里?放 header 最恰当吧

    客户端怎么处理这种情况,当然是全局拦截器一发现 header 有 token 就保持起来,然后如果登录请求 token 放 body,那么客户端代码将有 2 处处理 token 的代码逻辑,如果登录 token 放 header,那么将简化为只有一处代码,不管是登录还是其他请求,只要 header 有 token 就保持起来,是不是更简明一点
    rikka
        22
    rikka  
    OP
       2020-11-25 14:21:59 +08:00
    @hongweiliuruige #19 这跟我 10L 说的差不多
    BoringTu
        23
    BoringTu  
       2020-11-25 14:52:09 +08:00
    @rikka 这里我的建议是,什么接口就做什么接口应该做的事情,处理 token 就要用专门处理 token 的接口,而不是在业务逻辑接口上插一个系统级的数据,这样会让人有种很脏的感觉

    比如你所说的场景,token 过期了,你发起一个业务逻辑的请求,服务器直接打回,响应了一个约定好的代表 token 过期的 code,前端接到这个响应并匹配了 code,就自动发起一个 refresh token 的请求
    然后有两种情况:
    1. 当新 token 正常响应了之后,前端自动发起之前的业务逻辑请求
    2. 如果新 token 获取失败,前端直接踢到登录页

    这样不是看起来很干净么?还是遵循那个原则:什么接口就做什么接口应该做的事情
    rikka
        24
    rikka  
    OP
       2020-11-25 15:12:25 +08:00
    @BoringTu #23 你这建议我觉得很 OK,接受,下个项目可以考虑这么干

    目前来讲我是 token 还没过期,但是快过期了,比方一个 token 过期时间 30 天,那么最后 10%的时间,也就是 3 天,最后 3 天有请求就在 header 发个新 token 达到自动续签的目的,这样服务端还是客户端都能在一个地方统一处理 token 问题,要说个优点吧我就觉得这样特别简单轻松
    galikeoy
        25
    galikeoy  
       2020-11-27 18:16:10 +08:00
    @BoringTu #23 这个方法也是大多数网站的玩法,挺好的。楼主的方法让用户体验更好了,只要他在过期前有用过,就永远都是登录的,这特么不就是 cookie 吗。。。
    BoringTu
        26
    BoringTu  
       2020-12-02 09:45:49 +08:00
    @galikeoy 对于用户体验来说都是一样的,都是无感刷新登录状态
    rikka
        27
    rikka  
    OP
       2020-12-02 12:12:33 +08:00
    @BoringTu #26 突然想到另外一个问题,并发的情况

    比如客户端同时发 2 个请求过来,服务端检查都快过期了,于是都重新生成 token 给回去了,虽然最终只会保存一个 token,也暂时没导致什么大问题,但这令我不爽了

    你的方法好像也有同样的问题:2 个请求同时过来,服务端打回,于是同时发 2 个 refresh token 的请求

    请教下怎么考虑这种情况的
    rikka
        28
    rikka  
    OP
       2020-12-02 12:17:29 +08:00
    哦,我想到了,服务端 token 生成这个操作,独立出来,队列化处理吧
    cereschen
        29
    cereschen  
       2020-12-16 19:27:44 +08:00
    我记得 req 对象上挂载了 res 对象 所以....
    ashe
        30
    ashe  
       2023-08-24 13:23:28 +08:00
    可以看看类型声明
    node_modules/@nestjs/common/decorators/http/route-params.decorator.d.ts

    ```typescript
    /**
    * The `@Response()`/`@Res` parameter decorator options.
    */
    export interface ResponseDecoratorOptions {
    /**
    * Determines whether the response will be sent manually within the route handler,
    * with the use of native response handling methods exposed by the platform-specific response object,
    * or if it should passthrough Nest response processing pipeline.
    *
    * @default false
    */
    passthrough: boolean;
    }
    ```

    简单来说你的场景可以这样玩

    ```typescript
    @Post("/world")
    getHello(@Res({ passthrough: true }) response: Response) {
    response.setHeader("x-hello", "world");
    return { msg: "Hello World!" };
    }
    ```

    拦截器也能生效

    ```typescript
    @Injectable()
    export class RewritePost201To200Interruptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler) {
    const host = context.switchToHttp();
    const request: Request = host.getRequest();
    const response: Response = host.getResponse();
    return next.handle().pipe(
    map((data: unknown) => {
    if (response.statusCode === HttpStatus.CREATED && request?.method.toUpperCase() === "POST") {
    response.status(HttpStatus.OK);
    }
    return data;
    }),
    );
    }
    }
    ```

    客户端也可以直接拿到返回
    dockerman
        31
    dockerman  
       307 天前
    @ashe 正解 👍
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3161 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 13:36 · PVG 21:36 · LAX 05:36 · JFK 08:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.