故事梗概
Java程序员马意浓在互联网公司维护老旧电商后台系统。
渴望学习新技术的他在工作中无缘Docker和K8s。
他开始自学Vue3并使用SpringBoot3完成了一个前后端分离的Web应用系统,并打算将其用Docker容器化后用K8s上云。
5 历经磨难
马意浓看了一下表,已经是深夜23:30了。
考虑到第二天还要上班,他便关掉了电脑,准备去睡觉。
至于那个CORS的问题,他决定明天再处理。
5.1 重现CORS问题
✅第二天晚上下班一到家,马意浓就立即打开电脑,运行命令,重现了昨晚的CORS问题。
# 启动Docker Desktop
# 进入infrastructure文件夹
# 启动数据库及其管理工具的容器
docker compose up -d postgres pgadmin
# 进入back-end文件夹
# 设置jdk当前版本
jabba use openjdk@1.17.0
# 在本地Gradle开发环境启动后端app
.\\gradlew.bat bootRun
# 在新PowerShell窗口进入front-end文件夹
# 设置node.js和npm版本
nvm use 20.11.0
# 安装package.json文件所设置的依赖库
npm install
# 启动前端app
npm run dev
# 点击屏幕上的5173链接,打开Shopping List Web App前端页面
# 添加一个购物项,打开浏览器Developer tools的Console页签
# 重现CORS问题
吸取上次忘了记笔记的教训,马意浓打开笔记软件,准备记录这次解决CORS问题相关概念和解决步骤。
5.2 恶意转走银行存款的故事
他重新在网上搜索了CORS的资料,并在笔记中记录了CORS的概念。
「CORS(Cross-Origin Resource Sharing):跨源资源共享。」
他在网上浏览了CORS资料。但他不喜欢枯燥的定义。他喜欢听故事。
✅于是他打开AIGC对话框,问:「你是网络安全专家。假设浏览器没有跨源资源共享CORS安全机制,那么对于一个使用前后端分离的银行web应用的用户来说,会有什么风险?请用一个跌宕起伏的故事来讲述。」
AIGC很快回复:「在早春三月的第一个周末,一位名叫John的GuardianBank银行客户,像往常一样打开了网上银行。」
「他每个月这个时候,都要核对上个月的账单,并向母亲转1000元赡养金。」
「当他打开账户,却惊愕地发现,账户余额从原先的4万多元,变为不足1元!」
「他急忙检查自己账户的交易明细。发现账户中的钱,已经分5笔,转给了一个陌生账户。」
「他仔细查看了这5笔转账发生的时间,竟然都是在一个月前,他上次做账单核对并给母亲转账之后的半小时之内。」
「这太奇怪了!John在这一个月里,明明只给他母亲转了1笔钱。如图1。」
「后来John从网络安全专家那里得知,那5笔转账,是黑客冒用他的名义,把剩下的钱都给转走了。」
「黑客是如何做到的呢?让我们把时间回溯到事发前的一个月。」
「在一个名为CyberTown虚拟城市中,GuardianBank是一家深受大家信赖的银行。」
「GuardianBank因其提供的先进的网上银行服务而闻名遐迩。客户可以通过这一平台进行转账、支付账单和投资管理。」
「这一系统采用了现代的前后端分离架构,以提升开发效率,适应技术变革,以及获得更好的可维护性和可扩展性。」
「然而,John却生活在一个浏览器没有CORS安全机制的平行世界里。GuardianBank的故事发生了意想不到的转折。」
「二月的第一个周末,John照例使用GuardianBank网上银行,完成每月账单核对,并给他母亲转1000元赡养金。」
「之后,他开始在网上搜索关于如何投资的建议。不经意间,他发现了InvestSmart网站。」
「InvestSmart网页外观设计十分专业,其中的投资信息很有价值。」
「这吸引John仔细阅读了其中几篇文章,并收藏起来。」
「读了半小时文章后,他关上电脑,外出打球去了。」
「一个月后,当他再次在网上银行核对账单时,就发生了故事开头令他惊愕的一幕。」
「后来网络安全专家告诉John,InvestSmart网站,实际上是一个圈套。」
「一个名叫Eve的黑客,创建了InvestSmart网络诈骗网站。」
「她在网站中嵌入了恶意JavaScript代码,以便发动一场针对GuardianBank及其客户的精心策划的攻击。」
「这段代码在John的浏览器中静默运行,而John完全没有察觉。」
「Eve的代码将John的浏览器作为一个跳板,向GuardianBank的服务器发送请求。这个请求伪装成John想要执行的银行操作,比如转账。」
「恶意代码在John浏览InvestSmart网站投资文章的同时,构造了一个发向GuardianBank网上银行的转账请求。」
「这个请求看起来就像是John主动请求从他的账户转账到Eve指定的账户一样。」
「由于John之前已经登录了GuardianBank的网上银行系统,为他母亲转账,而且做完这些之后没有立即安全退出网上银行,所以他的浏览器还保存有登录会话。」
「网上银行之所以这样设计,是为了方便客户在接下来很短的时间内,接着继续使用网上银行时,不用再次输入登录密码。」
「但网上银行却不知道,客户紧接着却点开了InvestSmart网络诈骗网站。」
「恶意代码就利用这一点,使请求伪装得更加合法。」
「在这个平行世界中,由于缺乏适当的安全措施,比如浏览器缺失CORS安全机制,或没有二次验证,前后端分离的网上银行系统的后端app,就会处理这种伪造的请求,从John的账户将资金转移到Eve的账户。」
「由于整个过程是在后台进行,所以当John继续浏览InvestSmart网站时,完全不知道自己的银行账户正在被恶意代码操纵。」
马意浓很喜欢这个故事。他给AIGC的回复,点了一个赞。
5.3 恶意转走银行存款的故事的架构图
他的vscode中,安装了PlantUML和PlantUML Previewer插件。他用c4 plantuml为这个故事画了一个架构图。如图2。
他又在图中Web浏览器的上部,以及网上银行后端app的上部,分别画了一个红圈,并标上No CORS。
这表示在没有CORS安全机制的平行世界,就更容易发生银行存款被恶意转走的悲剧。
他用这种方法,提醒自己,CORS安全机制,需要浏览器和后端app相互配合,才能实现。
他意识到,由于浏览器是唯一与银行客户打交道的工具,所以它为客户起到了网络安全“管家”的作用。
5.4 什么是Origin(源)
他又问AIGC:「对于CORS安全机制来说,origin(源)指的是什么?」
AIGC回答:「在CORS(跨源资源共享)安全机制中,"origin"指的是发起请求或访问网页的源。」
「它由三个部分组成:协议(如HXXP或HXXPS)、主机名(或域名),以及端口号(如果指定)。」(注:因自媒体平台不喜外恋,故恋接协议都写作hxxp。下同)
「例如,对于一个以hxxp://localhost:5173/
为地址的网页,其origin就是hxxp://localhost:5173
。」
他领悟到,如果用前后端分离的购物清单web应用的架构,来设计网上银行,那么前端app和后端app,虽然主机名相同,都是localhost,但因为端口号不同,所以分属不同的“源”。
# ShoppingList前端app的源 hxxp://localhost:5173
ShoppingList后端app的源
hxxp://localhost/api/v1/shopping-items
5.5 为何看到CORS出错信息
马意浓继续思考。
在有CORS安全机制的现实世界,当浏览器发现源为hxxp://localhost
的前端app网页,试图向与自身不同的源hxxp://localhost/api/v1/shopping-items
的后端app发起请求时,就判断发生了跨源请求。
浏览器将根据CORS策略,来判断是否允许这个请求。
若作为资源服务器的后端app发给浏览器的响应头,包含了允许请求源的CORS头部信息Access-Control-Allow-Origin
,那么浏览器会让这个请求获得访问资源的权限。
反之,如果没有这个头部信息,或者返回的值与请求的源不匹配,那么浏览器就会阻止访问,并在Developer tools窗口的Console
页签,报CORS出错信息。
Access to XMLHttpRequest at '
hxxp://localhost/api/v1/shopping-items
' from origin 'hxxp://localhost
' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
马意浓现在理解了,这种机制有助于提升网页的安全性,防止不可信任的网页访问到敏感资源。
5.6 如何在后端app配置CORS
对于前后端分离的web网上银行来说,它的前端app,应该信任自己的后端app。
那该如何让后端app发给浏览器的响应头里,包含允许前端app请求源的CORS头部信息Access-Control-Allow-Origin
呢?
马意浓于是问AIGC:「你是spring boot编程专家。如何在一个前后端分离的Web应用中,在spring boot 3实现的后端web应用里,配置CORS,使得来自前端app源hxxp://localhost:8080
,能够访问这个后端app?」
AIGC回答:「在Spring Boot 3中配置CORS,可以通过几种不同的方法实现,让来自前端应用地址 hxxp://localhost:8080
的请求能够访问后端应用。以下是几种配置CORS的方法。」
「1 全局CORS配置。可以在Spring Boot应用中通过实现WebMvcConfigurer
接口并重写addCorsMappings
方法来全局配置CORS。这种方式适用于所有的控制器和映射路径。」
「2 使用@CrossOrigin
注解。在控制器或者具体的请求处理方法上使用@CrossOrigin
注解,也可以实现CORS配置。这种方式更加灵活,适用于只想对特定的控制器或请求方法开放跨域访问权限。」
「3 通过配置文件。在application.properties
或application.yml
配置文件中,也可以进行CORS配置。这种方式简单且不需要改动代码,适用于基本的CORS需求。」
马意浓查看了一下代码。发现back-end/src/main/java/com/wuzhenben/shoppinglist
目录下的ShoppingListApplicationConfig.java
文件,已经对CORS进行了全局配置。
registry.addMapping(
"/api/v1/shopping-items/**"
).allowedOrigins(
"hxxp://localhost:8080"
).allowedMethods(
"GET", "POST", "PUT", "DELETE");
5.7 如何解决CORS问题
马意浓从全局CORS配置中看出,后端app已经允许源自hxxp://localhost:8080
的前端app来访问它。
而之前运行命令npm run dev
所启动的前端app,源却是hxxp://localhost:5173
。难怪浏览器会报CORS出错信息。
在用命令npm run dev启动前端app时,该如何指定启动后的端口号是8080,而不是默认的5173?
马意浓在网上查到了命令。他回到之前启动前端app的PowerShell窗口,按q
键中止前端app。
之后他运行命令npm run dev -- --port 8080
。前端app果然在端口8080启动。
他按下Ctrl
键,并点击PowerShell窗口中的8080链接。ShoppingList前端app页面,便很快地在浏览器中显示出来。
他在页面Item输入框中输入a banana,并点击Add按钮。
”a banana”终于出现在下面的清单里!如图3。
他又试着点击a banana右边的radio button,把这个购物项设置为已购买。
然后他又点击Delete按钮,删除了这个购物项。之后,他又添加了an apple。
他还是有点不放心,于是用快捷键Ctrl+Shift+I
打开Developer Tools界面,查看了Console页签,没有看到任何错误信息。
他又用浏览器访问数据库管理界面hxxp://localhost:5050/
,看到了shoppingList数据库中确实存入了刚刚所添加的购物项an apple。
马意浓明白,至此,shopping list web app已经在本地Gradle/npm开发环境中成功运行了。
✅他在笔记中写道:「在有CORS安全机制的现实世界,因为黑客的InvestSmart网站中的投资文章的源,肯定不同于网上银行的源,并且网上银行的后端app绝对不会设置成允许黑客网站的源来访问,」
「所以即使客户在使用完网上银行后没有安全退出,就立即浏览包含恶意代码的黑客网站,CORS安全机制也能有效降低客户存款被恶意转走的风险。」
「当然,作为银行客户,一旦用完网上银行,最安全的做法就是立即安全退出。」
「就好比你打开保险柜放进钱后,就立即锁上保险柜,以防有贼趁虚而入一样。」
5.8 清理现场
马意浓知道,程序运行完后,清理现场,是个好习惯。
他回到前端app运行的PowerShell窗口,按q
键中止运行。
他在后端app运行的PowerShell窗口,按Ctrl+C
键中止运行。
最后他在PowerShell窗口,进入infrastructure文件夹,运行命令docker compose down
,终止并删除postgres和pgadmin容器,以及相关网络资源。
他回到docker desktop界面,看到这两个容器果然消失了。
历经了CORS磨难之后,马意浓知道下一步,就是要将后端app和前端app制作成docker image,并且在本地docker compose里试运行,以便为将来上k8s云做准备。
他还会碰到什么艰难险阻?且听下回分解。
【未完待续】
后面连载内容大纲先睹为快:
6 夺取宝剑
6.1 在本地docker compose里的软件架构
6.2 免费注册Docker hub账号以便推送docker image为部署k8s做准备
6.3 构建后端docker image并推送到docker hub
6.4 构建前端docker image并推送到docker hub
6.5 在本地docker compose里运行shopping list web app
6.6 清理现场
7 上云之路
7.1 注册Azure k8s service云平台账号
7.2 打开docker desktop kubernetes让kubectl能正常工作
8 复活重生
8.1 在k8s云集群中运行shopping list web app时如何配置前端app在k8s云集群中的对外域名和端口号以解决CORS问题
8.2 在全绽园的帮助下为前端app配置ingress后解决了这个问题
8.3 在k8s云集群中的软件架构
8.4 如何新增k8s的deployment、service和ingress的配置文件,以便使用kubectl命令将ingress和postgres、shopping-list-api和shopping-list-front-end这3个微服务部署到k8s上
8.5 构建后端docker image并推送到docker hub
8.6 构建前端docker image并推送到docker hub
8.7 在k8s云集群上配置postgres、shopping-list-api和shopping-list-front-end三个微服务和ingress并运行
8.8 清理现场
9 取经归来
当最终把前后端分离的web应用成功部署到azure k8s云集群上,并能顺利使用后,马意浓把整个容器化和上云之旅,写成系列文章,分享给其他程序员。
你能否跟着马意浓一步步做下来?在阅读中有任何疑问,欢迎在留言区留言。我会一一回复。
❤️如果喜欢本文,那么点赞和留言,并转发给身边有需要的朋友,就是对我的最大支持 。
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!