假设一个微信小程序端+安卓端+服务器的线上商城项目,需求如下:
要求遵循Restful风格使用django rest framework+OAuth 2.0 库架设提供给安卓App和微信小程序使用的API。API需要实现以下原则:
参考大神阮一峰博客:http://www.ruanyifeng.com/blog/2014/05/restful_api.html。为了论述的完整性,因此这里对Restful基础也进行了简单的概述。
REST,即Representational State Transfer的缩写。直接翻译的意思是"表现层状态转化"。 它是一种互联网应用程序的API设计理念,即:用URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述对资源的操作。表现层是指把"资源"具体呈现出来的形式(Representation)。互联网通信协议HTTP协议,是一个无状态协议(两次通信之间不能直接交互)。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer),而这种转化是建立在表现层之上的,所以就是"表现层状态转化"(使得资源的表现形式发生变化)。
(1)系统上的一切对象都要抽象为资源;
(2)每个资源对应唯一的资源标识(URI);
(3)对资源的操作不能改变资源标识(URI)本身(例如数据库一旦创建用户信息以后的操作都不修改用户id);
(4)对资源的所有的操作都是无状态的(即两个操作之间不能直接交互)。
任何事物,只要有被引用到的必要,它就是一个资源。资源可以是实体(如用户信息),也可以只是一个抽象概念(例如价值) 。要让一个资源可以被识别,需要有个唯一标识,在Web中这个唯一标识就是URI。URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源(存储在数据库中时通常是指id)。URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何定位这个资源,通常一个URL不仅提供了资源在服务器中的定位信息(相对路径或步进值等定位信息),还携带了资源的id。
不论什么样的资源,都使用相同的接口进行访问。接口应该使用标准的HTTP方法如GET,PUT,POST和DELETE并遵循这些方法的语义。GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
客户端和服务端之间传送的是资源的表述,而不是资源本身,资源的表述具体一点就是API传递的数据格式,如文本资源可以使用html,xml,json的形式传送。资源的表述可以在HTTP请求中指定,如客户端可以通过Accept头请求一种特定格式的表述,服务端则通过Content-Type告诉客户端资源的表述形式。
通过超链接使资源之间相互连接起来。如每个网页是html表示的文本资源,通过html 超链接,把图片和视频与当前这个html表示的网页连接起来,使得客户端访问这个html网页时可以通过超链接把图片和视频也加载出来。
客户端访问服务器中的资源并对其进行操作(比如修改用户信息),使得资源的表现层(资源表现形式)发生转化,即状态转移。
状态区分应用状态和资源状态,客户端负责维护应用状态(如浏览器历史记录),而服务端维护资源状态(如用户信息)。RESTful只维护资源的状态,而不需要维护客户端的状态,对于它来说,每次请求都是全新的,它只需要针对本次请求作相应的操作,不需要将本次请求的相关信息记录下来以便用于后续来自相同客户端请求的处理。如果客户端利用存储的Cookie跟踪服务器会话状态,此时就违反了无状态通信原则。
使用https作为API通信协议。
尽量将API部署在专用域名之下,如:
https://api.myhost.com
如果后期API不会有进一步扩展的可能,也可放在主域名下:
https://www.myhost.com/api/
应该将API的版本号放入API中URL:
https://api.myhost.com/v1/
路径即API的具体网址。在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。例如一个API 提供一个在线商店的信息,包括:商品、买家和卖家,则API路径应设计成:
https://api.myhost.com/v1/buyers/
https://api.myhost.com/v1/sellers/
https://api.myhost.com/v1/products/
对应三个数据表: buyer,seller,product。
HTTP含义与资源操作应该一一对应。
HTTP动词和对应的资源操作
HTTP动词 SQL(执行对资源的操作) 含义 GET SELECT 从服务器取出资源(一项或多项) POST CREATE 在服务器新建一个资源 PUT UPDATE 在服务器更新资源(客户端提供改变后的完整资源) PATCH UPDATE 在服务器更新资源(客户端提供改变的属性) DELETE DELETE 从服务器删除资源
以(4)中的product资源为例,则对其进行操作的HTTP 方法和API路径(相对于https://api.myhost.com/v1/)可以设计成以下形式:
HTTP动词 | API路径 | 含义 |
GET | products/ | 列出所有产品 |
GET | products/id/ | 获取指定id产品信息 |
GET | products/id/buyers/ | 获取指定id产品的所有买家 |
POST | products/ | 新建一个产品 |
PUT | products/id/ | 更新指定id产品的信息(提供该产品全部信息) |
PATCH | products/id/ | 更新指定id产品的信息(提供该产品要更新的信息) |
DELETE | products/id/ | 删除指定id产品 |
DELETE | products/id/buyers/id/ | 删除指定id产品的指定买 |
通常数据库中的资源非常多,不能一次返回给用户,否则会使用户等待时间过长,因此API应该提供参数,过滤返回结果。常见的过滤操作和对应的API 路径如下(相对于(5)中某个API路径,如https://api.myhost.com/v1/products/)。
API参数 | 含义 |
?limit=10 | 指定返回资源的数量 |
?offset=10 | 指定返回资源的起始位置(对应数据库中的查询起始位置) |
?page=2&per_page=100 | 指定第几页,以及每页的资源数量 |
?sortby=name&order=asc | 指定返回结果按照哪个属性排序,以及排序顺序(asc升序,desc降序) |
?product_type_id=1 | 指定筛选条件,如:返回的产品类型。 |
参数的设计允许API路径和URL参数相重复。比如,GET products/id/buyers/与 GET /buyers?product_id=ID 的就是重复的。
状态码分类如下
常见的HTTP状态码和含义如下:
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{error:"data type error"}
针对不同操作,服务器向用户返回的结果应该符合以下规范。
GET /collection/:返回资源对象的列表(数组)
GET /collection/resource/:返回单个资源对象
POST /collection/:返回新生成的资源对象
PUT /collection/resource/:返回完整的资源对象
PATCH /collection/resource/:返回完整的资源对象
DELETE /collection/resource/:返回一个空文档,但在实际使用时通常会返回一个消息,指明操作成功或者失败。
RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。例如用户向api.myhost.com发出请求,服务器向用户返回以下文档:
{"link": {
"rel": "collection https://www.myhost.com/products/",
"href": "https://api.myhost.com/products/",
"title": "List of products",
"type": "write your data type here "
},
"link": {
"rel": "collection https://www.myhost.com/buyers/",
"href": "https://api.myhost.com/buyers/",
"title": "List of buyers",
"type": "write your data type here "
},
"link": {
"rel": "collection https://www.myhost.com/sellers/",
"href": "https://api.myhost.com/sellers/",
"title": "List of sellers",
"type": "write your data type here "
}
}
文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型。Hypermedia API的设计被称为HATEOAS(Hypermedia as the Engine of Application State,超媒体即应用状态引擎),即客户端可以通过一个简单的初始资源URI,从API返回值获取可以操作的其他资源信息。
API的身份认证应该使用OAuth 2.0框架。
https://www.myhost.com/products/2.0/12334/
这个URI指向id=12334,版本号为2.0的资源。资源版本号可以在HTTP请求头信息的Accept字段中进行区分:
Accept: vnd.myhost-com.product+json; version=1.0
Accept: vnd..myhost-com.product+json; version=2.0
幂等性(Idempotent)表示发送一次和多次请求引起的效应是一致的。即在网速不够快的情况下,客户端发送一个请求后不能立即得到响应,由于不能确定是否请求是否被成功提交,所以它有可能会再次发送另一个相同的请求,如果请求是幂等性的,那么发送多次请求得到的资源表示形式是一样的,即服务器状态不会发生变化;反之,发送一次请求和发送多次请求所获取到的资源表示形式是不一样的,即非幂等性请求每发送一次都会引起服务器状态转移。
GET、HEAD和OPTIONS、DELETE和PATCH、PUT是幂等方法。POST由于总是进行添加操作,如果服务器接收到两次相同的POST操作,将导致两个相同的资源被创建,所以是一个非幂等的。在设计Web API的时,应该尽量根据HTTP方法的幂等性来决定处理的逻辑。例如,PUT是一个幂等方法,所以携带相同资源的PUT请求不应该引起资源的状态变化,如果我们在资源上附加一个自增的计数器表示被修改的次数,这实际上就破坏了PUT方法的幂等性。
API身份验证:为了确保确保授权用户才能使用我们的API,我们需要对API访问进行身份验证,只有通过验证的请求才能被响应,否则被拒绝。最常用的API身份验证协议是HTTP基本身份验证和OAuth2.0。由于HTTP基本身份验证安全性较差,基本不在公网使用,因此重点关注OAuth2.0。
API验证一般来说会使用基于Token的认证,第一次认证通过后,就会获取到一个Token字串,以后请求中带上此Token就可以。此Token凭据通常仅使用Base64编码,没有加密,没有散列。每个请求都在头部中包含此Token字串。因此,如果Web传输时候用的是没有加密的HTTP,而不是HTTPS的话,基本上没有任何安全可言,在网络传输过程中会被别人抓包,很容易窃取到,因此HTTP基本身份验证限制在内网、局域网内部使用。原则:A.不对外开放不安全的API访问权限;B.使用HTTPS加密传输。
参考:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同,用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。一些关键概念如下:
(1)第三方应用程序( Third-party application):第三方应用程序,即"客户端"(client),安卓APP和微信小程序等就是客户端。
(2)服务提供商(HTTP service):HTTP服务提供商,简称"服务提供商",通常指我们部署在阿里云或者腾讯云上的web服务器。
(3)用户(Resource Owner):资源所有者,又称"用户"(user),如微信用户,微信朋友圈的图片视频都是微信用户所有的。
(4)用户代理(User Agent,不是HTTP中UA头):用户代理,代替用户与认证服务器和客户端交互的程序,例如第三方应用使用微信登录时,微信认证SDK将代替用户与微信认证服务器和第三方应用交互。
(5)认证服务器(Authorization server):认证服务器,即服务提供商专门用来处理认证的服务器。
(6)资源服务器(Resource server):资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器的不同应用,也可以真实部署在不同云主机上。
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权(登录就是用户授权,授权后APP可以拿到用户所有的资源,即用户注册信息等)。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
其中(B)用户授权是关键,有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。
OAuth 2.0定义了四种授权方式,即:
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动,个人认为适合单纯开放API给第三方调用的应用。
步骤如下:
(A)用户访问客户端,客户端将用户导向认证服务器(例如转到登录页面)。
(B)用户选择是否给予客户端授权(选择是否登录)。
(C)假设用户给予授权(例如用户完成登录),认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码(例如登录后跳转到其他页面)。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
各个步骤所需参数如下:
A步骤中,客户端申请认证的URI,包含以下参数:
下面是一个例子:
GET https://api.myhost.com/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
C步骤中,服务器回应客户端的URI,包含以下参数:
下面是一个例子。
302 Found https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
下面是一个例子。
POST https://www.myhost.com/token
(请求头中封装)
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW,
Content-Type: application/x-www-form-urlencoded,
(参数中封装)
grant_type:authorization_code&code=SplxlOBeZQQYbYS6WxSbIA,
redirect_uri:https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb,
E步骤中,认证服务器发送的HTTP回复,包含以下参数:
下面是一个例子:
200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
从上面代码可以看到,参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。
简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证(个人认为适合纯web应用(只在浏览器中运行)的API验证,为了安全,用户代理中和认证服务器交互的程序应放在服务器中)。
步骤如下:
(A)客户端将用户导向认证服务器(如转到登录网页)。
(B)用户决定是否给于客户端授权(如:输入账号密码登录)。
(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌(可能不可见)。
(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
(F)浏览器执行上一步获得的脚本,提取出令牌。
(G)浏览器将令牌发给客户端(个人理解这里的客户端还是浏览器,令牌用于获取登录后才能查看的页面内容)。
A步骤中,客户端发出的HTTP请求,包含以下参数:
下面是一个例子:
GET Https://www.myhost.com/authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
C步骤中,认证服务器回应客户端的URI,包含以下参数:
下面是一个例子:
302 Found
Location:http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600
在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,浏览器运行资源服务器发送过来的代码并提取出Hash中的令牌。
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码,客户端使用这些信息,向认证服务器索要授权,个人认为适合用户注册/登录与API授权不分离的应用。
步骤如下:
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌(这一步必需加密,不能明文传输)。
(C)认证服务器确认无误后,向客户端提供访问令牌(这一步必需加密,不能明文传输)。
B步骤中,客户端发出的HTTP请求,包含以下参数:
下面是一个例子:
POST https://www.myhost.com/token
(请求头中)
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
(参数中)
grant_type=password&username=johndoe&password=A3ddj3w
C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子:
200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
上面代码中,各个参数的含义参见授权码模式一节。注意:整个过程中,客户端不得保存用户的密码。
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向认证服务器请求认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题,个人认为适合用作刷新Access Token。
步骤如下:
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌(需要加密)。
A步骤中,客户端发出的HTTP请求,包含以下参数:
请求例子:
POST https://www.myhost.com/token
(请求头)
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
(参数)
grant_type=client_credentials
认证服务器必须以某种方式,验证客户端身份。
B步骤中,认证服务器向客户端发送访问令牌,下面是一个例子:
200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"example_parameter":"example_value"
}
上面代码中,各个参数的含义参见授权码模式一节。
如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌,必须加密。
客户端发出更新令牌的HTTP请求,包含以下参数:
下面是一个例子。
POST https://www.myhost.com/token (请求头) Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded (参数) grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
重定向URI应进行全路径验证,避免跳转出现问题(防止被劫持而跳转到其他网页);
状态参数State要随时销毁;
对回调URL进行跳转校验(防止被劫持而跳转到其他网页);
获取访问Token时候,要验证App 密匙(另外颁发的密匙,例如微信小程序开发者账号注册时腾讯颁发的AppSecret);
处理用户输入时应该:"绝不信任任何用户的输入" ,所有用户输入应该被严格过滤,转义成普通文本,避免恶意用户输入如js脚本和SQL语句等被服务器执行。
DDOS (Distributed Denial of Service),分布式拒绝服务攻击,攻击原理是向目标主机发送大量的请求数据包,这些数据包经过伪装,无法识别它的来源,而且这些数据包所请求的服务往往要消耗大量的系统资源,造成目标主机无法为用户提供正常服务,甚至导致系统崩溃。通俗一点就是攻击者伪装成普通用户,请求服务器执行一些耗时的工作(比如一次性查询大量信息),使得服务器无法响应正常用户的请求。DDoS的表现形式主要有两种,一种为流量攻击,主要是针对网络带宽的攻击,即发送大量攻击包导致服务器网络带宽被阻塞,合法网络包被虚假的攻击包淹没而无法到达主机;另一种为资源耗尽攻击,主要是针对服务器主机的攻击,即通过大量攻击包导致主机的内存被耗尽或CPU负载过高而造成无法提供网络服务。DDOS攻击的是网络层。
防御措施
(1)设置备份主机
备份网站,或者最低限度有一个临时主页,生产服务器万一下线了,可以立刻切换到备份网站,不至于毫无办法。
(2)拦截恶意请求
如果恶意请求有特征(比如固定IP),直接配置防火墙规则屏蔽恶意请求。
(3)CDN
CDN 指的是网站的静态内容分发到多个服务器,用户就近访问,可以提高访问速度。网站内容存放在源服务器上,CDN 上面是内容的缓存。用户只允许访问 CDN,如果内容不在 CDN 上,CDN 再向源服务器发出请求,这样的话,只要 CDN 带宽够大,就可以抵御很大的攻击。不过,这种方法有一个前提,网站的大部分内容必须可以静态缓存。对于动态内容为主的网站(比如论坛),就要想别的办法,尽量减少用户对动态数据的请求。
CC攻击是指模拟多个用户(多线程)不停的访问 那些需要大量数据操作(需要大量CPU时间)的页面,并且与真实用户访问无差别,导致服务器过载而崩溃。与DDOS 攻击不同的是CC攻击的WEB应用层(Apache这类提供网络服务的程序)。
防御措施
(1)取消域名绑定
一般cc攻击都是针对网站的域名进行攻击,比如我们的网站域名是“www.abc.com”,那么攻击者就在攻击工具中设定攻击对象为该域名然后实施攻击。 对于这样的攻击我们的措施是取消这个域名的绑定,让CC攻击失去目标。
(2)域名欺骗解析
如果发现针对域名的CC攻击,我们可以把被攻击的域名解析到127.0.0.1这个地址上。我们知道127.0.0.1是本地回环IP,用来进行网络测试的,如果把被攻击的域名解析到这个IP上,就可以实现攻击者自己攻击自己的目的。
(3)更改Web端口
一般情况下Web服务器通过80端口对外提供服务,因此攻击者实施攻击就以默认的80端口进行攻击,所以,我们可以修改Web端口达到防CC攻击的目的。
(4)屏蔽IP
我们通过命令或查看日志发现了CC攻击的源IP,就可以在防火墙中屏蔽该IP对Web站点的访问,从而达到防范攻击的目的。
数据加密
常见加密算法参考:https://blog.csdn.net/baidu_22254181/article/details/82594072。请求数据使用以下三个方法保证安全:
下一节:Django 架设 Restful API(二)开发准备:django开发环境搭建
https://blog.csdn.net/anbuqi/article/details/114487902