初识T:意义与作用
任何人都可以访问和调用服务器接口,但有些接口是不能随意访问的,需要确定用户身份,令牌就是管理这个的,登录就是获取令牌
微信身份体系设计
微信的code相当于账号密码,向微信服务器发送后,返回代表用户身份的唯一标识openid,(还有一个session_key,在这个项目不需要使用),将openid存储数据库,生成token并缓存,再返回token
小程序只需要携带token令牌通过校验就可调用下单接口
实现T身份权限体系
applicationapicontrollerv1Token
class Token
{
public function getToken($code = '') {
}
}
applicationapivalidateTokenGet
class TokenGet extends BaseValidate
{
protected $rule = [
'code' => 'require|isNotEmpty'
];
protected $message = [
'code' => '没有code,不给你获取token'
];
}
applicationapivalidateBaseValidate.php
protected function isNotEmpty($value)
{
if (empty($value))
{
return false;
}
else
{
return true;
}
}
application oute.php
// 注意这里是post,提高安全性
Route::post('api/:version/token/user', 'api/:version.Token/getToken');
user表
建立模型
applicationapimodelUser
class User extends BaseModel
{
}
新建服务层
applicationapiserviceUserToken
class UserToken
{
public function get($code) {
}
}
applicationapicontrollerv1Token
use appapiserviceUserToken;
class Token
{
/**
* 用户获取令牌(登陆)
* @url /token
* @POST code
* @note 虽然查询应该使用get,但为了稍微增强安全性,所以使用POST
*/
public function getToken($code='')
{
(new TokenGet())->goCheck();
$ut = new UserToken($code);
$token = $ut->get($code);
return [
'token' => $token
];
}
}
获取openid
写配置
applicationextrawx.php
return [
// 小程序app_id
'app_id' => 'your appid',
// 小程序app_secret
'app_secret' => 'your appsecret',
// 微信使用code换取用户openid及session_key的url地址
// appid=%s&secret=%s&js_code=%s表示有三个参数是要动态填入的
'login_url' => "https://api.weixin.qq.com/sns/jscode2session?" .
"appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
];
applicationapiserviceUserToken
class UserToken
{
protected $code;
protected $wxAppID;
protected $wxAppSecret;
protected $wxLoginUrl;
function __construct($code)
{
$this->code = $code;
//读取配置文件
$this->wxAppID = config('wx.app_id');
$this->wxAppSecret = config('wx.app_secret');
$this->wxLoginUrl = sprintf(config('wx.login_url'), $this->wxAppID
,$this->wxAppSecret, $this->code);
}
public function get() {
}
}
新建一个http调用方法
applicationapicommon.php
function curl_get($url, &$httpCode = 0)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//不做证书校验,部署在linux环境下请改为true
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$file_contents = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $file_contents;
}
applicationapiserviceUsertoken
class Usertoken
{
protected $code;
protected $wxAppID;
protected $wxAppSecret;
protected $wxLoginUrl;
function __construct($code)
{
$this->code = $code;
$this->wxAppID = config('wx.app_id');
$this->wxAppSecret = config('wx.app_secret');
$this->wxLoginUrl = sprintf(config('wx.login_url'), $this->wxAppID
,$this->wxAppSecret, $this->code)
}
public function get() {
$result = curl_get($this->wxLoginUrl);
// 注意json_decode的第二个参数true,这将使字符串被转化为数组而非对象
$wxResult = json_decode($result, true);
if (empty($wxResult)) {
// 为什么以empty判断是否错误,这是根据微信返回规则摸索出来的
// 这种情况通常是由于传入不合法的code
// 为什么用框架的异常处理,因为是服务器异常,要记录日志
throw new Exception('获取session_key及openID时异常,微信内部错误');
}
else{
// 建议用明确的变量来表示是否成功
// 微信服务器并不会将错误标记为400,无论成功还是失败都标记成200
// 这样非常不好判断,只能使用errcode是否存在来判断
$loginFail = array_key_exists('errcode', $wxResult);
if ($loginFail) {
}
else {
}
}
}
}
调用接口异常处理和用postman做测试
applicationlibexceptionWeChatException.php
<?php
namespace applibexception;
/**
* 微信服务器异常
*/
class WeChatException extends BaseException
{
public $code = 400;
public $msg = 'wechat unknown error';
public $errorCode = 999;
}
使用
class Usertoken
{
public function get() {
$result = curl_get($this->wxLoginUrl);
$wxResult = json_decode($result, true);
if (empty($wxResult)) {
throw new Exception('获取session_key及openID时异常,微信内部错误');
}
else{
$loginFail = array_key_exists('errcode', $wxResult);
if ($loginFail) {
$this->processLoginError($wxResult)
}
else {
return $this->grantToken($wxResult)
}
}
}
// 处理微信登陆异常
// 那些异常应该返回客户端,那些异常不应该返回客户端
// 需要认真思考
private function processLoginError($wxResult)
{
throw new WeChatException(
[
'msg' => $wxResult['errmsg'],
'errorCode' => $wxResult['errcode']
]);
}
private function grantToken($wxResult){
// 拿到openid
// 到数据库看一下,这个openid是不是已存在
// 如果存在,则不处理,如果不存在则新增一条user记录
// 生成令牌准备缓存数据,写入缓存
// 把令牌返回到客户端去
$openid = $wxResult['openid'];
}
}
使用微信小程序的wx.login方法获取code进行测试
查看微信接口返回数据,如下表示成功
数据库中插入用户并将用户信息存入缓存
applicationapimodelUser
class User extends BaseModel
{
/**
* 用户是否存在
* 存在返回uid,不存在返回0
*/
public static function getByOpenID($openid)
{
$user = User::where('openid', '=', $openid)
->find();
return $user;
}
}
applicationapiserviceUserToken.php
class UserToken
{
private function grantToken($wxResult){
// 拿到openid
// 到数据库看一下,这个openid是不是已存在
// 如果存在,则不处理,如果不存在则新增一条user记录
// 生成令牌准备缓存数据,写入缓存
// 把令牌返回到客户端去
$openid = $wxResult['openid'];
$user = UserModel::getByOpenID($openid);
if ($user) {
$uid = $user->id;
}
else { //用户不存在
$uid = $this->newUser($openid);
}
$cachedValue = $this->prepareCachedValue($wxResult, $uid);
}
// 创建新用户
private function newUser($openid)
{
// 有可能会有异常,如果没有特别处理
// 这里不需要try——catch
// 全局异常处理会记录日志
// 并且这样的异常属于服务器异常
// 也不应该定义BaseException返回到客户端
$user = User::create(
[
'openid' => $openid
]);
return $user->id;
}
}
将用户信息存入缓存
class UserToken
{
// 存入缓存的键值对如下:
// key: 令牌
// value: [wxResult, uid(数据库中用户唯一身份), scope(权限)]
// 此方法用于构建value
private function prepareCachedValue($wxResult,$uid) {
$cachedValue = $wxResult;
$cachedValue['uid'] = $uid;
$cachedValue['scope'] = 16; //数字越大,权限越大
return $cachedValue;
}
private function saveToCache($cacheValue) {
//...
}
}
生成token
生成token的方法需要复用,写在基类Token
让UserToken继承基类Token
applicationapiserviceToken
class Token
{
public static function generateToken()
{
//32个字符组成的随机字符串
$randChar = getRandChar(32);
//当前访问时间戳
$timestamp = $_SERVER['REQUEST_TIME_FLOAT'];
//salt 盐,特殊加密信息
$tokenSalt = config('secure.token_salt');
// 用三组字符串,进行md5加密
return md5($randChar . $timestamp . $tokenSalt);
}
}
定义一个获取随机字符串的通用方法
applicationlibcommon.php
function getRandChar($length)
{
$str = null;
$strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
$max = strlen($strPol) - 1;
for ($i = 0;
$i < $length;
$i++) {
$str .= $strPol[rand(0, $max)];
}
return $str;
}
敏感信息配置文件
applicationextrasecure
return [
'token_salt' => 'your_salt'
];
缓存时间配置
applicationextrasetting.php
return [
'token_expire_in' => '7200'
];
applicationapiserviceUserToken
class UserToken extends Token
{
private function saveToCache($wxResult)
{
$key = self::generateToken();
//转成字符串
$value = json_encode($wxResult);
$expire_in = config('setting.token_expire_in');
//tp5的cache缓存方法,当前项目使用文件缓存
$result = cache($key, $value, $expire_in);
if (!$result){
throw new TokenException([
'msg' => '服务器缓存异常',
'errorCode' => 10005
]);
}
return $key; //返回令牌
}
}
异常处理
applicationlibexceptionTokenException.php
<?php
namespace applibexception;
/**
* token验证失败时抛出此异常
*/
class TokenException extends BaseException
{
public $code = 401;
public $msg = 'Token已过期或无效Token';
public $errorCode = 10001;
}
class UserToken extends Token
{
private function grantToken($wxResult){
$openid = $wxResult['openid'];
$user = UserModel::getByOpenID($openid);
if ($user) {
$uid = $user->id;
}
else { //用户不存在
$uid = $this->newUser($openid);
}
$cachedValue = $this->prepareCachedValue($wxResult, $uid);
$token = $this->saveToCache($cachedValue)
return $token; //返回令牌
}
}
小程序测试工具
let baseUrl = 'http://localhost:8000/public/index.php/api/v1'
Page({
getToken () {
wx.login({
success (res) {
var code = res.code;
console.log(code, 'code');
wx.request({
url: baseUrl + '/token/user',
data: {
code
},
method: 'POST',
success (res) {
console.log(res.data);
wx.setStorageSync('token', res.data.token)
},
fail (res) {
console.log(res.data);
}
})
}
})
}
})
商品详情分析与初步编写
新建路由
application
oute.php
Route::get('api/:version/product/:id', 'api/:version.Product/getOne');
applicationapicontrollerv1Product
class Product
{
public function getOne($id)
{
(new IDMustBePositiveInt())->goCheck();
}
}
applicationapimodelProduct
class Product
{
public static function getProductDetail($id)
{
}
}
一张商品详情图片只能属于一个商品,一个商品可以有多个商品详情图片
product_property商品参数和product商品也是一对多关系
product_image表只能通过img_id从image表间接拿到url
product_image表
product_property表
定义模型关联
class Product
{
public static function getProductDetail($id)
{
}
public function imgs()
{
return $this->hasMany('ProductImage', 'product_id', 'id');
}
public function properties()
{
return $this->hasMany('ProductProperty', 'product_id', 'id');
}
}
定义两个模型
applicationapimodelProductImage.php
<?php
namespace appapimodel;
class ProductImage extends BaseModel
{
protected $hidden = ['img_id', 'delete_time', 'product_id'];
public function imgUrl()
{
return $this->belongsTo('Image', 'img_id', 'id');
}
}
applicationapimodelProductProperty.php
<?php
namespace appapimodel;
class ProductProperty extends BaseModel
{
protected $hidden=['product_id', 'delete_time', 'id'];
}
applicationapimodelProduct
class Product
{
public static function getProductDetail($id)
{
//注意with的参数不能加空格
$products = self::with('imgs,properties')->find($id);
return $products;
}
}
applicationapicontrollerv1Product
class Product
{
public function getOne($id)
{
(new IDMustBePostiveInt())->goCheck();
$product = ProductModel::getProductDetail($id);
if (!$product) {
throw new ProductException();
}
return $product;
}
}
路由变量规则与分组
application oute.php
// Route::get('api/:version/product/:id', 'api/:version.Product/getOne')
// Route::get('api/:version/product/recent', 'api/:version.Product/getRecent')
// 以上的情况会导致recent接口不可用,recent会被当成id
// 使用正则,限定参数必须是正整数
Route::get('api/:version/product/:id', 'api/:version.Product/getOne', [], ['id' => 'd+'])
Route::get('api/:version/product/recent', 'api/:version.Product/getRecent')
分组路由效率更高
Route::group('api/:version/product', function () {
Route::get('/by_category', 'api/:version.Product/getAllInCategory')
Route::get('/:id', 'api/:version.Product/getOne', [], ['id' => 'd+'])
Route::get('/recent', 'api/:version.Product/getRecent')
})
闭包函数构建查询器
添加嵌套关联
applicationapimodelProduct
class Product
{
public static function getProductDetail($id)
{
$products = self::with('imgs.imgUrl,properties')->find($id);
return $products;
}
}
按照order字段进行排序
applicationapimodelProduct
class Product
{
public static function getProductDetail($id)
{
$products = self::with([
'imgs' => function($query) {
$query->with(['imgUrl'])->order('order', 'asc');
}
])->with(['properties'])->find($id);
return $products;
}
}
查看返回结果
用户收货地址,通过令牌获取用户标识
收货地址需要接口保护
applicationapicontrollerv1Address
class Address
{
public function createOrUpdateAddress ()
{
}
}
新建路由
Route::get('api/:version/address', 'api/:version.Address/createOrUpdateAddress');
user和user_address是一对一关系,本项目限定了一个用户只能有一个收货地址
applicationapivalidateAddressNew
class AddressNew extends BaseValidate
{
//uid不能做为接口的参数传,令牌是A用户的,如果参数里的uid是B用户的,就会导致A用户更改B用户的收货地址
//项目将使用令牌换取uid
protected $rule = [
'name' => 'require|isNotEmpty',
'mobile' => 'require|isMobile',
'province' => 'require|isNotEmpty',
'city' => 'require|isNotEmpty',
'country' => 'require|isNotEmpty',
'detail' => 'require|isNotEmpty',
]
}
新增一个手机号验证器
applicationapivalidateBaseValidate.php
class BaseValidate extends Validate
{
protected function isMobile($value)
{
$rule = '^1(3|4|5|7|8)[0-9]d{8}$^';
$result = preg_match($rule, $value);
if ($result) {
return true;
} else {
return false;
}
}
}
面向对象的方式封装获取uid方法
class Address
{
public function createOrUpdateAddress()
{
(new AddressNew()) -> goCheck();
// 根据Token来获取uid
// 根据uid 查找用户数据,判断用户是否存在,如果不存在抛异常
// 获取用户从客户端提交来的地址信息
// 根据用户地址信息是否存在,从而判断是添加地址还是更新地址
}
}
applicationapiserviceToken
class Token
{
public static function getCurrentTokenVar($key) {
//获取http请求头的token
$token = Request::instance()->header('token');
//获取缓存中的值
$vars = Cache::get($token);
if (!$vars) {
throw new TokenException();
}
else{
if (!is_array($vars))
{
$vars = json_decode($vars, true);
}
if (array_key_exists($key, $vars)) {
return $vars[$key];
}
else {
throw new Exception('尝试获取的Token变量并不存在');
}
}
}
public static function getCurrentUid() {
//token
$uid = self::getCurrentTokenVar('uid');
return $uid;
}
}
applicationapicontrollerv1Address.php
class Address
{
public function createOrUpdateAddress()
{
(new AddressNew())->goCheck();
$uid = TokenService::getCurrentUid();
$user = UserModel::get($uid);
if (!$user) {
throw new UserException();
}
}
}
applicationlibexceptionUserException.php
class UserException extends BaseException
{
public $code = 404;
public $msg = '用户不存在';
public $errorCode = 60000;
}
applicationapicontrollerv1Address.php
class Address
{
public function createOrUpdateAddress ()
{
(new AddressNew()) -> goCheck();
$uid = TokenService::getCurrentUid();
$user = UserModel::get($uid);
if (!$user) {
throw new UserException();
}
$dataArray = getDatas(); //伪代码,先讲下一步
$userAddress = $user->address;
if (!$userAddress)
{
$user->address()->save($dataArray);
}
else {
$user->address->save($dataArray);
}
//不需要返回模型,返回一个成功的提示
return new SuccessMessage();
}
}
在user模型新建关联关系,以读取地址
applicationapimodelUser.php
class User
{
public function address()
{
//一对一关系,在拥有外键的一方就是用belongTo,没有就用hasOne
return $this->hasOne('UserAddress', 'user_id', 'id');
}
}
applicationapimodelUserAddress.php
class UserAddress extends BaseModel
{
//
}
applicationlibexceptionSuccessMessage.php
<?php
namespace applibexception;
class SuccessMessage extends BaseException
{
//201创建成功
public $code = 201;
public $msg = 'ok';
public $errorCode = 0;
}
用户收货地址,参数过滤
当前没有限制用户传多余参数,可能会影响到正常的数据,以下是限制方法
applicationapivalidateBaseValidate
class BaseValidate extends Validate
{
public function getDataByRule($arrays){
if (array_key_exists('user_id', $arrays) | array_key_exists('uid', $arrays)) {
//可能是恶意请求
throw new ParameterException([
'msg' => '参数中包含有非法的参数名user_id或者uid'
]);
}
$newArray = [];
foreach ($this->rule as $key=>$value)
{
$newArray[$key] = $arrays[$key];
}
return $newArray;
}
}
更改刚刚的伪代码
applicationapicontrollerv1Address.php
class Address
{
public function createOrUpdateAddress ()
{
$validate = new AddressNew();
$validate -> goCheck();
$uid = TokenService::getCurrentUid();
$user = UserModel::get($uid);
if (!$user) {
throw new UserException();
}
$dataArray = $validate->getDataByRule(input('post.'));
$userAddress = $user->address;
if (!$userAddress)
{
$user->address()->save($dataArray);
}
else {
$user->address->save($dataArray);
}
//使用自定义状态码
return json(new SuccessMessage(), 201);
}
}