保护 SSR 应用

保护服务端渲染应用

如果你正在使用 AdonisJS 创建服务端渲染应用,那么你必须使用 @adonisjs/shield 包来保护你的应用免受常见的 Web 攻击,例如 CSRFXSSContent sniffing(内容嗅探) 等。

该包已预先配置在 web 启动套件 中。但是,你也可以按如下方式手动安装和配置该包。

@adonisjs/shield 包对 @adonisjs/session 包有对等依赖(peer dependency),因此请务必先配置 session 包

node ace add @adonisjs/shield
  1. 使用检测到的包管理器安装 @adonisjs/shield 包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
    providers: [
    // ...other providers
    () => import('@adonisjs/shield/shield_provider'),
    ]
    }
  3. 创建 config/shield.ts 文件。

  4. start/kernel.ts 文件中注册以下中间件。

    router.use([() => import('@adonisjs/shield/shield_middleware')])

CSRF 保护

CSRF(跨站请求伪造,Cross-Site Request Forgery) 是一种攻击,恶意网站会诱骗你的 Web 应用用户在未明确同意的情况下执行表单提交。

为了防范 CSRF 攻击,你应该定义一个隐藏的输入字段来保存 CSRF 令牌值,该令牌只能由你的网站生成并校验。因此,由恶意网站触发的表单提交将会失败。

保护表单

一旦你配置了 @adonisjs/shield 包,所有没有 CSRF 令牌的表单提交都会自动失败。因此,你必须使用 csrfField Edge 辅助函数来定义一个带有 CSRF 令牌的隐藏输入字段。

Edge 辅助函数

<form method="POST" action="/">
{{ csrfField() }}
<input type="name" name="name" placeholder="Enter your name">
<button type="submit"> Submit </button>
</form>

输出 HTML

<form method="POST" action="/">
<input type="hidden" name="_csrf" value="Q9ghWSf0-3FD9eCiu5YxvKaxLEZ6F_K4DL8o"/>
<input type="name" name="name" placeholder="Enter your name"/>
<button type="submit">Submit</button>
</form>

在表单提交期间,shield_middleware 会自动校验 _csrf 令牌,仅允许带有有效 CSRF 令牌的表单提交。

处理异常

当 CSRF 令牌缺失或无效时,Shield 会抛出一个 E_BAD_CSRF_TOKEN 异常。默认情况下,AdonisJS 会捕获该异常,并将用户重定向回表单,并附带一条错误闪现(flash)消息。

你可以在 Edge 模板中按以下方式访问闪现消息。

@error('E_BAD_CSRF_TOKEN')
<p> {{ $message }} </p>
@end
<form method="POST" action="/">
{{ csrfField() }}
<input type="name" name="name" placeholder="Enter your name">
<button type="submit"> Submit </button>
</form>

你也可以像下面这样在全局异常处理程序中自行处理 E_BAD_CSRF_TOKEN 异常。

import app from '@adonisjs/core/services/app'
import { errors } from '@adonisjs/shield'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
async handle(error: unknown, ctx: HttpContext) {
if (error instanceof errors.E_BAD_CSRF_TOKEN) {
return ctx.response
.status(error.status)
.send('Page has expired')
}
return super.handle(error, ctx)
}
}

配置参考

CSRF 守卫的配置存储在 config/shield.ts 文件中。

import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
csrf: {
enabled: true,
exceptRoutes: [],
enableXsrfCookie: true,
methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
},
})
export default shieldConfig

enabled

开启或关闭 CSRF 守卫。

exceptRoutes

要从 CSRF 保护中豁免的一组路由模式。如果你的应用有通过 API 接受表单提交的路由,你可能希望将它们豁免。

对于更高级的用例,你可以注册一个函数来动态地豁免特定路由。

{
exceptRoutes: (ctx) => {
// exempt all routes starting with /api/
return ctx.request.url().includes('/api/')
}
}

enableXsrfCookie

启用后,Shield 会将 CSRF 令牌存储在一个名为 XSRF-TOKEN 的加密 cookie 中,前端 JavaScript 代码可以读取它。

这使得像 Axios 这样的前端请求库能够自动读取 XSRF-TOKEN,并在发出 Ajax 请求时将其设置为 X-XSRF-TOKEN 头,而无需服务端渲染的表单。

如果你没有以编程方式触发 Ajax 请求,则必须保持 enableXsrfCookie 为禁用状态。

methods

需要保护的 HTTP 方法数组。所有上述方法的传入请求都必须提供有效的 CSRF 令牌。

cookieOptions

XSRF-TOKEN cookie 的配置。可用选项请参阅 cookies 配置

定义 CSP 策略

CSP(内容安全策略,Content security policy) 通过为加载 JavaScript、CSS、字体、图片等定义受信任的来源,保护你的应用免受 XSS 攻击。

CSP 守卫默认是禁用的。但是,我们建议你启用它,并在 config/shield.ts 文件中配置策略指令。

import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
csp: {
enabled: true,
directives: {
// policy directives go here
},
reportOnly: false,
},
})
export default shieldConfig

enabled

开启或关闭 CSP 守卫。

directives

配置 CSP 指令。你可以在 https://content-security-policy.com/ 上查看可用指令的列表。

const shieldConfig = defineConfig({
csp: {
enabled: true,
directives: {
defaultSrc: [`'self'`],
scriptSrc: [`'self'`, 'https://cdnjs.cloudflare.com'],
fontSrc: [`'self'`, 'https://fonts.googleapis.com']
},
reportOnly: false,
},
})
export default shieldConfig

reportOnly

当启用 reportOnly 标志时,CSP 策略不会阻止资源,而是将违规报告发送到使用 reportUri 指令配置的端点。

const shieldConfig = defineConfig({
csp: {
enabled: true,
directives: {
defaultSrc: [`'self'`],
reportUri: ['/csp-report']
},
reportOnly: true,
},
})

此外,还要注册 csp-report 端点来收集违规报告。

router.post('/csp-report', async ({ request }) => {
const report = request.input('csp-report')
})

使用 Nonce

你可以通过在行内 scriptstyle 标签上定义 nonce 属性 来允许它们。nonce 属性的值可以在 Edge 模板中通过 cspNonce 属性访问。

<script nonce="{{ cspNonce }}">
// Inline JavaScript
</script>
<style nonce="{{ cspNonce }}">
/* Inline CSS */
</style>

此外,在 directives 配置中使用 @nonce 关键字以允许基于 nonce 的行内脚本和样式。

const shieldConfig = defineConfig({
csp: {
directives: {
defaultSrc: [`'self'`, '@nonce'],
},
},
})

从 Vite 开发服务器加载资源

如果你正在使用 Vite 集成,可以使用以下 CSP 关键字来允许由 Vite 开发服务器提供的资源。

  • @viteDevUrl 将 Vite 开发服务器 URL 添加到允许列表。
  • @viteHmrUrl 将 Vite HMR websocket 服务器 URL 添加到允许列表。
const shieldConfig = defineConfig({
csp: {
directives: {
defaultSrc: [`'self'`, '@viteDevUrl'],
connectSrc: ['@viteHmrUrl']
},
},
})

如果你将 Vite 打包后的输出部署到 CDN 服务器,必须将 @viteDevUrl 替换为 @viteUrl 关键字,以允许来自开发服务器和 CDN 服务器的资源。

directives: {
defaultSrc: [`'self'`, '@viteDevUrl'],
defaultSrc: [`'self'`, '@viteUrl'],
connectSrc: ['@viteHmrUrl']
},

为 Vite 注入的样式添加 Nonce

目前,Vite 不允许为它注入到 DOM 中的 style 标签定义 nonce 属性。有一个未合并的 PR 正在处理此问题,我们希望它能尽快得到解决。

配置 HSTS

Strict-Transport-Security(HSTS,严格传输安全) 响应头会告知浏览器始终使用 HTTPS 加载网站。

你可以使用 config/shield.ts 文件配置该响应头指令。

import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
hsts: {
enabled: true,
maxAge: '180 days',
includeSubDomains: true,
},
})

enabled

开启或关闭 hsts 守卫。

maxAge

定义 max-age 属性。该值应该是一个以秒为单位的数字,或一个基于字符串的时间表达式。

{
// Remember for 10 seconds
maxAge: 10,
}
{
// Remember for 2 days
maxAge: '2 days',
}

includeSubDomains

定义 includeSubDomains 指令,以将设置应用到子域名。

配置 X-Frame 保护

X-Frame-Options 头用于指示是否允许浏览器将网站渲染嵌入到 iframeframeembedobject 标签中。

如果你已经配置了 CSP,可以改用 frame-ancestors 指令,并禁用 xFrame 守卫。

你可以使用 config/shield.ts 文件配置该响应头指令。

import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
xFrame: {
enabled: true,
action: 'DENY'
},
})

enabled

开启或关闭 xFrame 守卫。

action

action 属性定义响应头的值。它可以是 DENYSAMEORIGINALLOW-FROM

{
action: 'DENY'
}

ALLOW-FROM 的情况下,你还必须定义 domain 属性。

{
action: 'ALLOW-FROM',
domain: 'https://foo.com',
}

禁用 MIME 嗅探

X-Content-Type-Options 头指示浏览器遵循 content-type 响应头,不要通过检查 HTTP 响应的内容来执行 MIME 嗅探。

一旦启用此守卫,Shield 将为所有 HTTP 响应定义 X-Content-Type-Options: nosniff 响应头。

import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
contentTypeSniffing: {
enabled: true,
},
})