关于rest-api:MobTech-短信验证REST-API

前言本接口属于SMSSDK短信平台的凋谢验证服务,不提供短信发送服务,次要是承当验证APP 应用SMSSDK发送的短信验证码,应用该接口来验证验证码是否失常。 接口应用开明服务端验证开关第一步:开明服务端验证开关 在开发者后盾创立利用后开启SMSSDK,并配置服务器白名单 申请接口验证短信申请地址为:https://webapi.sms.mob.com/sm... 申请形式: POST 申请参数 返回后果 {status:200} 测试脚本 curl -d 'appkey=xxxx&phone=132****8362&zone=86&code=xxxx' 'https://webapi.sms.mob.com/sms/verify'样例代码注:仅供参考 php 请看 // 配置项$api = '接口地址(例:https://webapi.sms.mob.com);$appkey = '您的appkey';// 发送验证码$response = postRequest( $api . '/sms/verify', array( 'appkey' => $appkey, 'phone' => '152xxxx4345', 'zone' => '86', 'code' => '1234',) );/** * 发动一个post申请到指定接口 * * @param string $api 申请的接口 * @param array $params post参数 * @param int $timeout 超时工夫 * @return string 申请后果 */function postRequest( $api, array $params = array(), $timeout = 30 ) { $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $api ); // 以返回的模式接管信息 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); // 设置为POST形式 curl_setopt( $ch, CURLOPT_POST, 1 ); curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $params ) ); // 不验证https证书 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 ); curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 0 ); curl_setopt( $ch, CURLOPT_TIMEOUT, $timeout ); curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', 'Accept: application/json', ) ); // 发送数据 $response = curl_exec( $ch ); // 不要遗记开释资源 curl_close( $ch ); return $response;}C#开发者请看 ...

September 1, 2022 · 3 min · jiezi

关于rest-api:Sureness-104-发布-面向-REST-API-的高性能认证鉴权框架

Sureness 是一个面向REST API的高性能认证鉴权框架,致力于治理爱护API平安 Gitee Github Features公布了全新的官网和文档 #106 地址: https://usthe.com/sureness 或 https://su.usthe.com , 心愿能喜爱,欢送一起欠缺或给点意见。 提供了Sureness对国产框架Jfinal的认证鉴权反对爱护样例,#93 参见: Jfinal-Sureness-Demo 提供了Sureness对国产框架Solon的认证鉴权反对爱护样例和适配器,这里感激 @刘西东 大佬奉献 参见: Solon-Sureness-Demo Sureness原生适宜在网关层面进行API的认证鉴权爱护,这里咱们也提供了两个网关的反对样例,供大家参考极速集成进去。 对Spring gateway 网关进行API粒度的认证鉴权爱护的样例,参见: Spring Gateway-Sureness Demo 对Netfix Zuul 网关进行API粒度的认证鉴权爱护的样例,参见: Netflix Zuul-Sureness Demo 更多网关(shenyu)的反对咱们也在打算中。 remove unnecessary clearTree() in function rebuildTree (#111) BugFixsamples fix information exposure through a stack trace bug (#113)Fix spring-gateway-sureness running error (#118)docs update: step-by-step bugfix (#104)Bump prismjs from 1.23.0 to 1.24.0 in /home (#119)Bump normalize-url from 4.5.0 to 4.5.1 in /home (#110)Bump dns-packet from 1.3.1 to 1.3.4 in /home (#108)Bump ws from 6.2.1 to 6.2.2 in /home (#107) ...

July 9, 2021 · 1 min · jiezi

yii2-的-restful-api-路由实例

yii\rest\UrlRule使用yii\rest\UrlRule来自动映射控制器的 restful 路由,简单快捷,缺点是必须得按规定好的方法名去写业务。 映射的规则如下,当然,你可以修改源码为你的习惯: public $patterns = [ 'PUT,PATCH {id}' => 'update', 'DELETE {id}' => 'delete', 'GET,HEAD {id}' => 'view', 'POST' => 'create', 'GET,HEAD' => 'index', '{id}' => 'options', '' => 'options',];除了被限制了HTTP动词对应的方法名外,其他都很好用,比如pluralize是多么的优雅啊,可以自动解析单词的复数,laravel的话要一个个的去写,反而有些不方便了 'urlManager' => [ 'enablePrettyUrl' => true, 'showScriptName' => false, 'enableStrictParsing' => true, 'rules' => [ [ 'class' => 'yii\rest\UrlRule', 'controller' => [ 'v1/user', 'v1/news', 'routeAlias' => 'v1/box' ], 'pluralize' => true ], ]]自定义路由注意我路由里很刻意的用了复数模式,但很鸡肋,因为一些单词的复数并不是简单的加个 s 就可以了。 ...

May 13, 2019 · 1 min · jiezi

第三讲使用JUnit对Spring-Boot中的Rest-Controller进行单元测试

(第三讲)使用JUnit对Spring Boot中的Rest Controller进行单元测试本次教程主要讲解如何对Spring Boot中的Rest Service进行单元测试。以往我们主要是使用JUnit对业务层进行单元测试,本次课程将使用一个简单的案例来说明如何使用JUnit对Spring Boot的Rest Service进行单元测试。1. 主要类容快速搭建Restfull Service 环境创建GET请求以检索用户信息创建GET请求检索用户角色信息创建POST请求新增用户角色信息如何使用PostMan请求Restfull Service使用JUnit对GET请求进行单元测试使用JUnit对POST请求进行单元测试2. 你将需要准备的工具JDK 1.8及以上版本Maven 3.0及以上版本的项目构建工具IDEA代码编辑器3. 你可以通过以下的地址获取本次课程的所有示例代码项目代码已经上传到GitHub仓库中,你可以通过以下的地址获取示例源码:https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-junit-rest-service 4. 项目结构下面通过一张截图来了解以下本次课程中我们使用到的项目结构。 首先我们需要位单元测试提供一个可用的Rest Controller。UserController文件为我们提供了一个可用于测试的Rest Controller。在UserController类中,我们提供两种请求类型的方法,一种是GET请求,另一种是POST请求。然后我们为这两种请求方式的方法编写单元测试用例。 在接下来的测试过程中,我们将使用Mockito来模拟请求UserService的过程,使用MockMvc来模拟请求UserController。单元测试的目的是将测试范围尽可能的缩小。在本次案例中,我们仅对UserController中的方法进行测试。 5. 初始化项目我们依然使用Spring Initializr来初始化本次课程的项目,你需要配置如下图中的参数: 现在我们需要提供两个实体类:User和Role: User.java Role.java 6. 提供可用的业务服务所有的应用都需要有数据的存储,本次课程主要的重点是为了Rest Controller的单元测试,因此使用ArrayList来充当数据库的角色。在案例中,一个用户可以有多个角色,一个角色也可以被赋予给多个用户。用户有ID,名字,别名和角色列表,角色具有ID,名称和描述。在UserService类中,将提供如图所示的公共方法。 7. 提供GET请求方法在UserController类中,我们将提供如下几个公开的GET请求方法: @GetMapping(value="/users") : 获取所有的用户信息@GetMapping(value="/users/{id}/roles") : 根据用户ID获取该用户的所有角色信息UserController.java类中的详细代码如下: package com.ramostear.spring.boot.test.restservice.controller;import com.ramostear.spring.boot.test.restservice.model.Role;import com.ramostear.spring.boot.test.restservice.model.User;import com.ramostear.spring.boot.test.restservice.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import java.util.List;@RestControllerpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } @GetMapping(value = "/users") public List<User> findAllStudents(){ return userService.findAllUsers(); } @GetMapping(value = "/users/{id}/roles") public List<Role> findUserRoles(@PathVariable(value = "id")String id){ return userService.findUserAllRoles(id); }}8. 使用Postman对RestController进行测试我们将使用Postman工具对上述两个Rest API进行请求,首先向Postman地址栏输入http://localhost:8080/users 进行测试,获得的响应信息如下: ...

May 10, 2019 · 2 min · jiezi

Graphql实战系列(下)

前情介绍在《Graphql实战系列(上)》中我们已经完成技术选型,并将graphql桥接到凝胶gels项目中,并动态手写了schema,可以通过 http://localhost:5000/graphql 查看效果。这一节,我们根据数据库表来自动生成基本的查询与更新schema,并能方便的扩展schema,实现我们想来的业务逻辑。设计思路对象定义在apollo-server中是用字符串来做的,而Query与Mutation只能有一个,而我们的定义又会分散在多个文件中,因此只能先以一定的形式把它们存入数组中,在生成schema前一刻再组合。业务逻辑模块模板设计:const customDefs = { textDefs: type ReviseResult { id: Int affectedRows: Int status: Int message: String }, queryDefs: [], mutationDefs: []}const customResolvers = { Query: { }, Mutation: { } }export { customDefs, customResolvers }schema合并算法let typeDefs = [] let dirGraphql = requireDir('../../graphql') //从手写schema业务模块目录读入文件 G.L.each(dirGraphql, (item, name) =&gt; { if (item &amp;&amp; item.customDefs &amp;&amp; item.customResolvers) { typeDefs.push(item.customDefs.textDefs || '') //合并文本对象定义 typeDefObj.query = typeDefObj.query.concat(item.customDefs.queryDefs || []) //合并Query typeDefObj.mutation = typeDefObj.mutation.concat(item.customDefs.mutationDefs || []) //合并Matation let { Query, Mutation, ...Other } = item.customResolvers Object.assign(resolvers.Query, Query) //合并resolvers.Query Object.assign(resolvers.Mutation, Mutation) //合并resolvers.Mutation Object.assign(resolvers, Other) //合并其它resolvers } }) //将query与matation查询更新对象由自定义的数组转化成为文本形式 typeDefs.push(Object.entries(typeDefObj).reduce((total, cur) =&gt; { return total += type ${G.tools.bigCamelCase(cur[0])} { ${cur[1].join(’’)} } }, ''))从数据库表动态生成schema自动生成内容:一个表一个对象;每个表有两个Query,一是单条查询,二是列表查询;三个Mutation,一是新增,二是更新,三是删除;关联表以上篇中的Book与Author为例,Book中有author_id,会生成一个Author对象;而Author表中会生成一个对象列表[Book]mysql类型 =&gt; graphql 类型转化常量定义定义一类型转换,不在定义中的默认为String。const TYPEFROMMYSQLTOGRAPHQL = { int: 'Int', smallint: 'Int', tinyint: 'Int', bigint: 'Int', double: 'Float', float: 'Float', decimal: 'Float',}从数据库中读取数据表信息 let dao = new BaseDao() let tables = await dao.querySql('select TABLE_NAME,TABLE_COMMENT from information_schema.TABLES' + ' where TABLE_SCHEMA = ? and TABLE_TYPE = ? and substr(TABLE_NAME,1,2) &lt;&gt; ? order by ?', [G.CONFIGS.dbconfig.db_name, 'BASE TABLE', 't_', 'TABLE_NAME'])从数据库中读取表字段信息tables.data.forEach((table) =&gt; { columnRs.push(dao.querySql('SELECT COLUMNS.COLUMN_NAME,COLUMNS.COLUMN_TYPE,COLUMNS.IS_NULLABLE,' + 'COLUMNS.CHARACTER_SET_NAME,COLUMNS.COLUMN_DEFAULT,COLUMNS.EXTRA,' + 'COLUMNS.COLUMN_KEY,COLUMNS.COLUMN_COMMENT,STATISTICS.TABLE_NAME,' + 'STATISTICS.INDEX_NAME,STATISTICS.SEQ_IN_INDEX,STATISTICS.NON_UNIQUE,' + 'COLUMNS.COLLATION_NAME ' + 'FROM information_schema.COLUMNS ' + 'LEFT JOIN information_schema.STATISTICS ON ' + 'information_schema.COLUMNS.TABLE_NAME = STATISTICS.TABLE_NAME ' + 'AND information_schema.COLUMNS.COLUMN_NAME = information_schema.STATISTICS.COLUMN_NAME ' + 'AND information_schema.STATISTICS.table_schema = ? ' + 'where information_schema.COLUMNS.TABLE_NAME = ? and COLUMNS.table_schema = ?', [G.CONFIGS.dbconfig.db_name, table.TABLE_NAME, G.CONFIGS.dbconfig.db_name])) })几个工具函数取数据库表字段类型,去除圆括号与长度信息 getStartTillBracket(str: string) { return str.indexOf('(') &gt; -1 ? str.substr(0, str.indexOf('(')) : str }下划线分隔的表字段转化为big camel-case bigCamelCase(str: string) { return str.split('_').map((al) =&gt; { if (al.length &gt; 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') }下划线分隔的表字段转化为small camel-case smallCamelCase(str: string) { let strs = str.split('_') if (strs.length &lt; 2) { return str } else { let tail = strs.slice(1).map((al) =&gt; { if (al.length &gt; 0) { return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase() } return al }).join('') return strs[0] + tail } }字段是否以_id结尾,是表关联的标志不以_id结尾,是正常字段,判断是否为null,处理必填typeDefObj[table].unshift(${col[‘COLUMN_NAME’]}: ${typeStr}${col[‘IS_NULLABLE’] === ‘NO’ ? ‘!’ : ‘’}\n)以_id结尾,则需要处理关联关系 //Book表以author_id关联单个Author实体 typeDefObj[table].unshift(“““关联的实体””” ${G.L.trimEnd(col[‘COLUMN_NAME’], ‘_id’)}: ${G.tools.bigCamelCase(G.L.trimEnd(col[‘COLUMN_NAME’], ‘id’))}) resolvers[G.tools.bigCamelCase(table)] = { [G.L.trimEnd(col['COLUMN_NAME'], '_id')]: async (element) =&gt; { let rs = await new BaseDao(G.L.trimEnd(col['COLUMN_NAME'], '_id')).retrieve({ id: element[col['COLUMN_NAME']] }) return rs.data[0] } } //Author表关联Book列表 let fTable = G.L.trimEnd(col['COLUMN_NAME'], '_id') if (!typeDefObj[fTable]) { typeDefObj[fTable] = [] } if (typeDefObj[fTable].length &gt;= 2) typeDefObj[fTable].splice(typeDefObj[fTable].length - 2, 0, “““关联实体集合””"${table}s: [${G.tools.bigCamelCase(table)}]\n) else typeDefObj[fTable].push(${table}s: [${G.tools.bigCamelCase(table)}]\n) resolvers[G.tools.bigCamelCase(fTable)] = { [${table}s]: async (element) =&gt; { let rs = await new BaseDao(table).retrieve({ [col['COLUMN_NAME']]: element.id}) return rs.data } }生成Query查询单条查询 if (paramId.length &gt; 0) { typeDefObj['query'].push(${G.tools.smallCamelCase(table)}(${paramId}!): ${G.tools.bigCamelCase(table)}\n) resolvers.Query[${G.tools.smallCamelCase(table)}] = async (_, { id }) =&gt; { let rs = await new BaseDao(table).retrieve({ id }) return rs.data[0] } } else { G.logger.error(Table [${table}] must have id field.) }列表查询 let complex = table.endsWith('s') ? (table.substr(0, table.length - 1) + 'z') : (table + 's') typeDefObj['query'].push(${G.tools.smallCamelCase(complex)}(${paramStr.join(’, ‘)}): [${G.tools.bigCamelCase(table)}]\n) resolvers.Query[${G.tools.smallCamelCase(complex)}] = async (_, args) =&gt; { let rs = await new BaseDao(table).retrieve(args) return rs.data }生成Mutation查询 typeDefObj['mutation'].push( create${G.tools.bigCamelCase(table)}(${paramForMutation.slice(1).join(’, ‘)}):ReviseResult update${G.tools.bigCamelCase(table)}(${paramForMutation.join(’, ‘)}):ReviseResult delete${G.tools.bigCamelCase(table)}(${paramId}!):ReviseResult ) resolvers.Mutation[create${G.tools.bigCamelCase(table)}] = async (_, args) =&gt; { let rs = await new BaseDao(table).create(args) return rs } resolvers.Mutation[update${G.tools.bigCamelCase(table)}] = async (_, args) =&gt; { let rs = await new BaseDao(table).update(args) return rs } resolvers.Mutation[delete${G.tools.bigCamelCase(table)}`] = async (, { id }) => { let rs = await new BaseDao(table).delete({ id }) return rs }项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结我只能把大概思路写出来,让大家有个整体的概念,若想很好的理解,得自己把项目跑起来,根据我提供的思想,慢慢的去理解。因为我在编写的过程中还是遇到了不少的难点,这块既要自动化,还要能方便的接受手动编写的schema模块,的确有点难度。 ...

April 13, 2019 · 3 min · jiezi

Graphql实战系列(上)

背景介绍graphql越来越流行,一直想把我的凝胶项目除了支持restful api外,也能同时支持graphql。由于该项目的特点是结合关系数据库的优点,尽量少写重复或雷同的代码。对于rest api,在做完数据库设计后,百分之六十到八十的接口就已经完成了,但还需要配置上api文档。而基于数据库表自动实现graphql,感觉还是有难度的,但若能做好,连文档也就同时提供了。不久前又看到了一句让我深以为然的话:No program is perfect, even the most talented engineers will write a bug or two (or three). By far the best design pattern available is simply writing less code. That’s the opportunity we have today, to accomplish our goals by doing less. so, ready go…基本需求与约定根据数据库表自动生成schema充分利用已经有的支持restful api的底层接口能自动实现一对多的表关系能方便的增加特殊业务,只需要象rest一样,只需在指定目录,增加业务模块即可测试表有两个,book & authorbook表字段有:id, title, author_idauthor表字段有: id, name数据表必须有字段id,类型为整数(自增)或8位字符串(uuid),作为主键或建立unique索引表名为小写字母,使用名词单数,以下划作为单词分隔表关联自动在相关中嵌入相关对象,Book对象增加Author对象,Author对象增加books列表每个表会默认生成两个query,一个是以id为参数进行单条查询,另一个是列表查询;命名规则;单条查询与表名相同,列表查询为表名+s,若表名本身以s结尾,则变s为z桥接库比较与选择我需要在koa2上接入graphql,经过查阅资料,最后聚焦在下面两个库上:kao-graphqlapollo-server-koakao-graphql实现开始是考虑简单为上,试着用kao-graphql,作为中间件可以方便的接入,我指定了/gql路由,可以测试效果,代码如下:import * as Router from ‘koa-router’import BaseDao from ‘../db/baseDao’import { GraphQLString, GraphQLObjectType, GraphQLSchema, GraphQLList, GraphQLInt } from ‘graphql’const graphqlHTTP = require(‘koa-graphql’)let router = new Router()export default (() => { let authorType = new GraphQLObjectType({ name: ‘Author’, fields: { id: { type: GraphQLInt}, name: { type: GraphQLString} } }) let bookType = new GraphQLObjectType({ name: ‘Book’, fields: { id: { type: GraphQLInt}, title: { type: GraphQLString}, author: { type: authorType, resolve: async (book, args) => { let rs = await new BaseDao(‘author’).retrieve({id: book.author_id}) return rs.data[0] } } } }) let queryType = new GraphQLObjectType({ name: ‘Query’, fields: { books: { type: new GraphQLList(bookType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, title: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data } }, authors: { type: new GraphQLList(authorType), args: { id: { type: GraphQLString }, search: { type: GraphQLString }, name: { type: GraphQLString }, }, resolve: async function (, args) { let rs = await new BaseDao(‘author’).retrieve(args) return rs.data } } } }) let schema = new GraphQLSchema({ query: queryType }) return router.all(’/gql’, graphqlHTTP({ schema: schema, graphiql: true }))})() 这种方式有个问题,前面的变量对象中要引入后面定义的变量对象会出问题,因此投入了apollo-server。但apollo-server 2.0网上资料少,大多是介绍1.0的,而2.0变动又比较大,因此折腾了一段时间,还是要多看英文资料。apollo-server 2.0集成很多东西到里面,包括cors,bodyParse,graphql-tools 等。apollo-server 2.0实现静态schema通过中间件加载,放到rest路由之前,加入顺序及方式请看app.ts,apollo-server-kao接入代码://自动生成数据库表的基础schema,并合并了手写的业务模块import { getInfoFromSql } from ‘./schema_generate’const { ApolloServer } = require(‘apollo-server-koa’)export default async (app) => { //app是koa实例 let { typeDefs, resolvers } = await getInfoFromSql() //数据库查询是异步的,所以导出的是promise函数 if (!G.ApolloServer) { G.ApolloServer = new ApolloServer({ typeDefs, //已经不需要graphql-tools,ApolloServer构造函数已经集成其功能 resolvers, context: ({ ctx }) => ({ //传递ctx等信息,主要供认证、授权使用 …ctx, …app.context }) }) } G.ApolloServer.applyMiddleware({ app })}静态schema试验,schema_generate.tsconst typeDefs = type Author { id: Int! name: String books: [book] } type Book { id: Int! title: String author: Author } # the schema allows the following query: type Query { books: [Post] author(id: Int!): Author }const resolvers = { Query: { books: async function (, args) { let rs = await new BaseDao(‘book’).retrieve(args) return rs.data }, author: async function (, { id }) { let rs = await new BaseDao(‘author’).retrieve({id}) return rs.data[0] }, }, Author: { books: async function (author) { let rs = await new BaseDao(‘book’).retrieve({ author_id: author.id }) return rs.data }, }, Book: { author: async function (book) { let rs = await new BaseDao(‘author’).retrieve({ id: book.author_id }) return rs.data[0] }, },}export { typeDefs, resolvers}项目地址https://github.com/zhoutk/gels使用方法git clone https://github.com/zhoutk/gelscd gelsyarntsc -wnodemon dist/index.js然后就可以用浏览器打开链接:http://localhost:5000/graphql 查看效果了。小结这是第一部分,确定需求,进行了技术选型,实现了接入静态手写schema试验,下篇将实现动态生成与合并特殊业务模型。 ...

April 11, 2019 · 2 min · jiezi

实战:基于Spring Boot快速开发RESTful风格API接口

写在前面的话这篇文章计划是在过年期间完成的,示例代码都写好了,结果亲戚来我家做客,文章没来得及写。已经很久没有更新文章了,小伙伴们,有没有想我啊。言归正传,下面开始,今天的话题。目标写一套符合规范,并且具有RESTful风格的API接口。假定你已会使用Spring Boot 2.x。你已会使用Gradle构建Spring Boot工程。你已会基于Spring Boot编写API接口。你已会使用接口调试工具。如果你还不会使用Spring Boot写接口,建议先看一下这篇文章 :用Spring Boot开发API接口步骤1、基于Gradle构建Spring Boot示例项目。2、引入JavaLib。3、编写接口代码。4、测试接口。引入JavaLib测试版(SNAPSHOT),都会发布到 JitPack 上,所以,从这里拉取的,都会是最新的,但是需要配置仓库地址。正式版(RELEASE),才会推送到 Maven中央。UserModel我们用UserModel来存放我们的数据,以便存取。我个人比较喜欢用bean的,如果你喜欢用Map,那也是可以的。不过需要注意的是,需要加@JsonInclude(JsonInclude.Include.NON_NULL) ,他的作用是,如果某个字段为空时,在返回的JSON中,则不显示,如果没有,将为 null。完整代码如下:package com.fengwenyi.demojavalibresult.model;import com.fasterxml.jackson.annotation.JsonInclude;import lombok.Data;import lombok.experimental.Accessors;import java.io.Serializable;/** * User Model * @author Wenyi Feng * @since 2019-02-05 /@Data@Accessors(chain = true)@JsonInclude(JsonInclude.Include.NON_NULL)public class UserModel implements Serializable { private static final long serialVersionUID = -835481508750383832L; /* UID / private String uid; /* Name / private String name; /* Age / private Integer age;}编写接口返回码这里我们使用 JavaLib 中result模块为我们提供的方法。只需要调用 BaseCodeMsg.app(Integer, String)即可。这里我们只写几个用作示例,完整代码如下:package com.fengwenyi.demojavalibresult.util;import com.fengwenyi.javalib.result.BaseCodeMsg;/* * 自定义返回码以及描述信息 * @author Wenyi Feng * @since 2019-02-05 /public class CodeMsg { / user error ————————————————————————————————————*/ /** 用户不存在 / public static final BaseCodeMsg ERROR_USER_NOT_EXIST = BaseCodeMsg.app(10001, “User Not Exist”); /* UID不能为空 / public static final BaseCodeMsg ERROR_USER_UID_NOT_NULL = BaseCodeMsg.app(10002, “User UID Must Not null”);}BaseCodeMsg我们看一下源码:package com.fengwenyi.javalib.result;/* * (基类)返回码及描述信息 * @author Wenyi Feng * @since 2019-01-22 /public class BaseCodeMsg { /* 返回码 / private Integer code; /* 返回码描述 / private String msg; /* * 无参数构造方法 / private BaseCodeMsg() {} /* * 构造方法 * @param code * @param msg / private BaseCodeMsg(Integer code, String msg) { this.code = code; this.msg = msg; } public static BaseCodeMsg app(Integer code, String msg) { return new BaseCodeMsg(code, msg); } /* * 返回码填充 * @param args 填充内容 * @return CodeMsgEnum / public BaseCodeMsg fillArgs(Object … args) { this.msg = String.format(this.msg, args); return this; } /* * 获取返回码 * @return 返回码 / public Integer getCode() { return code; } /* * 获取描述信息 * @return 描述信息 / public String getMsg() { return msg; } /* 成功 / public static final BaseCodeMsg SUCCESS = BaseCodeMsg.app(0, “Success”); /* 失败 / public static final BaseCodeMsg ERROR_INIT = BaseCodeMsg.app(-1, “Error”);}成功的标识是:当 code=0 时。另外,我们还为你提供了预留字符串替换的方法。比如你想告诉用户某个字段不合法,那么你可以这样:第一步:在CodeMsg中添加public static final BaseCodeMsg ERROR_PARAM_ILLEGAL = BaseCodeMsg.app(20001, “Request Param Illegal : %s”);第二步:返回 /* * 测试参数错误 * @return {@link Result} / @GetMapping("/test-param-error") public Result testParamError() { return Result.error(CodeMsg.ERROR_PARAM_ILLEGAL.fillArgs(“account”)); }测试结果:编写接口代码接下来,开始编写我们的接口代码。首先指明,我们的接口接收和返回的文档格式。consumes = MediaType.APPLICATION_JSON_UTF8_VALUEproduces = MediaType.APPLICATION_JSON_UTF8_VALUE再使用 JavaLib 中 Result。完整代码如下:package com.fengwenyi.demojavalibresult.controller;import com.fengwenyi.demojavalibresult.model.UserModel;import com.fengwenyi.demojavalibresult.util.CodeMsg;import com.fengwenyi.javalib.result.Result;import org.springframework.http.MediaType;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.;import javax.annotation.PostConstruct;import java.util.ArrayList;import java.util.List;import java.util.UUID;/** * User Controller : 用户操作 * @author Wenyi Feng * @since 2019-02-05 /@RestController@RequestMapping(value = “/user”, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public class UserController { /* 临时存放用户信息 / private List<UserModel> userModelList = new ArrayList<>(); /* * 初始化用户 / @PostConstruct public void init() { for (int i = 0; i < 10; i++) userModelList.add(new UserModel().setUid(UUID.randomUUID().toString()).setName(“u” + i).setAge(10 + i)); } /* * 查询用户列表 * @return {@link Result} / @GetMapping("/list") public Result list() { return Result.success(userModelList); } /* * 添加用户 * @param userModel 这里传JSON字符串 * @return {@link Result} / @PostMapping("/add") public Result add(@RequestBody UserModel userModel) { if (userModel != null) { userModelList.add(userModel.setUid(UUID.randomUUID().toString())); return Result.success(); } return Result.error(); } /* * 根据UID获取用户 * @param uid UID * @return {@link Result} */ @GetMapping("/get/{uid}") public Result getByUid(@PathVariable(“uid”) String uid) { if (StringUtils.isEmpty(uid)) return Result.error(CodeMsg.ERROR_USER_UID_NOT_NULL); for (UserModel userModel : userModelList) if (userModel.getUid().equals(uid)) return Result.success(userModel); return Result.error(CodeMsg.ERROR_USER_NOT_EXIST); }}测试1、启动2、list访问:http://localhost:8080/user/list{ “code”: 0, “msg”: “Success”, “data”: [ { “uid”: “d8e2dfac-b6e8-46c7-9d43-5bb6bf99ce30”, “name”: “u0”, “age”: 10 }, { “uid”: “87001637-9f21-4bc7-b589-bea1b2c795c4”, “name”: “u1”, “age”: 11 }, { “uid”: “5e1398ca-8322-4a68-b0d2-1eb4c1cac9de”, “name”: “u2”, “age”: 12 }, { “uid”: “e6ee5452-4148-4f6d-b820-9cc24e5c91b5”, “name”: “u3”, “age”: 13 }, { “uid”: “3f428e26-57e1-4661-8275-ce3777b5da54”, “name”: “u4”, “age”: 14 }, { “uid”: “b9d994b4-f090-40de-b0f3-e89c613061f2”, “name”: “u5”, “age”: 15 }, { “uid”: “748d1349-5978-4746-b0c1-949eb5613a28”, “name”: “u6”, “age”: 16 }, { “uid”: “abaadb7c-23fb-4297-a531-0c490927f6d5”, “name”: “u7”, “age”: 17 }, { “uid”: “5e5917a1-8674-4367-94c6-6a3fd10a08d6”, “name”: “u8”, “age”: 18 }, { “uid”: “03ed6a83-0cc0-4714-9d0d-f653ebb3a2eb”, “name”: “u9”, “age”: 19 } ]}2、添加数据看一下,数据是什么样子与我们预想的结果一样。获取数据有数据样式:无数据样式:关于冯文议。2017年毕业于阿坝师范学院计算机应用专业。现就职于深圳警圣技术股份有限公司,主要负责服务器接口开发工作。技术方向:Java。 开源软件:JavaLib。后记到这里就结束了,如果在遇到什么问题,或者有不明白的地方,可以通过评论、留言或者私信等方式,告诉我。 ...

February 21, 2019 · 3 min · jiezi

RESTful杂记

前言在网上找了许久的关于REST的资料,发现网上大部分都是说的比较片面,虽然有部分说出了本质,但也没有详细提出,所以在这里记录一下。RESTful是什么首先,维基百科是这样说的:表现层状态转换(REST,英文:Representational State Transfer)是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息这样的概念有点难以理解,了解一个东西,通常可以先了解他的背景,他是为了解决什么问题而出现的? Fielding是一个非常重要的人,他是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。而下面则是他在论文中提出REST的目的。“本文研究计算机科学两大前沿—-软件和网络—-的交叉点。长期以来,软件研究主要关注软件设计的分类、设计方法的演化,很少客观地评估不同的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。“这段话比较绕口,总结一下,就是REST是一个为了进一步解耦client和server的架构风格。REST风格首先,根据论文可以得知,REST风格是由约束来定义的Web 架构背后的设计基本原理,能够被描述为由一组应用于架构中元素之上的约束组成的架构风格。当将每个约束添加到进化中的风格时,会产生一些影响。通过检查这些影响,我们就能够识别出 Web 的约束所导致的属性。然后就能够应用额外的约束来形成一种新的架构风格,这种风格能够更好地反映出现代 Web 架构所期待的属性。client-servetclient-server之间的解耦,服务提供者和服务消费者互不影响,也是我们常说的前后端分离。前后端分离的优势是比较显著的,改善了用户接口跨多个平台的可移植性;同时通过简化服务器组件,改善了系统的可伸缩性。无状态这个约束使架构拥有了可见性、可靠性和可伸缩性等三个架构属性。 可见性是指能单独的理解一个请求,可靠性是减轻了从局部故障中恢复的任务量, 可伸缩性是指为不必在多个请求之间保 存状态,从而允许服务器组件迅速释放资源。可缓存优势明显,不赘述。统一接口它强调组件之间要有 一个统一的接口。通过在组件接口上应用通用性的软件工程原则,整体的系统架 构得到了简化,交互的可见性(通过方法名即知道动作)也得到了改善。实现与它们所提供的服务是解耦的,这促进了独立的可进化性。然而,付出的代价是,统一接口降低了效率,因为信息都使用标准化的形 式来转移,而不能使用特定于应用的需求的形式。(只能使用put post delete get patch等)解决方法:为需要的动作增加一个 endpoint,使用 POST 来执行动作,比如 POST /resend 重新发送邮件。分层系统分层系统风格通过限制组件的行为(即,每个组件只 能“看到”与其交互的紧邻层),将架构分解为若干等级的层。通过将组件对系统的知识限 制在单一层内,为整个系统的复杂性设置了边界,并且提高了底层独立性。我们能够使用层来封装遗留的服务,使新的服务免受遗留客户端的影响,通过将不常用的功能转移到一个共享的中间组件中,从而简化组件的实现。中间组件还能够通过支持跨多个网络和处理器的负 载均衡,来改善系统的可伸缩性。也就是说服务器和客户端之间的中间层(代理,网关等)代替服务器对客户端的请求进行回应,而客户端不需要关心与它交互的组件之外的事情。按需加载代码通过下载并执行 applet 形式或脚本形式的代码,REST允许对客户端的功能进行扩展。通过减少必须被预先实现的功能的数目,简化了客户端的开发。允许在部署之后下载功能代 码也改善了系统的可扩展性。然而,这也降低了可见性,因此它只是REST的一个可选的约束。设计原则(GITHUB API)了解了REST是什么东西后,我们才能设计出合适的API,以下是根据GITHUB API来总结的(基本参考自:https://cizixs.com/2016/12/12…)使用https这个和 Restful API 本身没有很大的关系,但是对于增加网站的安全是非常重要的。API地址和版本如果 API 变化比较大,可以把 API 设计为子域名,比如 https://api.github.com/v3响应内容尽量使用JSON,JSON在多种语言中支持,如果需要使用其他的如XML, 应该在请求头部 Accept 中指定以资源为中心资源分为单个文档和集合,尽量使用复数来表示资源,单个资源通过添加id或者name等来表示。一个资源可以有多个不同的 URL。资源可以嵌套,通过类似目录路径的方式来表示,以体现它们之间的关系。/users/:username/repos/users/:org/repos/repos/:owner/:repo/repos/:owner/:repo/tags/repos/:owner/:repo/branches/:branch使用正确的METHOD这个比较容易理解,即get(获取),post(创建),put(替换),patch(局部更新),delete(删除),head(获取某个资源的头部信息。比如只想了解某个文件的大小,某个资源的修改日期等)对于不符合CURD的情况,可以采用参数协助如分页page=2&per_page=100:指定第几页,以及每页的记录数,或者增加一个endpoint,如上面说的重发邮件,或者将动作转换为资源(Github:比如“喜欢”一个 gist,就增加一个 /gists/:id/star 子资源,然后对其进行操作:“喜欢”使用 PUT /gists/:id/star,“取消喜欢”使用 DELETE /gists/:id/star)状态码https://developer.mozilla.org…2XX:请求正常处理并返回3XX:重定向,请求的资源位置发生变化4XX:客户端发送的请求有错误5XX:服务器端错误错误处理返回错误时,在响应内容里加上具体的错误信息。Hypermedia API当服务端修改API时,客户端不需要知道和修改。 验证和授权, OAUTH2等限流, 参考githubhttps://developer.github.com/… 对用户的请求限流之后,要有方法告诉用户它的请求使用情况,Github API 使用的三个相关的头部:X-RateLimit-Limit: 用户每个小时允许发送请求的最大值X-RateLimit-Remaining:当前时间窗口剩下的可用请求数目X-RateLimit-Rest: 时间窗口重置的时候,到这个时间点可用的请求数量就会变成 X-RateLimit-Limit 的值编写清晰的文档REST与http的关系?个人理解是REST是一种架构风格,而http则是这种架构实现下的一种协议。比较(以操作为中心)以操作为中心可见性低,即不够清晰。在除了CURD的接口外,以操作为中心调用效率高,不需要hack。以操作为中心没有HyperMidea Api,修改api效率低,需要客户端服务端同时修改。以操作为中心上手难度系数大。以资源为中心,简单数据操作,无事务处理,开发和调用简单, 以操作为中心,清晰的规范标准定义,能够处理较为复杂的面向活动的服务在通常的软件开发过程中,我们常常需要分析达成某个目标所需要使用的业务逻辑,并为业务逻辑的执行提供一系列运行接口。在一些Web服务中,这些接口常常表达了某个动作,如将商品放入购物车,提交订单等。这一系列动作组合在一起就可以组成完成目标所需要执行的业务逻辑。在需要调用这些接口的时候,软件开发人员需要向这些接口所在的URL发送一个请求,从而驱使服务执行该动作

February 12, 2019 · 1 min · jiezi

Yii2.0 RESTful API 之速率限制

Yii2.0 RESTful API 之速率限制什么是速率限制?权威指南翻译过来为限流,为防止滥用,你应该考虑对您的 API 限流。 例如,您可以限制每个用户 10 分钟内最多调用 API 100 次。 如果在规定的时间内接收了一个用户大量的请求,将返回响应状态代码 429 (这意味着过多的请求)。要启用速率限制,首先需要实现认证类,而关于认证的章节我在 Yii2.0 RESTful API 认证教程 进行了详细的阐述,本篇就不过多介绍,再次基础上进行操作启用速率限制翻阅权威指南,我们可以看到要启用速率限制首先 认证类 需要继承 yiifiltersRateLimitInterface生成两个关键字段php yii migrate/create add_allowance_and_allowance_updated_at_to_user修改 刚才的迁移文件/** * {@inheritdoc} /public function safeUp(){ $this->addColumn(‘user’, ‘allowance’, $this->integer()); $this->addColumn(‘user’, ‘allowance_updated_at’, $this->integer());}/* * {@inheritdoc} */public function safeDown(){ $this->dropColumn(‘user’, ‘allowance’); $this->dropColumn(‘user’, ‘allowance_updated_at’);}执行迁移php yii migrate编写认证类,并继承 RateLimitInterfacenamespace api\models;use Yii;use yii\base\NotSupportedException;use yii\behaviors\TimestampBehavior;use yii\db\ActiveRecord;use yii\filters\RateLimitInterface;use yii\web\IdentityInterface;class User extends ActiveRecord implements IdentityInterface,RateLimitInterface{ . . .}实现 RateLimitInterface 所需要的方法public function getRateLimit($request, $action){ return [1, 1]; // $rateLimit requests per second}public function loadAllowance($request, $action){ return [$this->allowance, $this->allowance_updated_at];}public function saveAllowance($request, $action, $allowance, $timestamp){ $this->allowance = $allowance; $this->allowance_updated_at = $timestamp; $this->save();}控制器中实现调用use yii\filters\auth\CompositeAuth;use yii\filters\auth\HttpBearerAuth;use yii\filters\auth\QueryParamAuth;use yii\filters\RateLimiter;public function behaviors(){ $behaviors = parent::behaviors(); $behaviors[‘rateLimiter’] = [ ‘class’ => RateLimiter::className(), ’enableRateLimitHeaders’ => true, ]; $behaviors[‘authenticator’] = [ ‘class’ => CompositeAuth::className(), ‘authMethods’ => [ //Http::className(), HttpBearerAuth::className(), QueryParamAuth::className(), ], ]; //$behaviors[‘rateLimiter’][’enableRateLimitHeaders’] = true; return $behaviors;}ok,请求下你的 action,多次请求如果出现 429,那么表示速率限制启用成功以上就是关于 Yii2.0 速率限制的使用,速率限制需要和认证配合着使用,关于认证的,查阅Yii2.0 RESTful API 认证教程 ,这篇文章,推荐您,先看完认证,先做完认证的功能,然后在启用速率限制 关于 Yii2.0 RESTFul API到此我觉得就结束了,核心功能就是这些,剩下的就是具体的实战了,多练、多敲,一共四篇文章,分别为:Yii2.0 RESTful API 基础配置教程Yii2.0 RESTful API 认证教程Yii2.0 RESTful API 之版本控制Yii2.0 RESTful API 之速率限制 ...

January 9, 2019 · 1 min · jiezi

13 个设计 REST API 的最佳实践

原文 RESTful API Design: 13 Best Practices to Make Your Users Happy写在前面之所以翻译这篇文章,是因为自从成为一名前端码农之后,调接口这件事情就成为了家常便饭,并且,还伴随着无数的争论与无奈。编写友好的 restful api 不论对于你的同事,还是将来作为第三方服务调用接口的用户来说,都显得至关重要。关于 restful api 本身以及设计原则,我陆陆续续也看过很多的文章和书籍,在读过原文后,感觉文中指出的 13 点最佳实践还是比较全面的且具有参考意义的,因此翻译出来分享给大家。如有错误,还望指正。由于我一般倾向于意译,关于原文中的开头语或者一些与之无关的内容,我就省略掉了,毕竟时间是金钱,英语好并且能科学上网的朋友我建议还是看原文,以免造成理解上的误差。1. 了解应用于 REST 之上的 HTTP 知识如果你想要构建设计优良的 REST API,了解一些关于 HTTP 协议的基础知识是很有帮助的,毕竟磨刀不误砍材工。在 MDN 上有很多质量不错的文档介绍 HTTP。但是,就 REST API 设计本身而言,所涉及到的 HTTP 知识要点大概包含以下几条:HTTP 中包含动词(或方法): GET、POST、PUT、PATCH 还有 DELETE 是最常用的。REST 是面向资源的,一个资源被一个 URI 所标识,比如 /articles/。端点(endpoint),一般指动词与 URI 的组合,比如 GET: /articles/。一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /articles 可能代表“创建一个新的 article”。在业务领域,我们常常可以将动词和 CRUD(增删查改)关联起来:GET 代表查,POST代表增,PUT 和 PATCH 代表改(注: PUT 通常代表整体更新,而 PATCH 代表局部更新),而 DELETE 代表删。当然了,你可以将 HTTP 协议中所提供的任何东西应用于 REST API 的设计之中,但以上这些是比较基础的,因此时刻将它们记在脑海中是很有必要的。2. 不要返回纯文本虽然返回 JSON 数据格式的数据不是 REST 架构规范强制限定的,但大多 REST API 都遵循这条准则。但是,仅仅返回 JSON 数据格式的数据还是不够的,你还需要指定返回 body 的头部,比如 Content-Type,它的值必须指定为 application/json。这一点对于程序化客户端尤为重要(比如通过 python 的 requests 模块来与 api 进行交互)—— 这些程序是否对返回数据进行正确解码取决于这个头部。注:通常而言,对于浏览器来说,这似乎不是问题,因为浏览器一般都自带内容嗅探机制,但为了保持一致性,还是在响应中设置这个头部比较妥当。3. 避免在 URI 中使用动词如果你理解了第 1 条最佳实践所传达的意思,那么你现在就会明白不要将动词放入 REST API 的 URI 中。这是因为 HTTP 的动词已经足以描述执行于资源的业务逻辑操作了。举个例子,当你想要提供一个针对某个 article 提供 banner 图片并返回的接口时,可能会实现如下格式的接口:GET: /articles/:slug/generateBanner/这里 GET 已经说明了这个接口是在做读的操作,因此,可以简化为:GET: /articles/:slug/banner/类似的,如果这个端口是要创建一个 article:// 不要这么做POST: /articles/createNewArticle/// 这才是最佳实践POST: /articles/尝试用 HTTP 的动词来描述所涉及的业务逻辑操作。4. 使用复数的名词来描述资源一些时候,使用资源的复数形式还是单数形式确实存在一定的困扰,比如使用 /article/:id/ 更好还是使用 /articles/:id/ 更好呢?这里我推荐使用后者。为什么呢?因为复数形式可以满足所有类型端点的需求。单数形式的 GET /article/2/ 看起来还是不错的,但是如果是 GET /article/ 呢?你能够仅通过字面信息来区分这个接口是返回某个 article 还是多个呢?因此,为了避免有单数命名造成的歧义性,并尽可能的保持一致性,使用复数形式,比如:GET: /articles/2/POST: /articles/…5. 在响应中返回错误详情当 API 服务器处理错误时,如果能够在返回的 JSON body 中包含错误信息,对于接口调用者来说,会一定程度上帮助他们完成调试。比如对于常见的提交表单,当遇到如下错误信息时:{ “error”: “Invalid payoad.”, “detail”: { “surname”: “This field is required.” }}接口调用者很快就是明白发生错误的原因。6. 小心 status code这一点可能是最重要、最重要、最重要的一点,可能也是这篇文章中,唯一你需要记住的那一点。你可能知道,HTTP 中你可以返回带有 200 状态码的错误响应,但这是十分糟糕的。不要这么做,你应当返回与返回错误类型相一致的具有一定含义的状态码。聪明的读者可能会说,我按照第 5 点最佳实践来提供足够详细的信息,难道不行吗?当然可以,不过让我讲一个故事:我曾经使用过一个 API,对于它返回的所有响应的状态码均是 200 OK,同时通过响应数据中的 status 字段来表示当前的请求是否成功,比如:{ “status”: “success”, “data”: {}}所以,虽然状态码是 200 OK,但我却不能绝对确定请求是否成功,事实上,当错误发生时,这个 API 会按如下代码片段返回响应:HTTP/1.1 200 OKContent-Type: text/html{ “status”: “failure”, “data”: { “error”: “Expected at least two items in list.” }}头部还是 text/html,因为它同时返回了一些 HTML 片段。正因为这样,我不得不在检查响应状态码正确的同时,还需校验这个具有特殊含义的 status 字段的值,才可以放心的处理响应返回的 data。这种设计的一个真正坏处在于,它打破了接口与调用者之间的“信任”,因为你可能会担心这个接口对你撒谎(注:言外之意就是,由于特设的字段可能会改变,因此增加了不可靠性)。所以,使用正确的状态码,同时仅在响应的 body 中返回错误信息,并设置正确的头部,比如:HTTP/1.1 400 Bad RequestContent-Type: application/json{ “error”: “Expected at least two items in list."}7. 保持 status code 的一致性当你掌握了正确使用状态码之后,就应该努力使它们具有一致性。比如,如果一个 POST 类型的端点返回 201 Created,那么所有的 POST 端点都应返回同样的状态码。这样做的好处在于,调用者无需在意端点返回的状态码取决于某种特殊条件,也就形成了一致性。如果有特殊情况,请在文档中显著地说明它们。下面是我推荐的与动词相对应的状态码:GET: 200 OKPOST: 201 CreatedPUT: 200 OKPATCH: 200 OKDELETE: 204 No Contenthttps://blog.florimondmanca.c…8. 不要嵌套资源使用 REST API 获取资源数据,通常情况下会直接获取多个或者单个,但当我们需要获取相关联的资源时,该怎么做呢?比如说,我们期望获取作者为某个 author 的 article 列表 —— 假设 authro 的 id=12。这里提供两种方案:第一种方案通过在 URI 中,将嵌套的资源放在所关联的资源后边来进行描述,比如:GET: /authors/12/articles/一些人推荐这种方案的理由是,这种形式的 URI 一定程度上描述了 author 与 article 之间的一对多关系。但与此同时,结合第 4 点最佳实践,我们就不太能够分清当前端点返回的数据到底是 author 类型还是 article 类型。这里有一篇文章,详细阐述了扁平化形式优于嵌套形式,因此一定有更好的方法,这就是下面的第二种方案:GET: /articles/?author_id=12直接将筛选 article 的逻辑抽离为 querystring 即可,这样的 URI 相比之前,更加清晰地描述了“获取所有 author(id=12) 的 article”的意思。9. 优雅地处理尾部斜杠一个好的 URI 中是否应当包含尾部斜杠,并不具有探讨价值,选择一种更倾向的风格并保持一致性即可,同时当客户端误用尾部斜杠时,提供重定向响应。我再来讲我自己的一个故事。某天,我在将某个 API 端点集成到项目中,但是我总是收到 500 Internal Error 的错误,我调用的端点差不多看起来这样:POST: /entities调试一段时间之后,我几乎崩溃了,因为我根本不知道我哪里做错了,直到我发现服务器之所以报 500 的错误,是因为我粗心丢掉了尾部斜杠(注:这种经历人人都会遇到,我在 SF 上遇过无数次类似的问题),当我把 URI 改成:POST: /entities/之后,一切正常运转。当然,大多数的 web 框架都针对 URL 是否包含尾部斜杠,进行了优雅地处理并提供定制选项,如果可以的话,找到它并开启这项功能。10. 使用 querystring 来完成筛选和分页功能大部分情况下,一个简单的端点没有办法满足负责业务场景。你的用户可能想要获取满足一定条件下的某些数据集合 ,同时为了保证性能,仅仅获取这个集合下的一个子集。换言之,这通常叫作筛选功能和分页功能:筛选:用户可以提供额外的属性来控制返回的数据集合分页:获取数据集合的子集,最简单的分页是基于分页个数的分页,它由 page 和 page_size 来决定那么问题来了,我们如何将这两项功能与 RESTful API 结合在一起呢?答案当然是通过 querystring。对于分页,很显然使用这种方式再合适不过了,比如:GET: /articles/?page=1&page_size=10但对于筛选,你可能会犯第 8 点最佳实践中所指出的问题,比如获取处于 published 状态的 article 列表:GET: /articles/published/除了之前提出的问题外,这里还涉及一个设计上的问题,就是 published 本身不是资源,它仅仅是资源的特征,类似这种特征字段,应该将它们放到 querystring 中:GET: /articles/?published=true&page=2&page_size=20更加优雅、清晰,不是吗?11. 分清 401 和 403当我们遇到 API 中关于安全的错误提示时,很容易混淆这两个不同类型的错误,认证和授权(比如权限相关)—— 老实讲,我自己也经常搞混。这里是我自己总结的备忘录,它阐述了我如何在实际情况下,区分它们:用户是否未提供身份验证凭据?认证是否还有效?这种类型的错误一般是未认证(401 Unauthorized)。用户经过了正常的身份验证,但没有访问资源所需的权限?这种一般是未授权(403 Forbidden)12. 巧用 202 Accepted我发现 202 Accepted 在某些场合是 201 Created 的一个非常便捷的替代方案,这个状态码的含义是:服务器已经接受了你的请求,但是到目前为止还未创建新的资源,但一切仍处于正常状态。我分享两种特别适合使用 202 Accepted 状态码的业务场景:如果资源是经过位于将来一系列处理流程之后才创建的,比如当某项作业完成时如果资源已经存在,但这是理想状态,因此不应该被识别为一个错误时13. 采用 REST API 定制化的框架作为最后一个最佳实践,让我们来探讨这样一个问题:你如何在 API 的实施中,实践最佳实践呢?通常的情况是这样的,你想要快速创建一个 API 以便一些服务可以互相访问彼此。Python 开发者可能马上掏出了 Flask,而 JS 开发者也不甘示弱,祭出了 Express,他们会使用实现一些简单的 routes 来处理 HTTP 请求。但这样做的问题是,通常,web 框架并不是针对构建 REST API 服务而专门存在的,换言之,Flask 和 Express 是两个十分通用的框架,但它们并非特别适合用于构建 REST API 服务。因此,你必须采取额外的步骤来实施 API 中的最佳实践,但大多数情况下,由于懒惰或者时间紧张等因素,意味着你不会投入过多精力在这些方面 —— 然后给你的用户提供了一个古怪的 API 端点。解决方案十分简单:工欲善其事,必先利其器,掌握并使用正确的工作才是最好的方案。在各种语言中,许多专门用于构建 REST API 服务的新框架已经出现了,它们可以帮助你在不牺牲生产力的情况下,轻松地完成工作,同时遵循最佳实践。在 Python 中,我发现的最好的 API 框架之一是 Falcon。它与 Flask 一样简单,非常高效,十分适合构建 REST API 服务。如果你更喜欢 Django 的话,使用 Django REST Framework就足够了,虽然框架不是那么直观(注:按我的理解应该是说不太容易上手,但是我不这么认为),但功能非常强大。在 NodeJS 中,Restify 似乎也是一个不错的选择,尽管我还没有尝试过。我强烈建议你给这些框架一个机会!它们将帮助你构建规范,优雅且设计良好的 REST API 服务。总结我们都应致力于让调用 API 这件事成为一种乐趣。希望本文能使你了解到在构建更好的 REST API 服务的过程中,涉及到的一些建议和技巧。对我而言,应该把这些最佳实践归结为三点,分别是良好的语义,简洁和合理性。关注公众号 全栈101,只谈技术,不谈人生 ...

December 20, 2018 · 2 min · jiezi