小程序授权登录1.0
parent
4a3ac1cbee
commit
aa74028ca2
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\Endpoint\Api\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class WechatMiniController extends Controller
|
||||
{
|
||||
public function decrypt(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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' =>'',
|
||||
],
|
||||
];
|
||||
|
|
@ -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
|
||||
* callback:OAuth授权完成后的回调页地址(如果使用中间件,则随便填写。。。)
|
||||
* 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),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue