如何为Spring Boot服务的Angular SPA添加CSP nonce以允许内联JS/样式
Spring Boot + Angular SPA 配置CSP Nonce 解决Google Maps内联样式报错方案
核心实现逻辑
每次客户端请求页面时,后端生成唯一安全随机nonce,注入到CSP响应头和返回的Angular入口index.html中,前端将nonce同步给Angular框架和第三方资源,满足CSP校验要求。
步骤1:Spring Boot 后端配置Nonce生成与注入
1.1 新增CSP Nonce全局过滤器
每次请求生成随机nonce,同时将nonce写入CSP响应头:
import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.crypto.codec.Hex; import org.springframework.stereotype.Component; import java.io.IOException; import java.security.SecureRandom; @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class CspNonceFilter implements Filter { // 使用安全随机数生成器,禁止用Random等弱随机实现 private final SecureRandom secureRandom = new SecureRandom(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 生成128位nonce,转16进制字符串 byte[] nonceBytes = new byte[16]; secureRandom.nextBytes(nonceBytes); String nonce = new String(Hex.encode(nonceBytes)); req.setAttribute("cspNonce", nonce); // 拼接完整CSP头,把nonce加入script-src和style-src指令 String csp = String.format(""" img-src 'self' data: maps.gstatic.com *.googleapis.com *.ggpht.com https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com *.a2develop.com; script-src 'self' maps.googleapis.com www.googletagmanager.com 'nonce-%s'; style-src 'self' maps.googleapis.com https://www.google-analytics.com https://ssl.google-analytics.com 'nonce-%s' """, nonce, nonce); resp.setHeader("Content-Security-Policy", csp); chain.doFilter(request, response); } }
1.2 配置SPA入口页渲染
将Angular的index.html从static目录移动到templates目录,引入轻量模板引擎(比如Thymeleaf),新增控制器处理SPA路由,把nonce注入到index.html的占位符中:
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import jakarta.servlet.http.HttpServletRequest; @Controller public class SpaIndexController { // 匹配所有SPA前端路由,排除静态资源路径 @GetMapping(value = {"/", "/**/{path:[^\\.]*}"}) public String renderIndex(HttpServletRequest request, Model model) { String nonce = (String) request.getAttribute("cspNonce"); model.addAttribute("cspNonce", nonce); return "index"; } }
步骤2:Angular 前端适配Nonce
2.1 入口页新增nonce占位符
在index.html的head中新增meta标签存储nonce,同时给所有自定义内联脚本/样式标签加上nonce属性:
<head> <!-- 存储nonce供Angular读取 --> <meta name="csp-nonce" th:content="${cspNonce}"> <!-- 示例:内联脚本加nonce属性 --> <script th:nonce="${cspNonce}"> // 自定义内联脚本逻辑 </script> </head>
注:上述
th:前缀是Thymeleaf语法,如果你用其他模板引擎,替换成对应占位符语法即可。
2.2 Angular框架注入Nonce
在app.module.ts中配置Angular官方提供的CSP_NONCE注入令牌,让Angular生成的内联样式自动带上nonce:
import { NgModule, CSP_NONCE } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; // 从meta标签读取后端注入的nonce export function getCspNonce(): string { const nonceMeta = document.querySelector<HTMLMetaElement>('meta[name="csp-nonce"]'); return nonceMeta?.content || ''; } @NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [ { provide: CSP_NONCE, useFactory: getCspNonce } ], bootstrap: [AppComponent] }) export class AppModule { }
临时替代方案(无需改代码)
如果暂时不想部署nonce全链路配置,可以直接把报错中给出的哈希值加入CSP的style-src指令:
style-src 'self' maps.googleapis.com https://www.google-analytics.com https://ssl.google-analytics.com 'sha256-a2VR/Wq1VPr0+3GRY+lEmAQm7wjwwnDtPpcCs2zTrw='
注意:该方案存在兼容性风险,若后续Google Maps更新内联样式内容,哈希会失效,需要重新添加新的哈希值,长期使用推荐nonce方案。
内容的提问来源于stack exchange,提问作者Jam
相关产品推荐
相关产品推荐

