nestjs 如何优雅地给 response 设置 header? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rikka
V2EX    Node.js

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

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

    很常见的需求:一个登录请求过来后验证通过后要给 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 问题,要说个优点吧我就觉得这样特别简单轻松
    dfourc
        25
    dfourc  
       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  
       2024-01-24 10:36:10 +08:00
    @ashe 正解
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2443 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 36ms UTC 15:27 PVG 23:27 LAX 08:27 JFK 11:27
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86