diff --git "a/_posts/2019-02-23-Restful\350\256\276\350\256\241.md" "b/_posts/2019-02-23-Restful\350\256\276\350\256\241.md" index c30deed..4910f85 100644 --- "a/_posts/2019-02-23-Restful\350\256\276\350\256\241.md" +++ "b/_posts/2019-02-23-Restful\350\256\276\350\256\241.md" @@ -27,7 +27,8 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中 ### 一切皆资源 -- 系统通常有一个用户登录,我们设计为 `/login`, 但是设计为 `POST /sessions ` 就略显别扭,虽然这个在逻辑上是说的通的 +- 系统通常有一个用户登录,我们设计为 `/login`, ~~但是设计为 `POST /sessions ` 就略显别扭,虽然这个在逻辑上是说的通的~~ + - `POST /session` 笔者见过这样的实践 - 播放音乐的接口,理解为创建一个播放状态,可以变更为 `POST /musics/{id}?status=play` , 修改某个音乐的状态为播放 ### 方法动词 @@ -41,15 +42,32 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中 | PUT | 更新 | 修改, 比如 PUT /cars/231, 修改标号为231 的车辆信息 | | DELETE | 删除 | 比如 DELETE /cars/231, 删除编号231 的车辆信息 | -- 多条件查询时, 使用GET方法只能在URL 后面缀上长串的 Key=Value ,显得啰嗦且难于记忆,这时,我们可以创建一个 `POST /cars/queries` 的URL ,表示创建一个查询; 如果设计成异步,可以在该接口返回202 状态码,并在body 里响应一个地址如: `https://domain.com/cars/queries/31421`, -- 使用一级URL 更有助于记忆和理解,但这不是绝对的 +##### GET 和 POST 区别 +- GET只接受ASCII 字符且长度受限, POST 不受限 + - GET 设计时考虑参数可读性,参数的数量 +- GET 请求更加容易被浏览器刷新,要考虑幂等性 +- GET 请求容易被缓存 + +##### 实战建议 +- 路径参数 (spring mvc 中 使用 `@PathVariable` 注解的参数) + - 建议处理可枚举的参数, 因为对监控不太友好, 按路径可能会统计出很多指标; + - 但是比如在一个多租户的系统中, 用来传递租户id,恰好有自然被 监控系统发现并自动分组 + - 关于路径参数中使用id 以符合 restful 风格的问题:在没有副作用的情况下使用,(可能作者当年也没有想到,互联网的数据是一个指数级的增长吧,作者当初也许是设想在一个 MIS 系统中,很自然的传递和查看一些数据吧)。 + +- queryString (spring mvc 中 使用 `@RequestParameter` 注解的参数) + - 参考意见是: 如果一个地址很方便的被 使用者 copy/paste 进行传递,那么那建议 使用 `GET + QueryString` 的组合。比如,在监控系统中, 把查询条件在 queryString 中 传递,方便使用中,传递信息,打开浏览器 接收者便能直接查看到问题所在,而不是按照 对方所说的设置查询参数。 比如 kibana 和 grafana 的 url 通常都是这样很长的。 + - 要注意GET URI 字符受限问题,比如某个资源的翻页查询,通常只有有限的5~6 个参数,不妨使用 `GET + QueryString` 组合。 + - 如果查询条件过于复杂,服务器不能提供响应,不妨改为POST + 202 的异步组合, facebook 的 广告数据接口有类似的设计 ### 使用正确的状态码 -- 比如404, 表示接口找不到呢还是资源接口不存在? 我的理解是,restful接口的实现者,就算没有很好的实践 `HATEOAS` ,但是至少会提供接口文档,所以不会存在调用了一个不存在的接口的情况 +- 比如404, 表示接口找不到呢还是资源接口不存在? + - 如果是被解读为资源不存在,那就是一个业务问题,对用户来说是能理解的,是合情合理的 + - 如果是被解读接口不存在,这就是一个技术问题,是应该消灭在开发阶段的, 而不应该暴露在生产环境的。 + - 综上, restful 原本就是 表示资源的,属于业务范畴,用404 表示资源不存在也是合理的。如果404 被解读成了接口找不到,那就是开发的问题。按照时间线来说,现有接口,后提供资源服务。 -- 状态码会不会不够用?绝大部分情况下,一个接口总是返回一个甚至2个期望的结果,但是出现异常的情况却有很多,所以与其期望明确定义好每一种异常情况,不如做好异常业务状态的提示信息的用户体验 +- 状态码会不会不够用?绝大部分情况下,一个REST API 总是返回一个甚至2个期望的结果,但是出现异常的情况却有很多,所以与其期望明确定义好每一种异常情况,不如做好异常业务状态的提示信息的用户体验 | 状态码 | 说明 | | ------ | ---------- | @@ -59,6 +77,19 @@ Restful[^Rest] 是由 Roy Thomas Fielding[^Thomas] 在2000 年的博士论文中 | 4xx | 错误请求 | | 5xx | 服务器错误 | +### Richardson Maturity Model + +| 级别 | 说明 | +| :---- | :--------- | +| Level 0 | The swarmp of POX| +| Level 1 | Resources | +| Level 2 | HTTP Verbs | +| Level 3 | Hypermedia Controls | + +这个不展开细说,可能我们大部分的API 处于 1~2 或 2~3 的一个中间状态, 尝试了就是最好的。 + +我想说的是,不管restful 风格在实践过程中有多少 争议,既然存在这么一个 非标准的`准则`, 作为开发就应该去布道,思考,并反复实践,抽象我们的业务,使我们的API **尽量**去符合和遵循这样一个东西。 + ## 总结 - 如果你不熟悉Restful, 还是多在各种业务场景下练习抽象思维 diff --git "a/_posts/2021-05-12-\345\272\217\345\210\227\345\214\226\345\217\215\345\272\217\345\210\227\345\214\226\345\270\270\347\224\250\350\256\276\347\275\256.md" "b/_posts/2021-05-12-\345\272\217\345\210\227\345\214\226\345\217\215\345\272\217\345\210\227\345\214\226\345\270\270\347\224\250\350\256\276\347\275\256.md" index 94b9369..907b75f 100644 --- "a/_posts/2021-05-12-\345\272\217\345\210\227\345\214\226\345\217\215\345\272\217\345\210\227\345\214\226\345\270\270\347\224\250\350\256\276\347\275\256.md" +++ "b/_posts/2021-05-12-\345\272\217\345\210\227\345\214\226\345\217\215\345\272\217\345\210\227\345\214\226\345\270\270\347\224\250\350\256\276\347\275\256.md" @@ -100,5 +100,15 @@ mybatis.configuration.default-enum-type-handler=org.apache.ibatis.type.EnumOrdin - 枚举类型会被序列化为Ordinal 存储 +## 关于枚举使用的一点建议 + +- 虽然框架都提供了 使用 ordinal 反序列化的设置, 实际使用中会出现 几个问题 + - 枚举的 ordinal 和 枚举的顺序有关, 会被代码格式等工具破坏 + - `java.lang.Enum` 默认不提供 `valueOf(int ordinal)` 方法, 在三方框架不提供适配时, 使用成本高, 需要手动封装 + - 在提供给三方使用的SDK 或者jar 包里使用 枚举 更合适, 源代码不会被破坏 + + + + diff --git "a/_posts/2021-10-20-\346\211\253\347\240\201\347\231\273\351\231\206\345\256\236\346\210\230\346\226\271\346\241\210.md" "b/_posts/2021-10-20-\346\211\253\347\240\201\347\231\273\351\231\206\345\256\236\346\210\230\346\226\271\346\241\210.md" new file mode 100644 index 0000000..acb7d82 --- /dev/null +++ "b/_posts/2021-10-20-\346\211\253\347\240\201\347\231\273\351\231\206\345\256\236\346\210\230\346\226\271\346\241\210.md" @@ -0,0 +1,91 @@ +# 扫码登陆实战方案 + +## 需求 + +- 用户 使用手机扫描 浏览器登录页的二维码 +- 用户手机上显示 用户许可页面,同时浏览器显示扫码成功,处于等待确认状态 +- 用户在手机上确认登陆,浏览器显示确认成功并跳转登陆后页面 + +## 分析 + +1. 浏览器请求服务器 并生成二维码,显然二维码内含有一些信息能被对应的手机客户端识别 + - 二维码内的信息不能被其他软件识别,否则的话,浏览器会被其他人的客户端识别并发起登陆请求 + - 二维码被扫描后不能再次被识别,有可能被其他账号扫描并登陆 +2. 浏览器二维码被展示出来后,需要实时监听到二维码的消费情况。 + - 浏览器轮询显然是个不错的选择,但是我们可以使用**`Server Sent Event` **会是更好的选择 +3. 二维码在一定时间内没有被消费,应当过期处理 + - 断网从连不能影响消息的消费 + +## 代码实战 + +### 浏览器 获取二维码等待响应 + +```java + @GetMapping("/qr-code/{client_id}") + public ResponseEntity QRLogin(@PathVariable("client_id") String clientId) { + String authcode = UUID.randomUUID().toString(); + SseEmitter sseEmitter = new SseEmitter(90 * 1000L); //90s 内没有完成 SSE会超时 + String id = clientId + '@' + authcode; + BlockingQueue queue = redisson.getBlockingQueue("SSE_QUE:" + id); // 生成通道 + localCachedMap.put(id, true); + + threadPoolTaskExecutor.submit(() -> { + Map body = new HashMap<>(); + body.put("clientId", clientId); + body.put("authCode", authcode); + body.put("redirectUrl", "immigrant.com/login/qr"); + + try { + sseEmitter.send(body, MediaType.APPLICATION_JSON); + while (true) { + String s = queue.take(); + log.info(s); + if ("END".equals(s)) { + localCachedMap.put(id, false); //完成后会关闭通道, 二维码泄露也不会发送消息到服务端 + sseEmitter.complete(); + } else { + sseEmitter.send(s); + } + } + + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + } + + + }); + // SSE 链接销毁时,要设置通道关闭 + sseEmitter.onCompletion(() -> { + queue.clear(); + log.info("sse {} completed", id); + }); + + + return ResponseEntity.ok(sseEmitter); +``` + + +### 客户端扫描二维码并发送请求 + +```java + @PostMapping("/qr-code/{client_id}") + public ResponseEntity QRLogin(@PathVariable("client_id") String clientId,@RequestBody ScanRequest request) { + BlockingQueue queue; + if (Boolean.TRUE.equals(localCachedMap.get(clientId))) { + queue = redisson.getBlockingQueue("SSE_QUE:" + clientId); + } else { + return new ResponseEntity<>(HttpStatus.GONE); + } + + try { + queue.offer(objectMapper.writeValueAsString(scanRequest)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + return ResponseEntity.noContent().build(); + } +``` + + +