Laravel 微信小程序授权登陆

Laravel 微信小程序授权登陆

安装依赖

1
$ composer require overtrue/wechat:~4.0

获取用户信息我们需要通过 button 拿到 ivencryptedData,而则两个却是密文的,我们需要通过 wx.login 拿到 code 去兑换到 session_key 来做一个解密。


2020.03.30 更新:

之前由于为了方便整合成了一个接口,即 ivencryptedDatacode 通过一个接口传过来然后兑换到 session_key 在去解密其他,这个过程中会有低概率遇到解密失败,错误信息 The given payload is invalid.

为了避免这个错误我们需要分成两个接口,第一个接口实现通过 code 兑换到 session_keyopenid ,下发一个临时鉴权令牌来识别身份。第二个接口需要加入路由中间价鉴权,对临时令牌进行验证后通过 ivencryptedData 和前面接口拿到的 session_key 进行解密,然后保存用户信息到数据库,同时下发正式鉴权令牌。为了避免复杂性这边不区分临时和正式鉴权令牌。

数据库设计

database\migrations\2014_10_12_000000_create_users_table.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
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
protected $table = 'users';

/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if(Schema::hasTable($this->table)) {
return true;
}

Schema::create($this->table, function (Blueprint $table) {
$table->increments('id');
$table->string('openid', 32)->unique()->comment('openid');
$table->string('session_key', 50)->nullable()->comment('session_key');
$table->string('avatar', 255)->nullable()->comment('头像');
$table->string('nickname', 255)->index()->comment('昵称');
$table->unsignedTinyInteger('sex')->nullable()->default(1)->comment('性别 0 未知 1 男 2女');
$table->string('tel', 11)->index()->nullable()->comment('手机');
$table->unsignedTinyInteger('status')->default(1)->comment('状态');
$table->string('token', 32)->nullable()->unique()->comment('令牌');
$table->timestamps();
$table->softDeletes();
});

\DB::statement("ALTER TABLE `$this->table` comment '用户表'");
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists($this->table);
}
}

鉴权控制器

app\Http\Controllers\AuthController.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
<?php

namespace App\Http\Controllers;

use EasyWeChat\Factory;
use App\Models\Users;
use App\Http\Controllers\Controller;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\Request;

class AuthController extends Controller
{
protected $app = null;

public function __construct()
{
$wechat_config = [
'app_id' => 'app_id',
'secret' => 'secret'
];

$this->app = Factory::miniProgram($wechat_config);
}

/**
* 用户登陆授权
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$input = $request->only('iv', 'encryptedData');

$validator = Validator::make($input, [
'iv' => 'required',
'encryptedData' => 'required',
]);

if($validator->fails()) {
return error((string) $validator->errors()->first());
}

$session_key = $request->user['session_key'];

if(!$session_key) {
return error('session_key 不存在');
}

try {
$data = $this->app->encryptor->decryptData(
$session_key,
$input['iv'],
$input['encryptedData']
);
} catch (\Exception $e) {
return error('session_key 无效');
}

if(!isset($data['openId'])) {
return error('获取 openid 错误');
}

$model = Users::where('openid', $data['openId'])
->first();

$model->nickname = $data['nickName'];
$model->avatar = $data['avatarUrl'];
$model->token = Str::random(32);

if(!$model->save()) {
return error_system();
}

return result([
'user' => [
'nickname' => $model->nickname,
'avatar' => $model->avatar,
],
'token' => $model->token,
]);
}

/**
* code
* code 兑换 session_key 和 openid ,如果 openid 用户不存在创建一个新的
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function code(Request $request)
{
$input = $request->only('code');

$validator = Validator::make($input, [
'code' => 'required',
]);

if($validator->fails()) {
return error((string) $validator->errors()->first());
}

try {
$data = $this->app->auth->session($input['code']);
} catch (\Exception $e) {
return error();
}

if(isset($data['errcode'])) {
switch ($data['errcode']) {
case 40029:
// 这里需要注意的是 code 只能换取一次
// 如果一直报 40029 需要前后端检查 appid 是否一致
return error('code 无效');
break;

default:
return error('请求频繁');
break;
}
}

$user = Users::where('openid', $data['openid'])
->first();

if(!$user) {
$user = new Users;
$user->openid = $data['openid'];
}

$user->session_key = $data['session_key'];
$user->token = Str::random(32);

if(!$user->save()) {
return error_system();
}

return result([
'token' => $user->token,
]);
}
}

辅助函数

app\helpers.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
<?php

if(!function_exists('result')) {
/**
* 返回数据
*
* @param string|array $data 数据
* @param int $status 状态码
* @return \Illuminate\Http\Response
*/
function result($data = null, int $status = 200) {
return response()->json(
is_null($data) ? ['code' => 1] : ['code' => 1, 'data' => $data],
$status
);
}
}

if(!function_exists('error')) {
/**
* 返回错误
*
* @param string $msg 错误信息
* @param int $status 状态码
* @return \Illuminate\Http\Response
*/
function error($msg = '参数错误', int $status = 200) {
return response()->json(
['code' => 0, 'message' => $msg],
$status
);
}
}

if(!function_exists('error_system')) {
/**
* 返回系统错误
*
* @param int $number 编号
* @return \Illuminate\Http\Response
*/
function error_system($number = null) {
return error(is_null($number) ? '系统错误' : '系统错误[' . $number . ']');
}
}

?>

辅助函数自动加载

composer.json

1
2
3
4
5
6
"autoload": {
...
"files": [
"app/helpers.php"
]
},
1
$ composer du

鉴权中间件

app\Http\Middleware\WechatMiddleware.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
<?php

namespace App\Http\Middleware;

use App\Models\Users;
use Closure;

class WechatMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$token = $request->header('Authorization');

$user = Users::where('token', $token)
->first();

if(!$user) {
return error('请登陆', 401);
}

if($user->status !== 1) {
return error('当前账户暂时无法使用');
}

$request->user = $user->toArray();
$request->uid = $user->getKey();

return $next($request);
}
}

中间件注册

app\Http\Kernel.php

1
2
3
4
protected $routeMiddleware = [
...
'wechat' => \App\Http\Middleware\WechatMiddleware::class,
];

路由

routes\web.php

1
2
3
4
5
Route::post('auth/code', 'AuthController@code');

Route::middleware('wechat')->group(function() {
Route::post('auth', 'AuthController@index');
});

POST 请求会触发 CSRF 验证,这里把它关了

app\Http\Middleware\VerifyCsrfToken.php

1
2
3
protected $except = [
'*',
];

小程序示例

index.wxml

1
2
3
<view class="container">
<button open-type="getUserInfo" bindgetuserinfo="handleGetUserInfo"> 登陆 </button>
</view>

index.js

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
Page({
data: {},

handleGetUserInfo (e) {
const self = this;
const data = e.detail;

if(Object.keys(data).indexOf('encryptedData') === -1) {
return wx.showToast({
title: '获取用户授权失败',
icon: 'none',
});
}

wx.checkSession({
success () {
self.handleLogin(data);
},
fail () {
wx.login({
success (res) {
if(!res.code) {
return wx.showToast({
title: '获取 code 失败',
icon: 'none',
});
}

self.handleGetToken(res.code).then(() => {
self.handleLogin(data);
});
},
});
},
});
},

handleLogin (data) {
wx.request({
url: 'https://example.com/wechat_api/auth',
method: 'POST',
data: {
iv: data.iv,
encryptedData: data.encryptedData,
},
header: {
'Authorization': wx.getStorageSync('token'),
},
success (res) {
if(Object.keys(res.data).indexOf('message') !== -1) {
return wx.showModal({
title: '提示',
content: res.data.message,
showCancel: false,
});
}

const userinfo = res.data.data.user;
const token = res.data.data.token;

wx.setStorage({
key: 'token',
data: token,
});

console.log(userinfo, token);
},
});
},

handleGetToken (code) {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://example.com/wechat_api/auth/code',
method: 'POST',
data: {
code,
},
success (res) {
if(Object.keys(res.data).indexOf('message') !== -1) {
return wx.showModal({
title: '提示',
content: res.data.message,
showCancel: false,
});
}

wx.setStorage({
key: 'token',
data: res.data.data.token,
});

resolve();
}
});
});
},
})
往上