後端選型
- overtrue/wechat 微信SDK
- hyperf/validation 驗證器
- firebase/php-jwt JWT元件
前端選型
本打算使用 wepy
來做小程式,但發現寫法又忘乾淨了。。所以選擇 uniapp
來做。
Uniapp
支援打包成各種小程式,但我們暫時只支援 微信小程式,所以程式碼設計中,暫不考慮其他情況。
建立使用者表
CREATE TABLE `users` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`nickname` varchar(256) NOT NULL DEFAULT '' COMMENT '暱稱',
`avatar` varchar(256) NOT NULL DEFAULT '' COMMENT '頭像',
`gender` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '1男',
`openid` varchar(64) NOT NULL DEFAULT '' COMMENT 'OPENID',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UNIQUE_OPENID` (`openid`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='使用者表';
建立模型
<?php
declare (strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace App\Model;
/**
* @property int $id
* @property string $nickname
* @property string $avatar
* @property int $gender
* @property string $openid
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class User extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'nickname', 'avatar', 'gender', 'openid', 'created_at', 'updated_at'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer', 'gender' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
}
登入流程
使用者註冊
增加對應路由
config.php
Router::post('/regist', 'App\Controller\UserController@regist');
實現微信登入
因為 EasyWeChat
的設計還是以 FPM
為基礎的,所以我們不能直接將 $app
存在記憶體中。
<?php
declare(strict_types=1);
namespace App\Kernel\Oauth;
use EasyWeChat\Factory;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Guzzle\CoroutineHandler;
use Hyperf\Guzzle\HandlerStackFactory;
use Overtrue\Socialite\Providers\AbstractProvider;
use Psr\Container\ContainerInterface;
class WeChatFactory
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->config = $container->get(ConfigInterface::class)->get('oauth.wechat');
// 設定 OAuth 授權的 Guzzle 配置
AbstractProvider::setGuzzleOptions([
'http_errors' => false,
'handler' => HandlerStack::create(new CoroutineHandler()),
]);
}
/**
* @return \EasyWeChat\MiniProgram\Application
*/
public function create()
{
$app = Factory::miniProgram($this->config);
// 設定 HttpClient,當前設定沒有實際效果,在資料請求時會被 guzzle_handler 覆蓋,但不保證 EasyWeChat 後面會修改這裡。
$config = $app['config']->get('http', []);
$config['handler'] = $this->container->get(HandlerStackFactory::class)->create();
$app->rebind('http_client', new Client($config));
// 重寫 Handler
$app['guzzle_handler'] = $this->container->get(HandlerStackFactory::class)->create();
return $app;
}
}
完善註冊
前端登入後把獲得的 code
,encrypted_data
和 iv
傳上來,後端進行解密,並儲存使用者。
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Request\LoginRequest;
use App\Request\RegistRequest;
use App\Service\Formatter\UserFormatter;
use App\Service\UserService;
use Hyperf\Di\Annotation\Inject;
class UserController extends Controller
{
public function regist(RegistRequest $request)
{
$code = (string) $request->input('code');
$encryptedData = (string) $request->input('encrypted_data');
$iv = (string) $request->input('iv');
[$token, $user] = $this->service->regist($code, $encryptedData, $iv);
return $this->response->success([
'token' => $token,
'user' => UserFormatter::instance()->base($user),
]);
}
}
以下 UserDao
的使用,只是一個資料庫操作的封裝,這裡不做介紹。
<?php
declare(strict_types=1);
namespace App\Service;
use App\Constants\ErrorCode;
use App\Exception\BusinessException;
use App\Kernel\Oauth\WeChatFactory;
use App\Service\Dao\UserDao;
use App\Service\Instance\JwtInstance;
use App\Service\Redis\UserCollection;
use Hyperf\Di\Annotation\Inject;
class UserService extends Service
{
/**
* @Inject
* @var WeChatFactory
*/
protected $factory;
/**
* @Inject
* @var UserDao
*/
protected $dao;
public function regist($code, $encrypted_data, $iv)
{
$app = $this->factory->create();
$session = $app->auth->session($code);
$userInfo = $app->encryptor->decryptData($session['session_key'], $iv, $encrypted_data);
$user = $this->dao->create($userInfo);
$token = JwtInstance::instance()->encode($user);
return [$token, $user];
}
}
另外這裡 Token
的存取直接使用 Jwt
來做,但真正開發時,就算使用 Jwt
來做授權,也要在後端驗證 Token
是否存在等邏輯,比如使用者 Jwt Token
被竊取,登出系統後,也可使 Token
作廢,及時止損。
<?php
declare(strict_types=1);
namespace App\Service\Instance;
use App\Constants\ErrorCode;
use App\Exception\BusinessException;
use App\Model\User;
use App\Service\Dao\UserDao;
use Firebase\JWT\JWT;
use Hyperf\Utils\Traits\StaticInstance;
class JwtInstance
{
use StaticInstance;
const KEY = 'NoteBook';
/**
* @var int
*/
public $id;
/**
* @var User
*/
public $user;
public function encode(User $user)
{
$this->id = $user->id;
$this->user = $user;
return JWT::encode(['id' => $user->id], self::KEY);
}
public function decode(string $token): self
{
try {
$decoded = (array) JWT::decode($token, self::KEY, ['HS256']);
} catch (\Throwable $exception) {
return $this;
}
if ($id = $decoded['id'] ?? null) {
$this->id = $id;
$this->user = di()->get(UserDao::class)->first($id);
}
return $this;
}
public function build(): self
{
if (empty($this->id)) {
throw new BusinessException(ErrorCode::TOKEN_INVALID);
}
return $this;
}
/**
* @return int
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return User
*/
public function getUser(): ?User
{
if ($this->user === null && $this->id) {
$this->user = di()->get(UserDao::class)->first($this->id);
}
return $this->user;
}
}