保护服务端渲染应用
如果你正在使用 AdonisJS 创建服务端渲染应用,那么你必须使用 @adonisjs/shield 包来保护你的应用免受常见的 Web 攻击,例如 CSRF、XSS、Content sniffing(内容嗅探) 等。
该包已预先配置在 web 启动套件 中。但是,你也可以按如下方式手动安装和配置该包。
@adonisjs/shield 包对 @adonisjs/session 包有对等依赖(peer dependency),因此请务必先配置 session 包。
node ace add @adonisjs/shield
-
使用检测到的包管理器安装
@adonisjs/shield包。 -
在
adonisrc.ts文件中注册以下服务提供者。{providers: [// ...other providers() => import('@adonisjs/shield/shield_provider'),]} -
创建
config/shield.ts文件。 -
在
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-TOKENcookie 的配置。可用选项请参阅 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
你可以通过在行内 script 和 style 标签上定义 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 secondsmaxAge: 10,}{// Remember for 2 daysmaxAge: '2 days',} -
includeSubDomains
-
定义
includeSubDomains指令,以将设置应用到子域名。
配置 X-Frame 保护
X-Frame-Options 头用于指示是否允许浏览器将网站渲染嵌入到 iframe、frame、embed 或 object 标签中。
如果你已经配置了 CSP,可以改用 frame-ancestors 指令,并禁用 xFrame 守卫。
你可以使用 config/shield.ts 文件配置该响应头指令。
import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({
xFrame: {
enabled: true,
action: 'DENY'
},
})
-
enabled
-
开启或关闭 xFrame 守卫。
-
action
-
action属性定义响应头的值。它可以是DENY、SAMEORIGIN或ALLOW-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,
},
})