整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

spring cloud security oaut

spring cloud security oauth2请求资源服务器401的排查过程

文的环境是参考这篇《Spring Cloud OAuth2 实现用户认证及单点登录”》搭建的。

https://www.cnblogs.com/fengzheng/p/11724625.html

问题描述

当前已经获取到了access_token,根据access_token请求自己的接口时报了401,token验证有问题。

我自己的接口是:

/**
     * 一个 RESTful 方法,只有当访问用户具有 ROLE_ADMIN 权限时才能访问,否则返回 401 未授权。
     *
     * 通过 Authentication 参数或者 SecurityContextHolder.getContext().getAuthentication() 可以拿到授权信息进行查看。
     * @param authentication
     * @return
     */
    @GetMapping("get")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Object get(Authentication authentication){
        authentication.getCredentials();
        OAuth2AuthenticationDetails details=(OAuth2AuthenticationDetails) authentication.getDetails();
        String token=details.getTokenValue();
        return token;
    }

解决方式

org.springframework.security.oauth2.provider.token.RemoteTokenServices

我们将这个类复制出来,在java下创建相同的包名,类。

源码图

在源码中,这里返回的active应该得为Boolean类型,但是这个map的值是个Object,返回直接返回了个String类型。

他使用了Boolean和String类型进行比较,所以我得到的返回结果永远都是false了,就一直走了这个异常。(具体他为什么会这样来比较,我真不太懂,欢迎大佬们在评论里指点一下)

修改后的图

那么我的处理就比较简单粗暴了,直接把这个类提出来,然后把这个Boolean.TRUE.toString()一下。类型一致,比较就正常了。

再次去请求,现在就没有问题了,返回成功

本文只是个人在学习过程中的一次排查处理过程,如果有更好的见解欢迎大佬们指点。

者:redmed

背景

Web 项目中经常会遇到处理 URL 中 Query 的情况,来看下下面问题你有疑惑吗?

  • 项目中发现会用到 qsquery-stringURLSearchParams、甚至 querystring 几种不同的库,其到底差异在哪里,我该用哪个?
  • 在 query 中 key=a&key=b 这种情况 key 取值是什么?和 key[]=a&key[]=b 有区别嘛?
  • 在 query 中会有结构如 %HH 的数据,为什么是这样形式的?我们为什么要使用 encodeURIComponent 进行编码?和过时的 escape 又有何区别?
  • Content-typex-www-form-urlencoded 的取值,是怎么一回事?

于是梳理一下关于 URL Query 的相关知识点,用来去伪解惑。

URL QueryString

首先介绍下 Query String 的基本概念,这是一切问题的开始。下面是 wiki[1] 的描述:

A query string is a part of a uniform resource locator[2] (URL) that assigns values to specified parameters.

通常的理解就是 URL 中问号(?)后面的部分,其设计最初是用做 HTML form 表单提交时的传参。

基本结构

下面我们看下 query 的基本结构 field1=value1&field2=value2&field3=value3...

包含了如下标准:

  1. Query String 由一组键值对(field-value)组成;
  2. 每组键值对的 fieldvalue=分割;
  3. 每组数据用&分割;

补充个冷知识:除了使用&分割每对数据外,W3C 曾在 1999 年建议所有 Web 服务器同时支持分号;分割符:

We recommend that HTTP server implementors, and in particular, CGI implementors support the use of ";" in place of "&" to save authors the trouble of escaping "&" characters in this manner.

但在 2014 年以来,就只建议使用 & 作为分隔符了。也就目前我们用到的方式。

  1. 允许多个value被关联到同一个field上,但 field 如何取值,其实并无明确的处理标准。

例如:field=a&field=b时,field 的值应该是 a、b、['a', 'b']、'a, b' 并无任何权威解释。

关于处理标准这点实在令人出乎意料。通常这类情况会按照数组的方式处理,即 field 值为 ['a', 'b'],但这仅是不同的框架的决定了如何实现而已。

关于这个问题可以前往 stackoverflow[3] 上查看。

数据编码

前面定义好了整体结构,接下来我们看下数据是如何在 query 中传输的。

由于某些字符集(如中文)和在 URL 中有特殊含义的字符(如 空格、%、&、=、?、# 等)无法直接在 Query String 中使用,因此使用了一种叫做「百分号编码[4] Percent-encoding[5]」的方式先将这类特殊字符进行编码后,再进行传输。

其基本结构就是 % + 2 个 16 进制数字(一个 Byte 的内容),范围 %00 - %FF。

具体规则如下:

  • 对保留字符进行编码,具体对应如下:

! # $ & ' ( ) * + , / : ; = ? @ [ ] %21 %23 %24 %26 %27 %28 %29 %2A %2B %2C %2F %3A %3B %3D %3F %40 %5B %5D

其对应的就是这些字符的 ASCII 编码的 16 进制格式;

  • 如下非保留字符不进行编码,包含:[A-Z][a-z][0-9]-_.~
  • %百分号编码为%25
  • 空格编码为+%20
  • 其余字符数据使用某种编码方式转换为字节流,再用百分号编码%HH方式表示。
    这里需要注意的是,由于早期规范中未明确应使用何种编码,所以会导致如果不明确说明使用何种编码,数据的解析会有歧义。因此在 2005 年发布的 RFC 3986 [6]建议是先转成 UTF-8 编码,再对每个字节进行%HH的编码。

注意,如果使用 from 表单 action 方式时,具体编码会根据 meta 头的 charset 的选择。

当然上述只是标准,实践中 JavaScript 内置了使用 UTF-8 编码的 encodeURI/encodeURIComponent 函数,大大简化的编码过程。

关于指定编码,这里有个有趣的事情:

在使用百度时,你会发现 URL 中有个 ie 参数,其实含义就是 Input Encoding(对,不是 IE 浏览器),目的就是指定关键词 wd 的编码格式。曾默认是 GB2312(因为当时很多网站还使用 GB2312 编码),当然现在已经默认成 UTF-8 。(不过百度结果里依然有不少文章还在说 ie 的默认值是 GB2312 )

可以用 https://www.baidu.com/s?wd=%E4%B8%AD&ie=gb2312 和 https://www.baidu.com/s?wd=%E4%B8%AD 来感受下他们的差异吧~

编码实践

这一节我们挑重点地对比下各类 Query String 的函数库,了解老虎老鼠的差异,避免开发时傻傻分不清楚。

以下仅对常用 API 的部分用法做演示,更多用法可自行查找。

瑞士军刀 qs

github[7]

A querystring parsing and stringifying library with some added security.

官方介绍很简单:一个增加了安全性的 Query String 解析和序列化的函数库。

.parse(string, [options])

  1. 对于简单 query,可以进行常规的转换,同时会对 fieldvalue 进行 decode 解码。
qs.parse('a=c&b%201=d%26e');

// { a: 'c', 'b 1': 'd&e' }

注意 qs 不会忽略头部的 ?,需要自行去掉,否则会当做 field 的一部分,例如:qs.parse('?a=b')会解析为 { '?a': 'b' }

  1. 支持 query 中的嵌套对象。
qs.parse('foo[bar]=baz');

// { foo: { bar: 'baz' } }

但默认子元素最多嵌套 5 层,需要通过 parse(string, [options])opinion.depth 来修改。

// defalut
qs.parse('a[b][c][d][e][f][g][h][i]=j');

// {a: {b: {c: {d: {e: {f: {'[g][h][i]': 'j'}}}}}}}

// set depth
qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
// { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } }
  1. 支持自定义除&以外的分隔符。
var delimited=qs.parse('a=b;c=d', { delimiter: ';' });
// { a: 'b', c: 'd' }

这点符合 W3C 对;支持的建议,但大部分情况应该不会用到。

  1. 支持各种 array 的解析,虽然官方文档写了 [] 作为数组标识,但实际上不使用[]依然可以解析。
var withArray=qs.parse('a[]=b&a[]=c');
// { a: ['b', 'c'] }

var withArray=qs.parse('a=b&a=c');
// { a: ['b', 'c'] }

同时也支持为数组指定索引顺序。

var withIndexes=qs.parse('a[1]=c&a[0]=b');
// { a: ['b', 'c'] };

并行支持 allowSparse 获取抽稀形式的数组。

var sparseArray=qs.parse('a[1]=2&a[3]=5', { allowSparse: true });
// { a: [, '2', , '5'] };

但默认指定的 index 最大值为 20,如果超过最大值,则按照 object 形式解析。使用 arrayLimit控制最大值。

var withMaxIndex=qs.parse('a[100]=b');
// { a: { '100': 'b' } }

var withArrayLimit=qs.parse('a[1]=b', { arrayLimit: 0 });
// { a: { '1': 'b' } }

.stringify(object, [options])

这里主要介绍下 array 类型的编码。qs 默认会对 fieldvalue 都进行编码,同时会使用[]作为数据的标识(且默认对[]进行百分号编码),需指定 encodeValuesOnly: true才仅对 value 编码。

// defalut
qs.stringify({key: ['a', 'b']});
// key%5B0%5D=a&key%5B1%5D=b

//
qs.stringify({key: ['a', 'b']}, { encodeValuesOnly: true });
// key[0]=a&key[1]=b

去掉[]标识,可使用 { indices: false }

qs.stringify({key: ['a', 'b']}, { indices: false });
// key=a&key=b

支持配置 charset

默认使用 UTF-8,内置了 ISO-8859-1 模式,也可以支持 encoder 扩展。

而接下来的库仅支持 UTF-8 的编码方式。

简洁专注 query-string

github[8]

Parse and stringify URL query strings[1]

For browser usage, this package targets the latest version of Chrome, Firefox, and Safari.

官方名字看起来,依旧是处理 Query String 的。

另外,官方还送上友(wei)情(xian)提示,各位同学不要看走眼。

Not npm install querystring !!!!!

.parse(string, [options])

  1. 基本的解析和 qs 一样,会对 field 和 value 进行 decode

不过,头部的?#的部分将被忽略,因此可以直接将 location.searchlocation.hash 传入。

queryString.parse('a=c&b%201=d%26e');

// { a: 'c', 'b 1': 'd&e' }
  1. 不支持嵌套,官方建议可以使用 JSON 序列化的方式传值。

This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of edge cases[10].

You're much better off just converting the object to a JSON string:

  1. query 中 array 的解析,默认不支持[]形式,需要指定 { arrayFormat: 'bracket' }开启。
queryString.parse('key=a&key=b');
// { key: ['a', 'b'] };

queryString.parse('key[]=a&key[]=b');
// { 'key[]': ['a', 'b'] };

queryString.parse('key[]=a&key[]=b', { arrayFormat: 'bracket' });
// { key: ['a', 'b'] };

当然 query-string 也支持索引的方式标记的数组,{arrayFormat: 'index'}

queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3', {arrayFormat: 'index'});
{foo: ['1', '2', '3']}

.stringify(object, [options])

依然重点介绍 array 类型的编码,默认不使用[]标识。

queryString.stringify({key: ['a', 'b']});
// key=a&key=b

需要[]的话,使用 {arrayFormat: 'bracket'}开启,默认[]也不会被 encode

queryString.stringify({key: ['a', 'b']}, {arrayFormat: 'bracket'});
// key[]=a&key[]=b

这点和 qs 是相反的,需要特别注意!

历史产物 querystring

NodeJS 中解析 query 的模块。

NodeJS 14.x[11] 中明确标记为 Legacy,官方推荐 URLSearchaParms 代替。

The querystring API is considered Legacy. New code should use the URLSearchParams[12] API instead.

但在 15.x 以及以后的版本又改为 Stable,但指出这是非标准 API。

querystring is more performant than ``[13] but is not a standardized API. Use <URLSearchParams> when performance is not critical or when compatibility with browser code is desirable.

功能类似 query-string,不支持嵌套对象的解析,这里不再赘述。

血统纯正 URL / URLSearchParams

URLURLSearchParamsURL API 规范[14] 中的两个标准的接口。其提供了访问、操作 URL 的 API。

其中,URL 定义了像域名、主机和 IP 地址等概念,URLSearchParams 定义了一些常用的方法来处理 Query String。我们重点介绍下后者。

URLSearchParams

两种方式创建 URLSearchParams 对象,URLSearchParams构造函数会忽略 search 中的?

// 1. 通过 URL
const url=new URL('https://abc.com/path/v1?key=a&key=b%26c');
const search1=url.searchParams;

// 2. 直接构造
const search2=new URLSearchParams(location.search);

.get(name)

该方法获取的值会被自动 decode,如果 name 不存在返回 null,如果 value 不存在返回空字符串。

const search=new URLSearchParams('key=b%26c&key2');
search.get('key'); // b&c
search.get('key2'); // ''
search.get('key3'); // null

.getAll(name)

需要特别注意,如果有多个相同的 name,get() 只能获取第一个值。获取全部需要使用 getAll(),该函数返回数组(即便只有一个 value)。

const search=new URLSearchParams('key=a&key=b');
search.get('key'); // a
search.getAll('key'); // ['a', 'b']

.set(name, string) / .append(name, string)

URLSearchParams 中添加数据,set() 会覆盖原有值。如果需要添加重复的 name,需要使用 append()

set()append() 仅支持 string 类型的 value。同时 field 和 value 都会被 encode,无需额外处理。

const search=new URLSearchParams();
search.append('key', 'a');
search.append('key', 'b');

search.toString(); // key=a&key=b

.keys()

返回一个 IterableIterator迭代器,可以使用for...of遍历。需要注意,重复的 key 会出现多次

const search=new URLSearchParams('key=a&key=b');
for (const key of search.keys()) {
  console.log(key);
}
// key
// key

.toString()

获取的 Query String,会被自动 encode 处理。空格转成+。对于重复 field,使用了 field=v1&field=v2 的方式。

const search=new URLSearchParams();
search.set('key', '?&=')
search.set('key2', 'a b');
search.toString(); // key=%3F%26%3D&&key2=a+b

兼容

关于兼容,目前浏览器占比基本上没有问题。实际开发中遇到 iOS10 以下不兼容的情况,使用 polyfill 即可。

总结对比

从上面的总结来看,我们发现 qsquery-string / URLSearchParams 最大的差异在于对于多层嵌套对象(Nested object)的支持与否。

  • qs被设计用于解析x-www-form-urlencoded数据,拥有强大的序列化能力,可以处理复杂的类 JSON数据。
  • query-stringURLSearchParams 则使用简单的序列化算法,适合常规的 Web 端数据传输,处理平面数据结构。
  • 对于平面数据,以上效果是一样的。

而当使用复杂的 JSON 数据结构时,我们通常会使用JSON.stringify() 方法先将数据进行序列化(也称字符串化),将复杂数据转换成基本的字符串数据后,再进行传输。

  • 另外如果有特殊编码需求,除qs外都仅支持 UTF-8 的编码。

因此通常情况下:

  • Web 项目中解析 GET 形式的 query,使用 URLSearchParams 就足够了(可代替query-string);
  • 而在 NodeJS 项目中,除了解析 GET query 外,还要解析 POST body 中的数据,因此使用 qs 可以获得更好的兼容性。同时不少框架也依然使用了 querystring 这个原生 API。

expressjsbody-parser 中,用户可以自行选择使用 qs 还是 querystring

koajskoa-bodybodyparser所依赖的 co-body,都选择了qs

当然了解了他们差异后,选择哪种方式就要根据你的实际情况而定了。

延伸话题

整理资料过程中,引申出更多有趣的问题,也稍作整理。

空格编码问题

还记得前面提到的编码规则里, 空格的编码可以是 + 或者%20,这里描述的就很模糊。

函数对比

我们先来看下上面不同 API 是如何处理的?

+%20的识别都没问题(毕竟兼容还是能做到的),但是转换空格URLSearchParams就有不同的逻辑了。至于为什么会有两种编码结果?

这里要特别说明的是URLSearchParams采用了application/x-www-form-urlencoded编码模式,而这个编码采用了一个非常早期(RFC 1738)的通用百分号编码方法——就是将 空格转换为+。至于为什么会采用这种方式,我猜想是因为要考虑到历史兼容问题——生成的 URL 需要被那些旧的仅支持+的程序识别。

当然+已经不推荐了,在 RFC 3986[15] 中已推荐使用%20

特别说明

这里特别说明下 decodeURIComponent,是无法解析+ 空格的,因此实际业务中,如果无法保证传入空格的编码方式,还是使用URLSearchParams或者query-string来解析数据吧。

或者做一个简单的兼容处理:

function decodeQueryParam(p) {
return decodeURIComponent(p.replace(/+/g, " "));
}

decodeQueryParam("search+query%20%28correct%29");
// 'search query (correct)'

扩展参考

URLSearchParams+的问题,具体细节可参考 whatwg 的描述:

As a URLSearchParams object uses the application/x-www-form-urlencoded format underneath there are some difference with how it encodes certain code points compared to a URL object (including href and search ). This can be especially surprising when using searchParams to operate on a URL[16]’s query[17].

URLSearchParams objects will percent-encode anything in the application/x-www-form-urlencoded percent-encode set, and will encode U+0020 SPACE as U+002B (+).

以及 whatwg 中关于 application/x-www-form-urlencoded 的描述:

Control names and values are escaped. Space characters are replaced by '+', and then reserved characters are escaped as described in [RFC1738][18], section 2.2: Non-alphanumeric characters are replaced by %HH, a percent sign and two hexadecimal digits representing the ASCII code of the character. Line breaks are represented as "CR LF" pairs (i.e., %0D%0A).

Content-type 中的 x-www-form-urlencoded

当我们在HTTP中使用 MIME 类型为x-www-form-urlencoded格式提交数据时,所使用的就是前文所介绍的编码方式。

只是如果发送的是 GET 请求,数据会拼接在 Query 中;而发送 POST 请求则会将数据放置在消息体(body)中,通过Header中的Content-Type 来指定 MIME 类型。

当然并不是所有的数据都适合使用 x-www-form-urlencoded,通常有二进制数据时,urlencoded使用百分号%HHUTF-8的编码方式,会大大增加了数据的长度。为了节省传输数据的空间,会选择form-data代替。

原生 from 表单的编码

除了上面提到的各类函数外,原生 html 的 form 表单在提交数据时,本身也是可以进行编码的。

<html>
<head>
  <meta charset="UTF-8">
  <!--  <meta charset="GBK">-->
</head>
<body>
  <form action="/search" method="get" enctype="application/x-www-form-urlencoded">
    <input type="text" name="name" required>
    <input type="submit" value="提交">
  </form>
</body>
</html>

当点击提交时, 表单内的 input 数据会进行百分号编码。但要注意的是编码的格式是按照 meta 中设定的 charset 进行的。例如当输入「中」时,UTF-8 是 name=%E4%B8%AD,GBK 则是 name=%D6%D0。空格 则是+

encodeURI(Component) 和 escape

前文提到过encodeURI(encodeURIComponent)使用 UTF-8 编码,而 escape 是一个已经被废弃的非标准方式,其采用了 UTF-16 编码,同时在码点小于 255 的使用 %uXX 表示,码点大于 255 的使用 %uXXXX 的方式。

同时要注意当decodeURI(decodeURIComponent)解析非法的 %HH 格式数据时(如不合规范的 UTF-8 数据、被截断的%HH 字符等),会包抛出URIError异常。

try {
  const a=decodeURIComponent("%E0%A4%A");
} catch (e) {
  console.error(e);
}
// URIError: malformed URI sequence

因此如果无法保证数据的可用性,记得总是要 try...catch 一下比较保险。

或者更推荐使用类似`safe-decode-uri-component`[19]的三方库,来避免这类麻烦。

至于 UTF-8 的合法格式是什么样的,这就要涉及更多的编码知识了。

参考资料

[1] https://en.wikipedia.org/wiki/Query_string

[2] https://en.wikipedia.org/wiki/Uniform_resource_locator

[3] https://bytedance.feishu.cn/docx/D6LTd2zgHo2S5NxaFE6cnh9knHf#Oo6UdgOqSog2G2xsp3VcSPdIn6d

[4] https://zh.wikipedia.org/wiki/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81

[5] https://en.wikipedia.org/wiki/Percent-encoding

[6] https://datatracker.ietf.org/doc/html/rfc3986

[7] https://github.com/ljharb/qs

[8] https://www.npmjs.com/package/query-string

[9] https://www.baeldung.com/postman-form-data-raw-x-www-form-urlencoded

[10] https://github.com/visionmedia/node-querystring/issues

[11] https://nodejs.org/docs/latest-v14.x/api/querystring.html

[12] https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams

[13] https://nodejs.org/dist/latest-v19.x/docs/api/url.html#class-urlsearchparams

[14] https://url.spec.whatwg.org/#api

[15] https://datatracker.ietf.org/doc/html/rfc3986#section-2.1

[16] https://url.spec.whatwg.org/#concept-url

[17] https://url.spec.whatwg.org/#concept-url-query

[18] https://www.w3.org/TR/html4/references.html#ref-RFC1738

[19] https://github.com/jridgewell/safe-decode-uri-component

[20] https://stackoverflow.com/questions/29175465/body-parser-extended-option-qs-vs-querystring/29177740#29177740

[21] https://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2

[22] https://stackoverflow.com/questions/1746507/authoritative-position-of-duplicate-http-get-query-keys

[23] https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1

[24] https://url.spec.whatwg.org/#interface-urlsearchparams

[25] https://datatracker.ietf.org/doc/html/rfc1738

[26] https://www.w3.org/International/O-URL-code.html


关注「字节前端 ByteFE」公众号,追更不迷路!


文末彩蛋 >>

码上掘金正在举办第 2 期月赛,在这里邀请大家参与「互动抽奖」活动,参与互动即可抽奖哦~活动玩法请看这里 >>
https://juejin.cn/post/7213184860675571749/

家好,我是站长 polarisxu。

看到标题,大家应该知晓今天聊的主角是谁。是的,它就是 PHP。

PHP 曾经很辉煌,现在怎么样?不做过多评价,前几天好未来不刚组织了一届 PHP 大会吗?!正因为曾经很辉煌,很多现在的 Go 爱好者曾经都是 PHPer,应该还有不少还在用着 PHP。我觉得完全没必要非得贬低一门语言去抬高另外一门语言,自己喜欢就好。而且掌握多门语言是自己的优势。

为什么聊 PHP,因为我也写了好几年 PHP,而且现在也会关注 PHP 的一些动态。PHP 8 发布差不多半个月了,有些人可能根本不知晓,还停留在 PHP 5.x。没想到吧,一眨眼,PHP 8 都发布了。

关于版本的那些事,这里不探讨,主要看看 PHP 8 有哪些新特性。另外,本文只会讲述新特性的一些关键点,因为官方文档对它们已经有更详细的介绍,你应该认真阅读官方文档。

01 Union Types(联合类型)

说明一点,从 PHP 7 开始,支持下面这样的语法:

function sum(int $a, int $b): int {
    return $a + $b;
}

是不是越来越强类型的感觉?虽然如此,但在非严格类型模式下(strict_types=0,这是默认值),你依然可以这么调用:

sum(1.2, 3);

但因为函数参数接收 int 类型(返回值也是 int 类型),因此上面结果是 4,而不是 4.2。如果是严格模式下,只允许传递 int 类型了。(sum('1.2', 3.0) 结果也是 4)

如果希望结果输出 4.2,同时又保持类型约束,怎么办?PHP 不支持方法重载。这就有了 PHP 8 的联合类型。

联合类型接受多个不同的类型做为参数。声明联合类型的语法为 T1|T2|...。

所以,上面代码可以改为:

function sum(int|float $a, int|float $b): int|float{
  return $a + $b;
}

这样 sum(1.2, 3) 的结果就是 4.2 了。

一些注意事项:

  • 联合类型也可用于类成员变量;
  • null 可以用于联合类型中,但不能单独作为类型。比如 int|null 允许,但 null 作为类型不允许;
  • 下面的函数是合法的:function index(): int|false{
    return false;
    }但返回值改为:int|true 却是非法的。这是出于历史原因,很多内部函数在失败时返回了 false 而不是 null。这类函数的典型例子是 strpos()。因此允许联合类型中使用 false,但不允许使用 true。注意 false 并非是类型,这里看出是伪类型,不能单独使用。

还有其他一些细节点,详情请访问官方文档查看:https://www.php.net/manual/zh/language.types.declarations.php。

不得不说,也许越来越意识到弱类型的问题,PHP 这是在做强类型的事情。然而,不少人要说了,搞这么费劲、这么复杂,还不如直接换强类型语言呢?!你觉得呢?

当然,你完全可以忽略联合类型,继续使用 5.x 的方式写 PHP。

03 Named Arguments(命名参数)

这个特性还是很棒的。了解 Python 的朋友应该对这个特性很熟悉。这样一来,PHP 的函数支持不定参数、参数默认值、命名参数等。相对来说,Go 的函数还是弱很多。

比如 htmlspecialchars 函数签名如下:

htmlspecialchars ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = TRUE ]]] ) : string

PHP 8 之前,如果想要最后一个参数传递 false,需要这么调用:

htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);

而有了命名参数后(PHP 8),可以这么调用:

htmlspecialchars($string, double_encode: false);

简单清晰。

总结一下就是:

  • 仅需指定必需的参数,可跳过可选的参数。
  • 参数是与顺序无关的且具有自记录功能。

命名参数确实带来了不少便利。不过我觉得也有一些要注意的点:

  • 函数参数可能会很多,Python 中很多函数一大堆参数,可维护性可能是一个问题;
  • 原本函数参数名称是不重要的,但命名参数使得参数名称不能随便改,因为调用者可能依赖它了;

04 Match 表达式

实际中我们经常通过 state 来表示各种状态,比如:0-待审核;1-上线;2-下线;3-删除。因为数据库中存的数字,但显示希望是文字说明。这时一般有两种做法:

switch ($state) {
  case 0:
    $stateDesc = '待审核';
    break;
  case 1:
    $stateDesc = '上线';
    break;
  case 2:
    $stateDesc = '下线';
    break;
  case 3:
    $stateDesc = '删除';
    break;
}

echo $stateDesc;

我个人喜欢通过 map 来实现:

$stateMap = [
  0 => '待审核',
  1 => '上线',
  2 => '下线',
  3 => '删除',
];

echo $stateMap[$state];

PHP 8 针对这样的场景提供了 match 表达式:

echo match($state) {
  0 => '待审核',
  1 => '上线',
  2 => '下线',
  3 => '删除',
};

可见 match 类似于 switch 语句,有如下特点:

  • Match 是一个表达式,因此其结果可以存储在变量中或返回;
  • Match 分支仅支持单行表达式,不需要 break 语句;
  • switch 相当于使用 == 比较,而 Match 使用 === 比较;
  • 如果没匹配到任何项,会抛 UnhandledMatchError 错误;
  • 也支持 default;

更多信息查看官方文档:https://www.php.net/manual/zh/control-structures.match.php。

05 Nullsafe 运算符(Nullsafe operator)

了解 Swift 之类的语言,应该知晓其中的可选型。PHP 8 新增的这个特性,我觉得多少有点可选型的意思。

在 PHP 7 中的如下代码:

$country =  null;

if ($session !== null) {
  $user = $session->user;

  if ($user !== null) {
    $address = $user->getAddress();
 
    if ($address !== null) {
      $country = $address->country;
    }
  }
}

在 PHP 8 中简化为:

$country = $session?->user?->getAddress()?->country;

06 构造器属性提升

PHP 8 起构造器的参数可以提升为类的属性。构造器的参数赋值给类属性的行为很普遍,否则无法操作。而构造器提升的功能则为这种场景提供了便利。例如下面的代码:

class Point {
  public float $x;
  public float $y;
  public float $z;

  public function __construct(
    float $x = 0.0,
    float $y = 0.0,
    float $z = 0.0
  ) {
    $this->x = $x;
    $this->y = $y;
    $this->z = $z;
  }
}

改为 PHP 8 的方式:

class Point {
  public function __construct(
    public float $x = 0.0,
    public float $y = 0.0,
    public float $z = 0.0,
  ) {}
}

07 字符串与数字的比较更符合逻辑

PHP 8 比较数字字符串(numeric string)时,会按数字进行比较。不是数字字符串时,将数字转化为字符串,按字符串比较。

这一点要注意,之前这样的代码:

0 == 'foobar' // true

现在是 false:

0 == 'foobar' // fals

更多说明参见这里:https://wiki.php.net/rfc/string_to_number_comparison。

08 注解(attributes)

现在可以用 PHP 原生语法来使用结构化的元数据,而非 PHPDoc 声明。

之前这么写:

class PostsController{
    /**
     * @Route("/api/posts/{id}", methods={"GET"})
     */
    public function get($id) { /* ... */ }
}

现在这么写:

class PostsController{
    #[Route("/api/posts/{id}", methods: ["GET"])]
    public function get($id) { /* ... */ }
}

09 即时编译

PHP 8 引入了两个即时编译引擎。Tracing JIT 在两个中更有潜力,它在综合基准测试中显示了三倍的性能, 并在某些长时间运行的程序中显示了 1.5-2 倍的性能改进。典型的应用性能则和 PHP 7.4 不相上下。

官方给了一个性能测试:

10 总结

PHP 8 还有很多其他改动,在这里有详细的说明:https://www.php.net/releases/8.0/zh.php。其中新增了 3 个函数实用的函数:str_contains()、str_starts_with() 和 str_ends_with()。(Go 表示第一天就有了)


这里面的新特性,命名参数我个人还是比较喜欢。你呢?