一、JWT与Dingo Api、手机短信验证码实现
关于JWT与Dingo Api的实现,可以参考本人最推崇的博主:你华还是你华
他的文章有详细的教程,以下是本人实现碰到的一些问题,仅供参考:
1.1 语言包问题
1.问题
在laravel-lang/lang - Packagist中没有看到install对应的版本,只看到composer require laravel-lang/lang,为此我输入该命令,导致无效,原因:生成zh_CN文件夹中并无有效的php文件,都是json文件。
2.过程:
然后我按实战视频输入:composer require laravel-lang/lang:~8.0,
报错:Class 'LaravelLang\Lang\ServiceProvider' not found
3.解决方法:
①:
临时法:composer require "laravel-lang/lang:~10.1.11"
再安装 overtrue 的:composer require "overtrue/laravel-lang"
②:暂时删除laravel-lang包:composer remove overtrue/laravel-lang
再:composer update
参考的是:https://www.zongscan.com/demo333/95349.html
1.2 生成的token,经过Auth::id()拿不到用户id,说明配置有问题
-
少了一步配置config->auth中的guards=>[]中加入:
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
2. 更新User模型
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
参考:https://learnku.com/laravel/wikis/25704
1.3 报错Failed to authenticate because of bad credentials or an invalid authorization header
//需要登录的路由
$api->group(['middleware'=>'api.auth'],function ($api){
//退出
$api->post('logout',[LoginController::class,'logout']);
//刷新token
$api->post('refresh',[LoginController::class,'refresh']);
代码中middleware有误
解决方法:代码中middleware应为auth:api
1.4 报错:Route[login] not defined
原因:用了auth的中间键,auth中间键里面校验了用户是否登录
解决方法:给login起个路由名。
二、手机短信验证码免密登录(首次登录即注册)
短信验证码的实现过程,我就不赘述了,参考博主: 你华还是你华
关于如何实现手机号码免密实现登录注册,我是直接改源码的,由于当时项目比较赶,此方法弊端在于,在删除所有依赖,重新composer install时,需要修改源码才行。
2.1 登录接口
use App\Http\Controllers\BaseController;
use App\Http\Requests\Auth\BindRequest;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\UserRequest;
use App\Http\Response\ApiResponse;
use App\Models\User;
use App\Transformers\User\UserTransformer;
use Dingo\Api\Http\Request;
use Illuminate\Support\Facades\Cache;
/**
* 用户登陆
*/
public function login(LoginRequest $request)
{
$credentials['type'] = $request->input('type');
switch ($request->input('type')) {
case 1://密码登录
$credentials['mobile'] = $request->input('mobile');
$credentials['password'] = $request->input('password');
//生成token
$token = auth('api')->attempt($credentials);
//token校验
if (!$token) throw new \Exception('手机号或密码错误!');
break;
case 2://手机验证码登录
$credentials['mobile'] = $request->input('mobile');
//验证code是否正确
User::checkMobileCache($credentials['mobile'], $request->input('code'), 1);
$is_exit = User::where('mobile', $credentials['mobile'])->first();
if ($is_exit === null) {
$user = new User();
$user->id = random_int(1000, 1099) . substr(time(), -6);
$user->nickname = $request->input('mobile') . '用户';
$user->mobile = $request->input('mobile');
$user->status = 1;
$user->pwd_status = 2;
$user->is_bind_mobile = 1;
$user->save();
}
//生成token
$token = auth('api')->attempt($credentials);
//token校验
if (!$token) throw new \Exception('手机号或验证码错误!');
break;
case 3://微信扫码登录
$uuid = $request->input('uuid');
$cache_key = 'login_' . $uuid;
if (!Cache::has($uuid)) return ApiResponse::success('二维码已过期,请重新刷新', 202);
if (!Cache::has($cache_key)) return ApiResponse::success('未登录', 201);
$open_id = Cache::get($cache_key);
$credentials['open_id'] = $open_id;
Cache::forget($uuid);
//生成token
$token = auth('api')->attempt($credentials);
//token校验
if (!$token) throw new \Exception('该用户已注销!');
break;
default:
throw new \Exception('登录方式异常!');
}
//检查用户信息
User::checkUser();
return $this->respondWithToken($token);
}
以上,是本人写的代码,仅供参考,直接复制,是没法直接用的,最上面是用的依赖,而login只是该类的一个方法。
2.2 修改源码
根据上述代码的'attempt'进行搜索,定位到如下:
然后找到:
然后再对retrieveByCredentials进行搜索定位如下:
之后,在该类找到:retrieveByCredentials()方法,首先先调整retrieveByCredentials()方法,记得做个备份。
本人的修改如下:主要是作了区分,type为1是手机号码密码登录,type为2是手机验证码登录,type为3是微信扫码登录。type为2,主要也是去掉了密码的验证,代码比较简单,很明细看得懂的。
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
//个人补充了type=2、3,手机短信验证码登录跳过检验password以及微信公众号扫码登录这一步
$type = $credentials['type'];
switch ($type){
case 1:
unset($credentials['type']);
if (empty($credentials) ||
(count($credentials) === 1 &&
Str::contains($this->firstCredentialKey($credentials), 'password'))) {
return;
}
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}
}
break;
case 2:
unset($credentials['type']);
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}
}
break;
case 3:
unset($credentials['type']);
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}
}
break;
default:
throw new \Exception('登录方式异常');
}
$credentials['type'] = $type;
return $query->first();
//原生代码
// if (empty($credentials) ||
// (count($credentials) === 1 &&
// Str::contains($this->firstCredentialKey($credentials), 'password'))) {
// return;
// }
// // First we will add each credential element to the query as a where clause.
// // Then we can execute the query and, if we found a user, return it in a
// // Eloquent User "model" that will be utilized by the Guard instances.
// $query = $this->newModelQuery();
//
// foreach ($credentials as $key => $value) {
// if (Str::contains($key, 'password')) {
// continue;
// }
//
// if (is_array($value) || $value instanceof Arrayable) {
// $query->whereIn($key, $value);
// } elseif ($value instanceof Closure) {
// $value($query);
// } else {
// $query->where($key, $value);
// }
// }
// return $query->first();
}
最后,也是在该类EloquentUserProvider,直接搜:validateCredentials()
根据type值进行调整即可:
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(UserContract $user, array $credentials)
{
//switch这条语句是hgh补充的。为了跳过密码验证
switch ($credentials['type']){
case 1:
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
case 2:
return true;
case 3:
return true;
default:
throw new \Exception('登录方式异常');
}
// 原生
// $plain = $credentials['password'];
//
// return $this->hasher->check($plain, $user->getAuthPassword());
}
要跳过密码,直接return true即可。
免密码登录注册优化(后补)
后续发现可以通过实例进行生成token,这样就不用上面这么麻烦,还需要改源码,很麻烦。
//通过实例,不要用DB,用模型
$instance_user = User::where('mobile',$mobile)->first();
//生成token
$token = auth('api')->login($instance_user);
三、简单封装一下laravel的响应格式
要求:由于本人的前端同事,要求成功响应值定为200,失败定为500,字段为:status_code,字段:message,要求为中文,他要直接抛出,数据存放在data里面,其次页码数据也要求在data里面,要求效果如下:
{
"message": "success",
"status_code": "200",
"data": {
"total": 11,
"rows": [
{
"id": 12,
"title": "产品21",
"content": "好使",
"picture": null,
"status": 1,
"created_at": 1704877313,
"updated_at": 1704877313
}
],
"count": 1,
"per_page": 10,
"current_page": 2,
"total_pages": 2
}
}
为此在他的基础之上,再加上本人对响应的要求是:从数据库查询的数据,再响应之前都要经过transform处理,以及没有数据的成功响应也不单单是状态码为200,而没有message、status_code,要求如下:
{
"message": "success",
"status_code": "200",
"data": {
"total": 0,
"rows": [],
"count": 0,
"per_page": 10,
"current_page": 1,
"total_pages": 1
}
}
由于这些是laravel框架自带的没法满足以上众多要求的,为此,我简单进行了封装。
3.1 安装league/fractal依赖包
安装的时候,注意php版本要求,目前,针对像本人这类从事laravel工作不久的新人,做新项目时,最好php版本8.0.2以上最佳(含8.0.2版本);
3.2 代码实现
在项目Http目录下新建Response目录,新建类ApiResponse.php(自定义)
代码如下:
<?php
namespace App\Http\Response;
use Illuminate\Http\JsonResponse;
use League\Fractal\Manager;
use League\Fractal\Resource\Collection;
class ApiResponse
{
/**
* 成功响应
* @param null $data
* @param string $message
* @param int $statusCode
*/
public static function successData($data = null, $message = 'Success', $statusCode = 200)
{
return self::response($data, $message, $statusCode);
}
/**
* 封装返回单条数据经transformation校验
* @param $model --单条查询结果响应
* @param $transformer --transformer
* @param $new_model --new_model
*/
public static function successOne($model,$transformer, $new_model = null)
{
if ($model === null) {
$model = $new_model;
}
$responseData = $transformer->transform($model);
return self::successData($responseData);
}
/**
* 封装返回多条数据经transformation校验
* @param $model
* @param $transformer
*/
public static function successMultiple($model, $transformer)
{
$fractal = new Manager();
$resource = new Collection($model->items(), $transformer);
$transformedModel = $fractal->createData($resource)->toArray()['data'];
$responseData = [
'message' => 'success',
'status_code' => '200',
'data' => [
'total' => $model->total(),
'rows' => $transformedModel,
'count' => $model->count(),
'per_page' => intval($model->perPage()),
'current_page' => $model->currentPage(),
'total_pages' => $model->lastPage(),
// 'links' => null,
],
];
return response()->json($responseData, 200);
}
/**
* 成功响应
* @param string $message
* @param int $statusCode
*/
public static function success($message = 'Success', $statusCode = 200)
{
return self::response(null, $message, $statusCode);
}
/**
* 失败
* @param string $message
* @param null $data
* @param int $statusCode
*/
public static function error($message = 'Error', $data = null, $statusCode = 500)
{
return self::response($data, $message, $statusCode);
}
protected static function response( $data, $message, $statusCode)
{
$response = [
'message' => $message,
'status_code' => $statusCode,
];
if (!is_null($data)) {
$response['data'] = $data;
}
return new JsonResponse($response, $statusCode);
}
}
代码说明:
successOne()方法
参数:
$model:为查询出来的单条数据
$transformer:经过transfor处理的类
$new_model:主要是防止$model数据为空时,报错,所以加上模型,保证数据为空时,也能进行过滤以及展示,只是数据内容为空而已
用法如下:
/**
* 用户详情
*/
public function show(Request $request)
{
$user_id = $request->input('id');
if (empty($user_id)) throw new \Exception('用户编号不能为空');
$users = User::find($user_id);
return ApiResponse::successOne($users,new UserTransformer(),new User());
}
---------------
successMultiple()方法:
参数同上,不过针对的是查询的数据是多条的情况
用法如下:
/**
* 用户列表
*/
public function index(Request $request)
{
$name = $request->input('name');
$mobile = $request->input('mobile');
$per_page = $request->input('per_page');
$users = User::when($name, function ($query) use ($name) {
$query->where('name', 'like', "%{$name}%");
})
->when($mobile, function ($query) use ($mobile) {
$query->where('mobile', 'like', "%{$mobile}%");
})
->paginate($per_page);
return ApiResponse::successMultiple($users,new UserTransformer());
}
其他的,就没必要赘述,比较简单。
3.3 laravel未授权响应内容编辑
需求:由于错误信息需要抛出到前端展示,为此,在校验是否token是否成功时,如果失败,JWT自带的是英文,本人想要的效果如下:
实现:
如上图,可以进行自定义。
四、总结
微信公众号的实现登录在上两篇文章中。 后续还会有更新,希望大家多多支持与鼓励,大家能在我这学到一点东西,这是我得荣幸!