这里不对CSRF攻击方式及原理描述
不做详细介绍。各种技术博客已经讲的很详细了,如果不了解的话,可以自行搜索查看。
默认你对
guard,interceptor等已经了解
在这之前先了解CSRF的基本知识
CSRF,全称是Cross-Site Request Forgery(跨站请求伪造),也是一种常见的网络攻击方式。它利用用户已经验证的身份,在用户不知情的情况下,以用户的名义完成非法操作。这可能包括提交表单、在线支付、更改用户设置等操作,而受害者对此毫无察觉。
CSRF攻击通常利用了网站对用户身份验证机制的信任。如果网站仅仅通过验证用户的cookie来确认用户的请求,那么攻击者就可以构造外部请求,诱导已登录的用户点击链接或通过其他方式触发请求,从而在用户不知情的情况下,以用户的身份执行攻击者预设的操作。
防御
对于CSRF攻击,则常用的方法包括
-
验证Referer头这个可以在中间件进行处理,也可以在Nginx层就处理。下面不做处理讲解。 -
使用
SameSite Cookie属性等SameSite属性是一个用于防止跨站点请求伪造(CSRF)攻击的Cookie属性。它允许服务器指定Cookie是否应该与跨站点请求一起发送。这个属性可以有三个值:
Strict、Lax和None。- Strict: 最为严格的设置。在这种模式下,Cookie只会在请求来自于同一个站点时被发送。这意味着,即使用户是通过点击第三方网站上的链接访问的,Cookie也不会被发送。 使用场景:适用于一些非常重视安全性,而且不需要跨站点请求携带用户状态的应用。
- Lax: 相对宽松的设置。在这种模式下,Cookie会在一些跨站点的GET请求中被发送,例如用户通过第三方网站上的链接访问。但在POST请求或者通过脚本发起的请求(如XHR或Fetch)中,Cookie不会被发送。 使用场景:这是大多数网站的推荐设置,因为它在保护用户免受CSRF攻击的同时,还允许了一些用户友好的跨站点请求,比如从其他网站的链接直接访问。
- None: 在这种模式下,Cookie会在所有的请求中被发送,无论是同站点还是跨站点请求。要使用SameSite=None,还必须将Secure属性设置为true,这意味着Cookie只能通过安全的HTTPS连接发送。 使用场景:适用于需要在多个站点间共享Cookie的应用,例如跨域身份验证。
-
使用
CSRF令牌;CSRF的验证有几种方法,这里使用对比验证即前端Request中Headers字段与Cookies字段进行比较验证。
前端从后端获取到token,在发送请求时携带在headers中。后端在下发token时,同时在cookie中增加token。
Code it
在NestJS中,有一些配置是开箱即用,引用对应的插件会默认处理,如果你有自定义的需要,你可以在插件处配置一些自定义配置
我的项目中使用 @fastify/cookie,@fastify/csrf-protection
总共分为几部:
注册以及配置组件
在main.ts中注册并配置组件
import fastifyCookie from '@fastify/cookie';
import fastifyCsrf from '@fastify/csrf-protection';
// 增加一些自定义项
await app.register(fastifyCookie, {
secret: process.env.CSRF_SECRET,
});
// // 注册 @fastify/csrf-protection 插件
await app.register(fastifyCsrf, {
cookieKey: process.env.CSRF_KEY,
cookieOpts: {
path: '/',
maxAge: Number(process.env.CSRF_EXPIRE), // 配置生效时间,建议与JWT时间一致
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // 在开发环境下不使用https
sameSite: 'lax', // sameSite: 'none' | 'lax' | 'none' | 'strict' | boolean;
},
});
上面有一些环境变量
CSRF_SECRET = 12345678901234567890123456789012
CSRF_KEY = csrf-token # 这个是cookieName
CSRF_EXPIRE = 172800 # 2days,但是单位是s
可配置字段
这是 @fastify/csrf-protection的可配置字段,具体可前往github仓库查阅文档
interface FastifyCsrfProtectionOptionsBase {
cookieKey?: string;
cookieOpts?: CookieSerializeOptions;
sessionKey?: string;
getUserInfo?: (req: FastifyRequest) => string;
getToken?: GetTokenFn;
}
在登录时生成CSRF-TOKEN
因为csrf是借助已登录的凭据,则我们在登录时同时生成并下发token。
注意:为了保证CSRF生成不会和登录逻辑造成耦合,我将CSRF的生成独立成一个新的Response拦截器。
写一个csrf的response拦截器
使用isLogin方法判断是否是登录接口。这个接口就是判断当前request.url。
import { isLogin } from '@/utils';
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FastifyReply } from 'fastify';
@Injectable()
export class CsrfInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const response = ctx.getResponse<FastifyReply>();
const CSRF_DATA = {};
const isLoginUrl = isLogin(ctx.getRequest().url);
if (isLoginUrl) {
CSRF_DATA[process.env.CSRF_KEY] = response.generateCsrf();
}
return next.handle().pipe(map((data) => ({ ...data, ...CSRF_DATA })));
}
}
全局注册拦截器
import { CsrfInterceptor } from './interceptors/csrf.interceptor';
@Module({
providers: [
// CSRF拦截
{
provide: APP_INTERCEPTOR,
useClass: CsrfInterceptor,
},
]
})
定义CSRF守卫
守卫的作用,就是在收到Request后,对csrf进行验证。
我们或许并不需要对所有接口进行CSRF的防范,所以在特定接口使用守卫就可以。 如果你需要全局守卫,你在app.module.ts中,全局注册守卫也是可以的
需要注意的是,这里是Fastify的实现方式,Express是不一样的
调用csrfProtection进行验证。
这个方法验证 cookie中的csrf 与headers中csrf的一致性。
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class CsrfGuard implements CanActivate {
constructor(private readonly adapterHost: HttpAdapterHost) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const http = context.switchToHttp();
const request = http.getRequest<FastifyRequest>(),
response = http.getResponse<FastifyReply>();
const instance = this.adapterHost.httpAdapter.getInstance();
instance.csrfProtection(request, response, () => {}); // 因为csrf的Protection的next是必须要存在,且一定是方法。所以给一个匿名空方法即可。
return true;
} catch (error) {
throw error;
}
}
}
注意,csrfProtection的默认方法
补充说明:csrfProcetion获取请求体中csrf-token的默认方法。
所以如果你未在@fastify/csrf-protection配置项中重新定义getToken方法你的前端需要按照如下方式将csrf传入。
function getTokenDefault (req) {
return (req.body && req.body._csrf) ||
req.headers['csrf-token'] ||
req.headers['xsrf-token'] ||
req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token']
}
使用守卫
我们在特定的接口(controller)或者全局增加守卫
- 示意接口使用
import { CsrfGuard } from '@/guards/csrf.guard';
@Controller('user')
export class UserController {
constructor() {}
@UseGuards(CsrfGuard)
getInfo(userId:string) {
return this.userService.getInfo(userId);
}
}
- 示意全局使用
import { CsrfGuard } from '@/guards/csrf.guard';
@Module({
providers: [
// CSRF守卫
{
provide: APP_GUARD,
useClass: CsrfGuard,
},
]
})