ThinkPHP微信登录Token验证

我发现,你在我心中的位置会膨胀。

小程序端获取code

1
2
3
4
5
6
wx.login({
success: function (login_res) {
if (login_res.code) {
console.log("微信登录请求返回code:" + login_res.code);

......

带上code访问服务器接口

服务器接口根据code获取Token

路由设计

1
Route::post('api/:version/token/user', 'api/:version.Tokens/getToken');

控制器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

namespace app\api\controller\v1;

use app\api\service\UserToken;
use app\api\validate\TokenGet;

class Tokens
{
public function getToken($code)
{
(new TokenGet())->goCheck(); // code 格式验证
$ut = new UserToken($code);
$token = $ut->get();
return ['token' => $token];
}
}

Service服务方法
tp5\application\api\service\UserToken.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<?php

namespace app\api\service;

use app\api\model\User;
use app\lib\enum\ScopeEnum;
use app\lib\exception\TokenException;
use app\lib\exception\WeChatException;
use think\Exception;

class UserToken extends Token
{

protected $code;
protected $wxLoginUrl;
protected $wxAppID;
protected $wxAppSecret;

/**
* 初始化接口
* @param $code
*/
public 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);
}

/**
* 请求微信接口, 返回openid和sessionKey
*
* @return string
* @throws Exception
*/
public function get()
{
$res = curl_get($this->wxLoginUrl);
$wxResult = json_decode($res, true);
if (empty($wxResult)) {
throw new Exception('微信内部错误');
} else {
$loginFail = array_key_exists('errcode', $wxResult);
if ($loginFail) {
$this->processLoginError($wxResult);
} else {
return $this->grantToken($wxResult);
}
}
}


/**
* 使用 openid 作为区分用户的主键, 有则返回, 无责创建用户后再返回
*
* @param $wxResult
* @return string
*/
private function grantToken($wxResult)
{
$openid = $wxResult['openid'];
$user = User::getByOpenID($openid);
if (!$user) {
$uid = $this->newUser($openid);
} else {
$uid = $user->id;
}

$cachedValue = $this->prepareCachedValue($wxResult, $uid);
$token = $this->saveToCache($cachedValue);
return $token;
}

/**
* 生成一个唯一的token作为key, 使用 @prepareCachedValue() 函数生成的值作为 值,生成缓存,存储在redis中
* 并设置一个过期时间, 我们可以参照微信的接口过期时间, 7200s, 也就是2个小时
*
* @param $wxResult
* @return string
* @throws TokenException
*/
private function saveToCache($wxResult)
{
$key = self::generateToken();
$value = json_encode($wxResult);
$expire_in = config('setting.token_expire_in');
$result = cache($key, $value, $expire_in);

if (!$result){
throw new TokenException(['msg' => '服务器缓存异常', 'errorCode' => 10005]);
}
return $key;
}

/**
* 将微信接口返回的数据, 用户ID, 用户权限 作为 缓存的值
*
* @param $wxResult
* @param $uid
* @return mixed
*/
private function prepareCachedValue($wxResult, $uid)
{
$cachedValue = $wxResult;
$cachedValue['uid'] = $uid;
$cachedValue['scope'] = ScopeEnum::User;
return $cachedValue;
}

/**
* 使用 openid 在 user表中创建一条新的记录, 并返回Id
*
* @param $openid
* @return mixed
*
*/
private function newUser($openid)
{
$user = User::create(['openid' => $openid]);
return $user->id;
}


/**
* 微信登录错误抛出异常
*
* @param $wxResult
* @throws WeChatException
*/
private function processLoginError($wxResult)
{
throw new WeChatException(
[
'msg' => $wxResult['errmsg'],
'errorCode' => $wxResult['errcode']
]);
}
}

生成token函数

1
2
3
4
5
6
7
8
 // 生成令牌
public static function generateToken()
{
$randChar = getRandChar(32);
$timestamp = $_SERVER['REQUEST_TIME_FLOAT'];
$tokenSalt = config('secure.token_salt');
return md5($randChar . $timestamp . $tokenSalt);
}

接口返回

接口正确返回token, 我们需要存储在客户端,以后的每次请求, 都需要在header头中带上token信息

1
2
3
4
wx.setStorage({
key:"token",
data:"value"
})

请求新的接口

  • 接口地址
    http://tp5.me/api/v1/order
  • 请求方式
    POST
  • 请求参数
    header头信息
    1
    {"token" : "b19d0f4e05a6f3898d52b5d181a867da"}
    body参数信息
    1
    2
    3
    4
    5
    6
    {
    "products": [
    {"product_id": 1, "count": 1},
    {"product_id": 2, "count": 1}
    ]
    }

接口权限验证

我们使用ThinkPHP 5 的前置方法在下单前检测用户的身份权限
tp5\application\api\controller\v1\Orders.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Orders extends BaseController
{
protected $beforeActionList = [
'checkExclusiveScope' => ['only' => 'placeOrder']
];

/**
* 下单
* @url /order
* @HTTP POST
*/
public function placeOrder()
{
(new OrderPlace())->goCheck();
$products = input('post.products/a');
$uid = Token::getCurrentUid();
$order = new OrderService();
$status = $order->place($uid, $products);
return $status;
}

tp5\application\api\controller\BaseController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace app\api\controller;

use app\api\service\Token;
use think\Controller;

class BaseController extends Controller
{
/**
* 用户专有权限检测
*/
protected function checkExclusiveScope()
{
Token::needExclusiveScope();
}

/**
* 最基本权限, token是否有效检测
*/
protected function checkPrimaryScope()
{
Token::needPrimaryScope();
}

/**
* 超级管理员权限检测
*/
protected function checkSuperScope()
{
Token::needSuperScope();
}
}

tp5\application\api\service\Token.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
<?php

namespace app\api\service;

use app\lib\enum\ScopeEnum;
use app\lib\exception\ForbiddenException;
use app\lib\exception\ParameterException;
use app\lib\exception\TokenException;
use think\Cache;
use think\Exception;
use think\Request;

class Token
{

/**
* 生成令牌, 发送给客户端
*
* @return string
*/
public static function generateToken()
{
$randChar = getRandChar(32);
$timestamp = $_SERVER['REQUEST_TIME_FLOAT'];
$tokenSalt = config('secure.token_salt');
return md5($randChar . $timestamp . $tokenSalt);
}

/**
* 根据请求header 头的 token值,去缓存中寻找对应的记录
*
* @param $key
* @return mixed
* @throws Exception
* @throws TokenException
*/
public static function getCurrentTokenVar($key)
{
$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变量并不存在');
}
}
}

/**
* 在缓存中根据token取出当前用户的id
*
* @return mixed
* @throws ParameterException
*/
public static function getCurrentUid()
{
$uid = self::getCurrentTokenVar('uid');

// 获取权限
$scope = self::getCurrentTokenVar('scope');

if ($scope == ScopeEnum::Super) {
// 只有Super权限才可以自己传入uid
// 且必须在get参数中,post不接受任何uid字段
$userID = input('get.uid');
if (!$userID) {
throw new ParameterException(
[
'msg' => '没有指定需要操作的用户对象'
]);
}
return $userID;
}
else {
return $uid;
}
}


/**
* 验证基本权限
*
* @return bool
* @throws ForbiddenException
* @throws TokenException
*/
public static function needPrimaryScope()
{
$scope = self::getCurrentTokenVar('scope');
if ($scope) {
if ($scope >= ScopeEnum::User) {
return true;
}
else{
throw new ForbiddenException();
}
} else {
throw new TokenException();
}
}

/**
* 用户专有权限
*
* @return bool
* @throws ForbiddenException
* @throws TokenException
*/
public static function needExclusiveScope()
{
$scope = self::getCurrentTokenVar('scope');
if ($scope ){
if ($scope == ScopeEnum::User) {
return true;
} else {
throw new ForbiddenException();
}
} else {
throw new TokenException();
}
}

/**
* 管理员权限
*
* @return bool
* @throws ForbiddenException
* @throws TokenException
*/
public static function needSuperScope()
{
$scope = self::getCurrentTokenVar('scope');
if ($scope){
if ($scope == ScopeEnum::Super) {
return true;
} else {
throw new ForbiddenException();
}
} else {
throw new TokenException();
}
}

}