laravel-JWT与Dingo Api、阿里云短信实现登录、注册、Token、免密注册登录、数据响应格式封装

一、JWT与Dingo Api、手机短信验证码实现

        关于JWT与Dingo Api的实现,可以参考本人最推崇的博主:你华还是你华

文档:Dingo API2.0.0中文文档

他的文章有详细的教程,以下是本人实现碰到的一些问题,仅供参考:

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,说明配置有问题

  1. 少了一步配置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自带的是英文,本人想要的效果如下:

        

        实现:

        如上图,可以进行自定义。

四、总结

            微信公众号的实现登录在上两篇文章中。 后续还会有更新,希望大家多多支持与鼓励,大家能在我这学到一点东西,这是我得荣幸!

  • 32
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现用户手机号验证码登录可以分为以下几个步骤: 1. 用户输入手机号和验证码,点击登录按钮。 2. 后端接收到手机号和验证码后,先验证验证码是否正确。 3. 如果验证码正确,后端生成JWT token并将token存储到Redis中,同时将token返回给前端。 4. 前端将token存储到本地,以便后续请求时使用。 5. 后续请求时,前端需要在请求头中加入token,后端通过解析token来判断用户是否已登录。 下面是具体实现过程: 1. 在阿里云短信控制台创建短信模板,获取accessKeyId和accessKeySecret。 2. 在Spring Boot项目中添加依赖: ``` <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.0.3</version> </dependency> ``` 3. 实现发送短信验证码的接口: ``` @PostMapping("/sendSms") public Result sendSms(@RequestParam("phone") String phone) { // 生成随机验证码 String code = String.valueOf((int) ((Math.random() * 9 + 1) * 100000)); // 发送短信验证码 DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); IAcsClient client = new DefaultAcsClient(profile); CommonRequest request = new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain("dysmsapi.aliyuncs.com"); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("RegionId", "cn-hangzhou"); request.putQueryParameter("PhoneNumbers", phone); request.putQueryParameter("SignName", "短信签名"); request.putQueryParameter("TemplateCode", "短信模板编号"); request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}"); try { CommonResponse response = client.getCommonResponse(request); // 将验证码存储到Redis中,有效期为5分钟 redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES); return Result.success("短信验证码发送成功"); } catch (Exception e) { return Result.error("短信验证码发送失败"); } } ``` 4. 实现用户手机号验证码登录接口: ``` @PostMapping("/login") public Result login(@RequestParam("phone") String phone, @RequestParam("code") String code) { // 验证验证码是否正确 String redisCode = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisCode)) { return Result.error("验证码已过期,请重新发送"); } if (!redisCode.equals(code)) { return Result.error("验证码不正确"); } // 生成JWT token,并存储到Redis中 String token = JwtUtils.generateToken(phone); redisTemplate.opsForValue().set(phone, token, 1, TimeUnit.DAYS); // 将token返回给前端 return Result.success(token); } ``` 5. 实现JWT token的生成和解析: ``` public class JwtUtils { private static final String SECRET_KEY = "jwt_secret_key"; // JWT密钥 private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000; // JWT过期时间(7天) public static String generateToken(String phone) { Date now = new Date(); Date expiration = new Date(now.getTime() + EXPIRATION_TIME); return Jwts.builder() .setSubject(phone) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getPhoneFromToken(String token) { try { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); return claims.getSubject(); } catch (Exception e) { return null; } } } ``` 6. 在拦截器中验证token并获取用户信息: ``` public class JwtInterceptor implements HandlerInterceptor { private static final String AUTH_HEADER = "Authorization"; // token在请求头中的名称 @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(AUTH_HEADER); if (StringUtils.isBlank(token)) { throw new BusinessException("未登录登录已过期"); } String phone = JwtUtils.getPhoneFromToken(token); if (StringUtils.isBlank(phone)) { throw new BusinessException("无效的token"); } String redisToken = redisTemplate.opsForValue().get(phone); if (StringUtils.isBlank(redisToken) || !redisToken.equals(token)) { throw new BusinessException("未登录登录已过期"); } return true; } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值