前言
在日常开发中,我们知道 http 是无状态的,也就是前后两次请求之间是独立的。那么我们怎么实现登录状态的保存和登录校验呢?
常用的登录校验方式有两种:session + cookie 和 jwt:
- 服务端存储的 session + cookie 方案
- 客户端存储的 jwt token 方案
这里我们使用 Nest 来实现下这两种方法。
session + cookie
首先,新建一个nest项目,然后安装依赖 express-session ,即 express 的中间件。
nest new nest-jwt-and-session-demo-240609 -p npm
pnpm i express-session @types/express-session
接着,在 main.ts 模块中引入它:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(session({
secret: 'hhh', // used to sign the session cookie.
resave: false, // Forces the session to be saved back to the session store, even if the session was never modified during the request.
saveUninitialized: false, // Forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.
}));
await app.listen(3000);
}
bootstrap();
根据 express-session 文档可知:
-
secret 是指定的服务端用于签名 session cookie 。secret 可以是字符串或者多个secret 组成的数组,当是数组时,只有第一个元素会用于签名 session ID cookie,不过所有元素都会用于请求的校验签名。
-
resave 默认值是 true。设置为 true 表示每次访问都会更新 session并存入 session store,不管请求中有没有修改 session 的内容。
-
saveUninitalized 默认值是 true。设置为 true 表示不管 session 是否初始化了,都会存入 session store 中。session 的初始化意思是生成后未修改过。这里我们设置为 false 就好。
接着,我们试着在 controller 里面注入 session 对象:
@Get('ses')
ses(@Session() session) {
console.log(session)
session.count = session.count ? session.count + 1 : 1;
return session.count;
}
然后 pnpm start:dev 运行项目,并访问 http://localhost:3000/ses
可以看到,第一次访问时,页面内容是1,第二次访问是2了。而两次的打印内容分别是:
Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true }
}
Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
count: 1
}
说明 cookie 成功应用了,服务器可以识别到是已有的用户,也就是说 http 请求已经有了状态。
我们知道,浏览器在收到服务端返回的Set-Cookie后,会保存下来,在下次再访问该网站时,HTTP请求头就会携带Cookie。保存的位置是 Application - Cookie。再看两次请求的请求头和响应头,
第一次请求的请求头不带 Cookie,响应头中带了 Set-Cookie,Set-Cookie 的值为:
Set-Cookie: connect.sid=s%3AmYgJCiQ4vXPvE9yxN_ry5U_PU7UVpFOd.MaGvC4DGwJrySoQjl23RSqlCtkP%2Fv23KS9QpAntCtgE; Path=/; HttpOnly
而第二次请求的请求头带了 Cookie,响应头中不带 Set-Cookie,Cookie的值为:
Cookie: connect.sid=s%3AmYgJCiQ4vXPvE9yxN_ry5U_PU7UVpFOd.MaGvC4DGwJrySoQjl23RSqlCtkP%2Fv23KS9QpAntCtgE
这与 Set-Cookie 中的数值是对应的。
jwt token
首先,安装依赖项 @nestjs/jwt:
pnpm i @nestjs/jwt
然后,在 AppModule 里引入 JwtModule:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
// JwtModule 是一个动态模块,通过 register 传入 options。
JwtModule.register({
secret: 'hhh',
signOptions: {
expiresIn: '3d'
}
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
这里设置了 secret 为 hhh,以及过期时间为 3天。
然后,在 controller 中注入 JwtService,并定义路由 jwt:
import { JwtService } from '@nestjs/jwt';
@Inject(JwtService)
private jwtService: JwtService;
@Get('jwt')
jwt(@Res({ passthrough: true}) response: Response) {
const newToken = this.jwtService.sign({
count: 1
});
response.setHeader('token', newToken);
return 'hello';
}
这里,我们使用了 jwtService.sign()生成了一个 jwt token,内容是 count: 1,并把 token 放到 response header 里,返回给浏览器。
此外,根据官方文档可知,当我们注入 response 对象之后,需要在 @Res() 装饰器中设置 passthrough 为 true,才可以正常返回内容。
测试一下:
当浏览器访问 http://localhost:3000/jwt 时,可以看到响应头中带上了 token 字段:
接下来的请求,我们带上这个 token,在服务端接收后,count +1 之后再放入新的 token 返回给浏览器:
@Get('jwt')
jwt(@Headers('authorization') authorization: string, @Res({ passthrough: true }) response: Response) {
if(authorization) {
try {
const token = authorization.split(' ')[1];
const data = this.jwtService.verify(token);
const newToken = this.jwtService.sign({
count: data.count + 1
});
response.setHeader('token', newToken);
return data.count + 1
} catch(e) {
console.log(e);
throw new UnauthorizedException();
}
} else {
// 生成一个 jwt token,放到 response header 里
const newToken = this.jwtService.sign({
count: 1
});
response.setHeader('token', newToken);
return 1;
}
}
在日常开发中,我们知道使用 token 是通过在请求头中加上 autorization 字段,内容为 Bearer xxx。因此,这里我们首先通过 @Headers 装饰器取出 autorization,然后通过 jwtService.verify() 进行验证。如果验证成功,就重新生成 jwt 并返回给浏览器;如果验证失败,则抛出 UnauthorizedException 异常,由 Nest 内置的 Exception Filter 来进行处理。
我们测试下代码:
第一次请求时,请求头不带 token,服务器会返回 token,返回值是 1;
第二次请求时,将服务端的 token 带上,返回值是 2;
如果是错误的 token 呢?服务器的jwtService.verify()校验不通过,提示 JsonWebTokenError: jwt malformed,服务端最终会返回401状态码给浏览器:
这样,我们就实现了 jwt 保存 http 状态的需求。
后记
总的来说,在日常开发中,我们实现登录状态的保存和登录校验的方式有两种:session + cookie 和 jwt:
- 服务端存储的 session + cookie 方案
- 客户端存储的 jwt token 方案
尤其是 jwt token 方案,在接触过的几个开发项目中都是使用了这种方案。
在 Nest 后端应用中,学会这两种登录校验方式是很重要的一环。