一 set与变量
① 知识回顾
rewrite模块
1) 关注一些'易错'点、'难'点的案例场景
2) 本文内容很'杂',建议读者'选取感兴趣的阅读'
3) 重点关注: nginx.conf中的'脚本类'指令、本节关注'if和set'
rewrite功能
② 带着问题学习
1) 变量的'作用域'
2) 变量的'声明周期'
3) nginx变量值的'类型'
4) 变量与'请求'的关系
③ nginx变量漫谈
(1) nginx 变量的'创建'和'赋值'操作发生在全然'不同'的'时间'阶段
[1]、nginx '变量的创建'只能发生在'nginx 配置加载'的时候,也即'reload|restart|start'时
[2]、而'赋值'操作则只会发生在'请求实际处理'的时候;
[3]、这意味着'不创建而直接使用变量'会导致启动'失败';
[4]、同时也意味着我们'无法在请求处理时'dynamic'动态地'创建新的 nginx 变量
结论1:
[1]、'变量名'的'可见'范围虽然是'整个'配置,但'每个请求'都有所有变量的'独立'副本
[2]、或者说'都有各变量'用来存放'值'的容器的独立副本,彼此'互不干扰'
结论2:
[1]、nginx 变量的生命期是'不'可能跨越'请求边界'的
遗留: 请求导致的'internal'是在请求边界的,但是'subrequest'各个模块的处理手段'不一样'
[2]、nginx 变量理解成某种在'请求之间'全局'共享'的东西
(3) 常见的内建'非只读变量',这里的只读是指无法通过'set'显示修改
[1]、常见非只读变量:'$args'、'$limit_rate'、'$uri'
1) $args可以通过'set、rewrite形式修改'
备注: '$args_xxx'是只读变量,无法修改
附加: $arg_xxx 变量在请求url中有多个'同名xx'参数时,只会返回最先出现那个'xxx'参数的值
补充: 可以'通过'ngx.req.get_uri_args 函数获取'所有同名的'
2) $limit_rate 只能通过'set'形式修改
[2]、常见的'只读'变量:'$uri',但是可以通过'特殊手段'变相修改
1) $uri可以通过'internal'形式修改,无法通过'$uri'形式修改
2) 常见:try_file、index、rewrite、error_page、'proxy_pass+location'
备注:这里也'隐含'修改传递给后端的'$request_uri'
[3]、set 修改只读变量'报错'
1) [emerg] the duplicate "uri" variable ... --> "尝试 set $uri ..."
2) 修改'$arg_xxx'导致'进程崩溃'
[4]、并非所有的内建变量都作用于当前请求,少数内建变量只作用于"主请求",比如$request_method
(4) 变量与'请求'
1) 变量值容器的生命期是'与请求绑定'的
2) 请求'分类'
[1]、客户端发起的 'HTTP' 请求 --> "原始请求或主请求(main request)"
[2]、internal的内部'重定向'请求 --> 也是'主请求'
常见:try_file、index、rewrite、error_page、echo_exec、internal指令发起"内部跳转"
[3]、subrequest'子请求'
子请求: 是由 nginx 正在处理的请求'在 nginx 内部'发起的一种'级联'请求
常见子请求'指令': auth_request、echo_location、mirror
1) echo_location /sub --> 发起的是'GET'请求
特点: 由 echo_location 发起的"子请求",其执行是按照配置书写的'顺序串行'处理的
说明: 子请求在处理过程中对'变量 $var 各自所做的修改'都丝毫'没有'影响到"主请求"
2) auth_request /sub
特点:ngx_auth_request 模块发起的"子请求"却会自动'共享'其"父请求"的变量值容器
细节: echo 指令作了一些'输出',所以隐式地返回了指示正常的' 200 '状态码
3) ngx_lua、ngx_srcache 在内的许多'第三方模块'都选择了'禁用'父子请求间的'变量共享'
(5) map: '惰性'求值,只有在该'变量被实际读取'时才会执行
nginx调试必备
(6) nginx 变量的值的'类型'
[1] 变量的值'只有'一种类型,那就是'字符串(string)'
[2] 但是变量也有可能压根就'不存在有意义'的值,'没有值的变量'也有两种'特殊'的值
1) 一种是"不合法[invalid]"
备注: 当 nginx 用户变量 $wzj '创建了却未被赋值时',$foo 的值便是"不合法"
2) 另一种是"没找到[not found]"
备注: 当前请求url参数串中并没有提及'xxx'这个参数,则"$arg_XXX"内建变量值便是'没找到'
[3] 无论是'不合法'还是'没找到',这两种nginx变量所拥有'特殊值、空字符串'这种取值是完全不同
1) 'js语言'中也有专门的 'undefined' 和 'null' 这两种'特殊'值
2) 而 lua 语言中也有'专门的 nil 值':
特点: 它们既'不等同于空字符串',也不等同于'数字0',更不是'布尔值' false
[4]、由 set 指令创建的变量未初始化就用在"变量插值"中时,效果等同于'空字符串'
解读:set 指令为它创建的变量自动注册了一个"取处理程序",将'不合法'变量值转换为'空字符串'
++++++++++++++ "上面输出的解读" ++++++++++++++
1) nginx 会首先'检查'其值容器里的取值,结果它看到了"不合法"这个特殊值
2) 于是它这才决定继续调用 $name 变量的"取处理程序",于是 $foo 变量的'取处理程序'开始运行
3) 它向 nginx 的错误日志打印出上面那条'警告'消息
4) 然后返回一个'空字符串'作为 $name 的值,并从此缓存在 $name 的值容器中
+++++++++ nginx'原生'无法区分'undefined [nil]'和'空串' +++++++++
备注: 通过'第三方模块 ngx_lua'可以轻松'区分'
补充:
1) nginx 变量 $arg_language 为特殊值'没找到'、'不合法'时
2) ngx.var.arg_name 在 lua 世界中的值就是 'nil',即 lua 语言里的'空'
判断方式: if ngx.var.arg_language == nil
二 nginx.conf中的脚本类指令
① 带着问题学习
② 为什么需要set外部变量?
考虑方面: set 修改有什么'用处'?
原因1:set $var value,完成两件事情
[1]、如果是'第一次出现'就创建'新new'变量;变量存在则'修改'这个变量值
[2]、通过修改变量'与模块dynamic动态交互',见下面的'两个场景',常见'限速'和设置'memcached'
备注: 可以通过'请求相关信息','动态'交互
注意: 不要与'内建变量或模块预定义变量'重复
ngx_http_memcached_module
③ nginx.conf中二次编程能力
1) nginx.conf'原生支持'脚本类指令,有'条件'分支,有设置变量的'归属'关系,甚至'循环'等等?
[1]、这种二次开发,不需要'lua、js'就可以实现
[2]、原来的'rewrite模块'就是来做这些事情,'set指令'也是rewrite提供的
[3]、set主要是为'给if用'的
+++++++++++++++ nginx.conf的'二次编程'能力 [后续"重点"] +++++++++++++++
1) 早期引入web语言,就会引入很多'不确定'性,现在'通用的lua和js',这些语言很'完备'
2) 后续通过' 三个维度来理解nginx的二次开发' ngx-perl、ngx-lua、ngx-js
二次开发的'产品': openresty、kong、ingress、nginx plus、nginx unit、APISIX
3) nginx原生通过simple virtual stack machine的形式进行二次编程能力,实现简单动态脚本功能
++++++++++++ '脚本语言'涉及'编译器'前端技术 ++++++++++++
例如: if、set、rewrite、return,涉及脚本语言,'脚本语言'涉及四个部分,'见上';
③ 为什么需要set外部变量?
原因2: '脚本类指令'需要完成很多'复杂'的功能,需要依托'set'
1) 但是rewrite模块提供的'脚本类指令是有很多坑'的
[1]、因为'普通的nginx指令'是Directive,是定义式和声明式的指令
[2]、但是'脚本类指令不是',我们一般称之为'动态脚本、动态语言、解释性语言'
[3]、二者完全是'两码'事,但是却'并存'在nginx.conf中
[4]、而且'没有明显'的差别,不像嵌入'lua代码或js代码'这样'明显'
2) 官方的nginx博客说'if是邪恶'的
3) 为什么要讲解'if'指令,这也是回答之前提出的为什么要'set 外部变量',下面先看'if指令'的用法
+++++++++++++ 注意事项
1) 为什么'需要set 外部变量'?了解'if指令'的用法就明白了
2) 'condition'条件的形式
备注:跟'正常的编程语言'的条件'不一样','不'支持计算表达式,类似: '($a > 100)' 等等
3) 只支持'以下'的几种方式,导致我们必须'加入set变量'才能实现一些'复杂'的功能
[1]、直接形式,形如:'($var)' ,不区分'数字'还是'字符串',统一作为'字符串'
[2]、加不加引号: 带有‘空格'、() {}'等'特殊'字符必须加"双"引号或者'单'引号
[3]、注意判断'相等',不要写成'==',nginx压根'没有用=赋值'的形式,所以'不会引起歧义'
[4]、前'三个'与'set'强相关,因为'使用到了变量'
[5]、后面'四'个:跟文件、做'静态资源映射',做静态服务器这个时候才会用到,常见'CDN'使用
备注: 做'反向代理'负载均衡'很少'用到
[6]、上传php的文件,因为php有执行能力,可以通过-x进行判断,检查是否是可执行文件,避免被误执行
4) 如果字符串包含'某种特性'匹配,则提取'局部'变量,也算是'多条件'的一种
例如: if ($uri ~ /kk(?<name>.*) {...},通过if 结合正则捕获,然后引用
5) 为什么需要set?
说明: if实现条件判断,一般需要'逻辑and、or、else(非)',通过set来实现
④ if指令是邪恶的
脚本指令: 'rewrite'模块提供的,'set'、'rewrite'、'break'、'return'、'if'
普通指令: 'add_header'、'proxy_pass'之类的
原因1: 配置块'间'的'指令继承'问题
1)location'天然有作用域'的概念,由于'{}'
[1]、不管是哪种location,同一层级location间'(除非脚本类指令)'是互不影响,互不'干扰'的
举例:多个并列的if就是多个'同级的匿名 no named location'
原因2: 脚本类指令和11个HTTP阶段的执行顺序是'完全不一样'的
[1]、'普通指令'的执行顺序和'脚本类'的执行顺序不一样
[2]、'直观上(感性认识)'好像要么是从'上到下'执行,要么是'按照阶段'执行 --> "错误认知"
[3]、其实不是,'脚本类'指令是按照'相应阶段'从上到下执行,'普通'指令是按照HTTP 11个阶段执行
+++++++++++++++ "分割线" +++++++++++++++
对'指令的继承'和'if指令的作用域'的探讨
1)普通指令有一个'向下继承'关系 --> 父子配置块
2)兄弟配置块,'同一层级';if 之间的'普通指令互相不继承'的 --> 见'下面的add_header'指令
解读: 兄弟之间是无关的'(两个if形成的匿名loation)',上面的if中的普通指令是无法'影响'下面的
细节点: 如果if里面只有'脚本类'指令,没有普通指令,则'不用管',一旦'掺入'普通指令,就要'注意'了
常见: 脚本类指令'(rewrite模块提供)'和普通指令(proxy_pass)'混合使用'
原因2: '脚本类指令'的执行
1) 不存在向下'继承'关系
上面'案例'解读: 也即'不是'location中的rewrite'覆盖'server中的rewrite
而是'按照出现的顺序'执行,两次rewrite'都执行'了
2) '下面'图谱:脚本式指令的执行流程 --> '米红色的阶段执行(rewrite模块的脚本集指令)'
注意:
[1]、server_rewrite中'指令'只会执行一遍
[2]、但是location_rewrite'由于find_location',可能会执行'多次',最多10次
补充: 并不是每次请求'完全经过'11个阶段
[1]、return 直接调用filter
[2]、access'拒绝' 直接调用filter
[3]、 限速,直接调用filter
1) if中放置普通指令,一定要注意'先后'关系,'语义'要自己把握
set的功能主要是'满足if',满足if的'与或非'的功能,就需要'把set放进来',会导致'几个'问题
[1]、if对普通类指令的'继承'
[2]、多个if是匿名location,是同级,是'兄弟关系'location块,是没有继承关系,是排他性的,
[3]、脚本类指令是'顺序'执行的,后续讲解'指令寄存器和栈寄存器'
[4]、普通类指令是按照'11个阶段'执行的
⑤ nginx中利用if 等价&&多条件
需求背景:
1) nginx'不'支持'&&、||、and、or'等逻辑操作符
场景: 一个变量'多个值'的场景
案例: if ($http_name ~ ^(jane|tony)$) { } <==> 任意条件'满足'即可,相当'||'
2) 思考:如何'实现' 类似 if ($arg_a == "" && $arg_b == "") 形式
3) 本文针对'不同'场景,通过'4种'方式来'讲解'
4) 不建议if'进行多条件'判断
注意: 首先'测试案例'要全面,避免有'遗漏',其次测试方式多样化:"浏览器和curl"多种方式
5) 以下只是提供了'一种解决问题'的思路,要根据'场景'合理使用
正则表达式中的if then else 正则表达式if语句
方式1: '多个条件'刚好在一个'变量'中,可以用'if 正则'进行匹配
方式2: 可以通过'map'进行变量的'组合嵌套',然后通过 if ($new_var ~)进行匹配
备注: 这里演示只是'简单'进行变量判断
map优点: 'text'文本可以使用'多个变量',最终'解析的字符串'作为最终的text文本;if'不行'
方式3:变量初始化,然后多个if层层判断 其它参考
原理: 首先通过'if'设置'$flag'标志位,然后在if判断过程中改变'$flag'达到效果
说明: 下面的'案例'演示了'&、|、!'的场景
方式4:PCRE正则表达式if语句
方式4: PCRE正则'零宽度断言',来'模拟if'语句的行为,在匹配模式时根据'条件'选择'不同'的模式
(?(condition)true-pattern|false-pattern) --> "遗留"
备注1: 一般'condition'使用'?=regex、?!regex、?<=regex、?<!regex'或'?=regex'形式
备注2:'condition'也可以使用其它'任何'形式
应用场景: 变量之间有'依赖关系',这里的依赖指的是'值'有关联 --> '某种正则模式'、'包含'关系
补充:
1) 关于正则'被匹配的地方':不能'引用已存在的变量($解读正则字符)'、但可'通过补获产生新变量'
备注: 不管是'map ~',还是'if 中 ~',变量表示的'pattern'都不会进行'解析'
补充: '非'正则场景,if ($http_name = ${arg_name}) {...} --> "变量解析字面字符串"
2) 匹配'地方'可以预定义变量:$(http|arg|cookie)_xx --> 产生'独一无二的变量',避免报错
3) 重点: nginx正则不支持'$var'变量内插,不会进行'变量解析',而是直接作为'正则字符'
⑥ if进行域名跳转
背景: 'A域名'要下线,在'彻底下线'过渡阶段,'A'域名跳转到'B'域名,只进行'域名替换'
细节点: 在server块配置,在'location_config寻址前',直接处理
⑦ if指令再探
1) 从'源码'分析'unexpect if'案例
2) 从'案例'加以'原理'辅助理解
3) if指令是'邪恶'的:不了解'nginx的执行阶段,在'location内'滥用,'server context'不存在
4) 目的: 如何'随心所欲'的使用if,写出'符合要求'的配置
if指令是邪恶的
+++++++++ "if的一些非预期的案例场景" +++++++++
"重点结论":
1) 如果location上下文'if'指令的结果是'match' 的,那么 if 会创建一个'内嵌的 location 块'
2) 只有这里面的 content 处理指令'NGX_HTTP_CONTENT_PHASE 阶段'会执行
3) if 条件为'真(true)'时,nginx 使用这个'无名 location 作用域的配置'处理当前请求
备注:
1、在解析if指令时,会把'if'当做'location'来处理;
2、并且把这个'if对应的location'的type被设置成了'noname',在内部创建一个'无名'location
3、所以在进行'url匹配'时并'不会查找'到此location;
1) 过滤模块是过滤'响应头response_header'和'内容content'的模块
备注: 'add_header'指令就属于'filter'模块
2) 它工作在'获取响应内容'之'后',向用户发送响应之'前'
3) 处理过程分为'两个阶段':过滤HTTP响应的头部和主体,在这两个阶段可以分别对头部和主体进行修改
nginx的filter过滤模块 nginx的11个阶段 详解nginx的11个阶段 if is evil解读
1) proxy_pass 是一个 'action directive',动作指令
2) 按照定义和其它嵌套location 的实现来看,这个指令'不应该被子作用域'继承
3) 但是 location 'if(true) {}' 当作子作用域的话,会被'继承'
NGINX脚本语言原理及源码分析(一)
NGINX脚本语言原理及源码分析(二)
NGINX脚本语言原理及源码分析(三)
NGINX脚本语言原理及源码分析(四)
陶辉大神的三篇变量使用脚本指令
思考: break 指令 和 rewrite 'flag' 为break的'区别'?
1) break'指令'终止所有的'rewrite模块的指令',不仅仅包括'rewrite指令',还有其它'set、if等'
2) rewrite 的'falg'为break也禁止执行后续的'rewrite'模块指令执行
3) 不管哪种'break',都只是停止对应'(SERVER|LOCATION)_REWRITE'阶段的'rewrite'模块指令
4) 然后进入nginx的'下一个'执行阶段
eg: server 块中if指令中使用rewrite ... break,还是会进行'location级别'find_location
⑧ 避坑if方案
+++++++++ "绝对安全[官方推荐]" +++++++++
location / {
if (condition) {
return ...;
}
if (condition) {
rewrite ... last;
}
}
++++++++++ "分割线"
location / {
if ($uri = '/wzj12') {
proxy_pass http://127.0.0.1:11111;
break; # 这里的 break 将阻止下面 if 的执行,跳过其它rewrite模块的阶段
}
if ($uri ~ '/wzj1') {
proxy_pass http://127.0.0.1:11112;
}
}
++++++++++ "分割线"
备注: 两个条件'互斥',不用break
location / {
if ($uri = '/wzj1') {
proxy_pass http://127.0.0.1:11111;
}
if ($uri = '/wzj2') {
proxy_pass http://127.0.0.1:11112;
}
}
⑨ 关于预定义变量,set可以操作的
1) 只读'变量': $request_uri、$uri、$query_string这两个变量也'不能 set'修改;
2) 只有$args 、$limit_rate等极少数可以'直接 set'修改强行修改只读变量会导致程序运行错误;
if ($args ~ (^|.*&)databases=(.*)) {
set $args $1database=$2;
}
验证: 修改'$args'是否影响'proxy_paas'? --> "会影响"