从上面这些情况来看,有两个错误会返回 422 ,不过他们的原因是不同的。这就是为什么我们需要一个错误码,甚至是一个错误描述。要区分代码和描述,我打算将 error (代码)作为机器可识别的常量,将 description 作为可更改的用于人类识别的字符串。
字段校验错误
对于字段的错误,可以这样返回:
POST /v1/register // 请求 { "email": "end@@user.comx" "password": "abc" } // 响应 - 422 { "error": { "status": 422, "error": "FIELDS_VALIDATION_ERROR", "description": "One or more fields raised validation errors." "fields": { "email": "Invalid email address.", "password": "Password too short." } } }操作校验错误
对于返回操作校验错误:
POST /v1/register // 请求 { "email": "end@user.com", "password": "password" } // 响应 - 409 { "error": { "status": 409, "error": "EMAIL_ALREADY_EXISTS", "description": "An account already exists with this email." } }这样,你的程序的错误提取逻辑要当心非200的错误了,你可以直接从响应中检查 error 字段,然后将其与客户端中相应的逻辑进行比较。
status 这个字段似乎也很有用,如果你不想检查响应里的元数据,那你可以在需要的时候有条件地添加这个字段。
description 可作为备用的用户可读的错误消息。
密码规则在做了很多密码规则的研究之后,我比较赞同 密码规则是废话 和 NIST禁止做的事情 这两篇帖子的观点。
整理了一些处理密码的规则:
在某种程度上,所有这些规则能使密码验证更容易!
使用访问和刷新令牌现代的无状态、RESTful API一般会使用令牌来实现身份认证。这消除了在无状态服务器上处理会话和Cookie的需要,并且可以很容易地使用 Authorization 头(或 access_token 查询参数)来调试网络请求。
访问令牌用于认证所有未来的API请求,生命期短,不会被取消。
刷新令牌在初始登录的响应中返回,然后跟过期时间戳和与使用者的关系一起进行散列计算后存储到数据库中。这个长生命期的像密码一样的密钥,可以被用来请求新的短生命期的JWT访问令牌。刷新令牌也可以用于续订并延长其使用寿命,这意味着如果用户持续使用该服务,则无需再次登录。
但是,如果API希望签订一个不同的“密钥”,JWT就会被取消,但是这将使所有当前发出的令牌全部无效,但因为这些令牌是短生命期的,所以这并没有关系。
登录
在我的程序实现中,正常的登录过程如下所示:
续订令牌
正常的续订验证流程如下所示:
验证令牌
通过检查到期日期和签名哈希可以校验JWT访问令牌的有效性。如果校验失败,则认为是一个无效的令牌。
如果验证通过,则JWT的有效载荷中包含了一个 uid ,它用于在API响应的上下文中传递一个对应的 user 对象来检查权限/角色,并相应地创建/读取/更新/删除数据。
终止会话
由于刷新令牌存储在数据库中,因此可以将其删除来“终止会话”。这为用户提供了一个控制方法,即他们可以通过主动的刷新令牌“会话”来保护自己的帐户,并且通过这种方法来进行多次重复认证(通过调整超时时间戳来实现)。
让JWT保持小巧在把信息序列化到JWT访问令牌中时,请尽可能地让这个信息小巧,身份验证令牌的生命期不需要很长,因此没必要。如果可以的话,只序列化用户的 uid (id)就可以了,其余的可以通过“GET /me”来传递。
还值得注意的是,存储在JWT有效载荷中的任何敏感信息并不安全,因为它只是一个经过base64编码的字符串。
使用根级别的“Me”端点(URL)一般人会使用 /profile 这个URL来提供自身的基本属性。但是,我也看到过比较混论的实现,例如对于 /users/:id 这种接受整数的URL,它竟然允许传入字符串 me 来指向自身的属性。
通过 /me 访问自身信息的更深层次的URL,例如 /me 的 /settings 或者 /billing 信息,而通过 users/:id/billing 访问其他用户的信息。
// 不推荐 GET /v1/users/me // 推荐,因为更短,没有把整数和字符串混在一起 GET /v1/me 避免对嵌套路由的操作有一个采用了以上一些设计理念的重构的项目,最后却设计出了一个难用的URL系统:
// 一个长长的URL PATCH /v1/projects/:id/collections/:id/items/:id/attachments如果要POST上传一个附件,这个URL可能看起来还行,但是如果在开发客户端应用程序时想要实现像对附件标星号这么一个简单操作的功能的话,那你就需要重写相关的代码。相关代码如下:
const apiRoot = 'https://api.myservice.com/v1' const starAttachment = (projectId, collectionId, itemId, attachmentId, starred) => { fetch( `${apiRoot}/projects/${projectId}/collections/${collectionId}/items/${itemId}/attachments/${attachmentId}`, { method: 'PATCH', body: JSON.stringify({ starred }), // ... } }助手函数的代码如下:
import { starAttachment } from './actions/attachments.js' class MyComponent extends React.Component { doStarAttachment = (id, starred) => { // now all the "boilerplate" for starring the attachment const { projectId, collectionsId, itemId } = this.props.entities.attachments[id] // now actually plugging in all that information starAttachment(projectId, collectionId, itemId, id, starred) } // ... }如果你把获取附件属性这个功能委派给服务器来实现,并且只使用根级别的URL,这样不是更好吗?
const apiRoot = 'https://api.myservice.com/v1' const starAttachment = (id, starred) => { fetch( `${apiRoot}/attachments/${id}`, { method: 'PATCH', body: JSON.stringify({ starred }), // ... } } import { starAttachment } from './actions/attachments.js' class MyComponent extends React.Component { doStarAttachment = (id, starred) => { // simple as, and you could even easily call it from a gallery-like list starAttachment(id, starred) } // ... }总的来说,我认为这两种方法各有各的优势,而我倾向于用一个 长的路径来创建/提取 资源,用一个 短的路径来更新/删除 资源。
提供分页功能分页很重要,因为你不会想让一个简单的请求就获得数千行的记录。这个问题似乎很明显,但是还是会有许多人忽略这个功能。
有多种方法来实现分页:
“From”参数
可以说这是最容易实现的,API接受一个 from 查询字符串参数,然后从这个偏移量开始返回有限数量的结果(通常返回20个结果)。
另外最好提供一个limit参数来限制最大记录数,例如Twitter,最大限制为1000,而默认限制为200。
“下一页”令牌