6
0
Fork 0

小程序授权登录1.0

release
vine_liutk 2022-02-22 18:09:00 +08:00
parent 4a3ac1cbee
commit aa74028ca2
12 changed files with 556 additions and 1 deletions

View File

@ -67,3 +67,6 @@ ALIYUN_OSS_STS_HOST =
ALIYUN_SMS_ACCESS_ID=
ALIYUN_SMS_ACCESS_KEY=
ALIYUN_SMS_SIGN_NAME=
WECHAT_MINI_PROGRAM_APPID=
WECHAT_MINI_PROGRAM_SECRET=

View File

@ -0,0 +1,334 @@
<?php
namespace App\Endpoint\Api\Http\Controllers\Auth;
use App\Constants\Device;
use App\Endpoint\Api\Http\Controllers\Controller;
use App\Exceptions\BizException;
use App\Helpers\PhoneNumber;
use App\Models\SmsCode;
use App\Models\SocialiteUser;
use App\Models\User;
use App\Models\UserInfo;
use App\Rules\PhoneNumber as PhoneNumberRule;
use App\Services\SmsCodeService;
use EasyWeChat\Factory as EasyWeChatFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Overtrue\Socialite\SocialiteManager;
use Throwable;
class SocialiteAuthController extends Controller
{
/**
* 三方code登录
*/
public function codeAuth($provider, Request $request)
{
$input = $request->validate([
'code' => ['bail', 'required', 'string'],
]);
$code = $input['code'];
//获取第三方用户信息
$socialiteUser = $this->getSocialiteUserByCode($provider, $code);
//通过第三方用户信息登录已绑定账号
$token = $this->loginUser([
'socialite_type'=>$provider,
'socialite_id'=>$socialiteUser?->id,
], $request);
return response()->json([
'token' => $token?->plainTextToken,
]);
}
public function codeBindUser($provider, Request $request)
{
$type = $request->input('type', 'default');
$rules = [
'code' => ['bail', 'required', 'string'],
'inviter_code' => ['bail', 'nullable', 'string'],
];
switch ($type) {
case 'default'://手机号+密码
$rules = array_merge($rules, [
'phone' => ['bail', 'required', 'string'],
'password' => ['bail', 'required', 'string'],
]);
break;
case 'sms_code'://手机号+验证码
$rules = array_merge($rules, [
'phone' => ['bail', 'required', new PhoneNumberRule()],
'verify_code' => ['bail', 'required', 'string'],
]);
break;
case 'wechat_mini'://微信小程序解密手机号
$rules = array_merge($rules, [
'data' => ['bail', 'required', 'string'],
'iv' => ['bail', 'required', 'string'],
]);
break;
default://默认手机号+密码
$rules = array_merge($rules, [
'phone' => ['bail', 'required', 'string'],
'password' => ['bail', 'required', 'string'],
]);
break;
}
$input = $request->validate($rules);
$code = $input['code'];
//获取第三方用户信息
$socialiteUser = $this->getSocialiteUserByCode($provider, $code);
//绑定用户并返回token
$token = $this->bindUser([
'socialite_type'=>$provider,
'socialite_id'=>$socialiteUser?->id,
], $type ?? 'default', $request);
return response()->json([
'token' => $token?->plainTextToken,
]);
}
/**
* [目前支持:微信小程序]
*/
protected function getSocialiteUserByCode($provider, $code)
{
//获取第三方用户信息
$user = null;
$config = config('socialite', []);
$socialite = new SocialiteManager($config);
switch ($provider) {
case 'wechat-mini'://微信小程序
$user = $socialite->create('wehcat-mini')->userFromCode($code);
break;
default:
throw new BizException(404);
}
return $user;
}
/**
* 第三方登录现有绑定的用户
*
* @param [array] $socialite
* @param [Request] $request
*/
protected function loginUser(array $socialite, Request $request)
{
$token = null;
$socialiteUser = SocialiteUser::firstOrCreate($socialite);
$user = $socialiteUser->user;
if ($user) {
$user->last_login_at = now();
$user->last_login_ip = $request->realIp();
$user->save();
// 获取登录设备
$device = $request->header('client-app', Device::UNIAPP);
switch ($device) {
case Device::MERCHANT:
if ($user->userInfo?->agent_level < UserInfo::AGENT_LEVEL_VIP) {
throw new BizException('账户没有权限');
}
// 清理此用户的商户端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商户端令牌
$token = $user->createToken($device);
break;
case Device::DEALER:
if (!$user->isDealer()) {
throw new BizException('账户没有权限');
}
// 清理此用户的商户端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商户端令牌
$token = $user->createToken($device);
break;
default:
$device = Device::UNIAPP;
// 清理此用户的商城端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商城端令牌
$token = $user->createToken($device, ['mall']);
break;
}
}
return $token;
}
protected function bindUser(array $socialite, string $type, Request $request)
{
$token = null;
$socialiteUser = SocialiteUser::firstOrCreate($socialite);
$user = null;
$input = $request->input();
switch ($type) {
case 'default'://手机号+密码
$user = User::where('phone', $input['phone'])->first();
//手机号不存在,或者密码错误
if (! $user?->verifyPassword($input['password'])) {
throw new BizException(__('Incorrect account or password'));
}
break;
case 'sms_code'://手机号+验证码
app(SmsCodeService::class)->validate(
$input['phone'],
SmsCode::TYPE_REGISTER,
$input['verify_code']
);
$user = User::where('phone', $input['phone'])->first();
break;
case 'wechat_mini'://微信小程序解密手机号
//解密失败
$app = EasyWeChatFactory::miniProgram([
'app_id' => config('wechat.mini_program.default.app_id', ''),
'secret' => config('wechat.mini_program.default.secret', ''),
// 下面为可选项
// 指定 API 调用返回结果的类型array(default)/collection/object/raw/自定义类名
'response_type' => 'array',
'log' => [
'level' => 'debug',
'file' => storage_path('logs/wechat-mini.log'),
],
]);
$session = Cache::get($socialite['socialite_id']);
try {
$decryptedData = $app->encryptor->decryptData($session, $input['iv'], $input['data']);
} catch (\EasyWeChat\Kernel\Exceptions\DecryptException $e) {
return $this->error('系统错误, 请重新进入小程序');
}
$phone = data_get($decryptedData, 'phoneNumber');
//解密成功,$user
$user = User::where('phone', $phone)->first();
break;
}
//走登录逻辑
if ($user) {
$user->last_login_at = now();
$user->last_login_ip = $request->realIp();
$user->save();
// 获取登录设备
$device = $request->header('client-app', Device::UNIAPP);
switch ($device) {
case Device::MERCHANT:
if ($user->userInfo?->agent_level < UserInfo::AGENT_LEVEL_VIP) {
throw new BizException('账户没有权限');
}
// 清理此用户的商户端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商户端令牌
$token = $user->createToken($device);
break;
case Device::DEALER:
if (!$user->isDealer()) {
throw new BizException('账户没有权限');
}
// 清理此用户的商户端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商户端令牌
$token = $user->createToken($device);
break;
default:
$device = Device::UNIAPP;
// 清理此用户的商城端令牌
$user->tokens()->where('name', $device)->delete();
// 颁发新的商城端令牌
$token = $user->createToken($device, ['mall']);
break;
}
} else {//走注册逻辑
$time = now();
$ip = $request->realIp();
$inviter = $this->findUserByCode((string) Arr::get($input, 'code'));
try {
DB::beginTransaction();
$user = User::create(
array_merge($input, [
'phone_verified_at' => $time,
'register_ip' => $ip,
'last_login_at' => $time,
'last_login_ip' => $ip,
]),
$inviter
);
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
report($e);
throw new BizException(__('Registration failed, please try again'));
}
// 获取登录设备
$device = $request->header('client-app', Device::UNIAPP);
switch ($device) {
case Device::DEALER:
$token = $user->createToken(Device::DEALER);
break;
default:
$token = $user->createToken(Device::UNIAPP, ['mall']);
break;
}
}
//解绑以前的关系
SocialiteUser::where('user_id', $user->id)->update([
'user_id' => null,
]);
//绑定用户和三方信息关系
$socialiteUser->update([
'user_id' => $user->id,
]);
return $token;
}
/**
* 通过邀请码搜索用户
*
* @param string $code
* @return \App\Models\User|null
*
* @throws \App\Exceptions\BizException
*/
protected function findUserByCode(string $code): ?User
{
if ($code === '') {
return null;
}
$user = User::when(PhoneNumber::validate($code), function ($query) use ($code) {
$query->where('phone', $code);
}, function ($query) use ($code) {
$query->whereRelation('userInfo', 'code', $code);
})->first();
if ($user === null) {
throw new BizException(__('Inviter does not exist'));
}
return $user;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\Endpoint\Api\Http\Controllers;
use Illuminate\Http\Request;
class WechatMiniController extends Controller
{
public function decrypt(Request $request)
{
}
}

View File

@ -10,6 +10,7 @@ use App\Endpoint\Api\Http\Controllers\AfterSaleController;
use App\Endpoint\Api\Http\Controllers\AliOssController;
use App\Endpoint\Api\Http\Controllers\AppVersionController;
use App\Endpoint\Api\Http\Controllers\ArticleController;
use App\Endpoint\Api\Http\Controllers\Auth;
use App\Endpoint\Api\Http\Controllers\Auth\LoginController;
use App\Endpoint\Api\Http\Controllers\Auth\LogoutController;
use App\Endpoint\Api\Http\Controllers\Auth\RegisterController;
@ -85,6 +86,14 @@ Route::group([
//获取配置
Route::get('configs', [SettingController::class, 'index']);
//三方登录聚合
Route::group([
'prefix' =>'socialite',
], function () {
Route::post('code-auth/{provider}', [Auth\SocialiteAuthController::class, 'codeAuth']);
Route::post('code-bind-user/{provider}', [Auth\SocialiteAuthController::class, 'codeBindUser']);
});
Route::middleware(['auth:api'])->group(function () {
// 我的信息
Route::get('me', [UserController::class, 'show']);

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SocialiteUser extends Model
{
use HasFactory;
protected $fillable = [
'socialite_type',
'socialite_id',
'user_id',
];
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -9,6 +9,7 @@ use EasyWeChat\Payment\Application as EasyWeChatPaymentApplication;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Overtrue\EasySms\EasySms;
use Symfony\Component\HttpFoundation\HeaderUtils;

View File

@ -0,0 +1,76 @@
<?php
namespace App\Providers\Socialite;
use App\Exceptions\BizException;
use EasyWeChat\Factory as EasyWeChatFactory;
use Illuminate\Support\Facades\Cache;
use Overtrue\Socialite\Providers\Base;
use Overtrue\Socialite\User;
class WechatMini extends Base
{
/**
* Undocumented function
*
* @param string $code
* @return \Overtrue\Socialite\User
*/
public function userFromCode(string $code): User
{
$app = EasyWeChatFactory::miniProgram([
'app_id' => $this->config->get('client_id'),
'secret' => $this->config->get('client_secret'),
// 下面为可选项
// 指定 API 调用返回结果的类型array(default)/collection/object/raw/自定义类名
'response_type' => 'array',
'log' => [
'level' => 'debug',
'file' => storage_path('logs/wechat-mini.log'),
],
]);
$result = $app->auth->session($code);
if ($result) {
if (data_get($result, 'errcode')) {
throw new BizException(data_get($result, 'errmsg'));
}
//缓存微信小程序会话密钥48小时
Cache::put($result['openid'], $result['session_key'], 48 * 60 * 60);
return $this->mapUserToObject([
'openid'=>$result['openid'],
// 'unionid'=>$result['unionid'],
]);
} else {
throw new BizException('解析失败');
}
}
protected function getAuthUrl(): string
{
return '';
}
protected function getTokenUrl(): string
{
return '';
}
protected function getUserByToken(string $token): array
{
return [];
}
protected function mapUserToObject(array $user): User
{
return new User([
'id' => $user['openid'] ?? null,
'name' => $user['nickname'] ?? null,
'nickname' => $user['nickname'] ?? null,
'avatar' => $user['headimgurl'] ?? null,
'email' => null,
]);
}
}

View File

@ -26,6 +26,7 @@
"laravel/sanctum": "^2.12",
"laravel/tinker": "^2.5",
"overtrue/easy-sms": "^2.0",
"overtrue/socialite": "*",
"simplesoftwareio/simple-qrcode": "^4.2",
"tucker-eric/eloquentfilter": "^3.0",
"w7corp/easywechat": "^5.10"

2
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f53d3bea81e27c131da9f29019c886a1",
"content-hash": "5ea856539b7c9cb2093859052437846e",
"packages": [
{
"name": "adbario/php-dot-notation",

View File

@ -0,0 +1,12 @@
<?php
use App\Providers\Socialite\WechatMini;
return [
'wehcat-mini'=>[
'provider' => WechatMini::class,
'client_id' => env('WECHAT_MINI_PROGRAM_APPID', ''),
'client_secret' => env('WECHAT_MINI_PROGRAM_SECRET', ''),
'redirect' =>'',
],
];

View File

@ -1,6 +1,54 @@
<?php
return [
/*
* 公众号
*/
'official_account' => [
'default' => [
'app_id' => env('WECHAT_OFFICIAL_ACCOUNT_APPID', 'your-app-id'), // AppID
'secret' => env('WECHAT_OFFICIAL_ACCOUNT_SECRET', 'your-app-secret'), // AppSecret
'token' => env('WECHAT_OFFICIAL_ACCOUNT_TOKEN', 'your-token'), // Token
'aes_key' => env('WECHAT_OFFICIAL_ACCOUNT_AES_KEY', ''), // EncodingAESKey
/*
* OAuth 配置
*
* scopes公众平台snsapi_userinfo / snsapi_base开放平台snsapi_login
* callbackOAuth授权完成后的回调页地址(如果使用中间件,则随便填写。。。)
* enforce_https是否强制使用 HTTPS 跳转
*/
'oauth' => [
'scopes' => array_map('trim', explode(',', env('WECHAT_OFFICIAL_ACCOUNT_OAUTH_SCOPES', 'snsapi_userinfo'))),
'callback' => env('WECHAT_OFFICIAL_ACCOUNT_OAUTH_CALLBACK', '/examples/oauth_callback.php'),
'enforce_https' => true,
],
],
],
/*
* 开放平台第三方平台
*/
// 'open_platform' => [
// 'default' => [
// 'app_id' => env('WECHAT_OPEN_PLATFORM_APPID', ''),
// 'secret' => env('WECHAT_OPEN_PLATFORM_SECRET', ''),
// 'token' => env('WECHAT_OPEN_PLATFORM_TOKEN', ''),
// 'aes_key' => env('WECHAT_OPEN_PLATFORM_AES_KEY', ''),
// ],
// ],
/*
* 小程序
*/
'mini_program' => [
'default' => [
'app_id' => env('WECHAT_MINI_PROGRAM_APPID', ''),
'secret' => env('WECHAT_MINI_PROGRAM_SECRET', ''),
'token' => env('WECHAT_MINI_PROGRAM_TOKEN', ''),
'aes_key' => env('WECHAT_MINI_PROGRAM_AES_KEY', ''),
],
],
'payment' => [
'default' => [
'sandbox' => env('WECHAT_PAYMENT_SANDBOX', false),

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSocialiteUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('socialite_users', function (Blueprint $table) {
$table->id();
$table->string('socialite_type')->comment('平台');
$table->string('socialite_id')->comment('三方唯一ID');
$table->unsignedBigInteger('user_id')->nullable()->comment('绑定的用户ID');
$table->timestamps();
$table->index(['socialite_type', 'socialite_id']);
$table->index('user_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('socialite_users');
}
}