首頁 >web前端 >uni-app >uni-app 小程式 Laravel+jwt 權限認證系列

uni-app 小程式 Laravel+jwt 權限認證系列

coldplay.xixi
coldplay.xixi轉載
2020-11-27 17:15:5512248瀏覽

uni-app開發教學欄位介紹系列權限認證的方法。

uni-app 小程式 Laravel+jwt 權限認證系列

推薦:funi-app開發教學

#環境說明

uni-app
laravel 5.7 jwt- auth 1.0.0

權限認證整體說明

  1. #設計表格結構
  2. ##前端request 類別
  3. #有關權限認證的js 封裝包含無感知刷新token
  4. laravel auth 中間件包含無感知刷新token
  5. #取得手機號碼登陸
  6. 無痛刷新access_token 思路
  7. 小程式如何判斷登陸狀態

設計表結構

和一般設計表沒有什麼區別,如果是多平台小程序,透過account_id 關聯聯合表。

CREATE TABLE `users` (
  `u_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '账号id',
  `u_username` varchar(15) NOT NULL DEFAULT '' COMMENT '手机号隐藏 ',
  `u_nickname` varchar(15) NOT NULL COMMENT '分配用户名',
  `u_headimg` varchar(200) DEFAULT NULL COMMENT '头像',
  `u_province` varchar(50) DEFAULT NULL,
  `u_city` varchar(50) DEFAULT NULL,
  `u_platform` varchar(30) NOT NULL COMMENT '平台:小程序wx,bd等',
  `u_mobile` char(11) NOT NULL COMMENT '手机号必须授权',
  `u_openid` varchar(100) DEFAULT NULL COMMENT 'openid',
  `u_regtime` timestamp NULL DEFAULT NULL COMMENT '注册时间',
  `u_login_time` timestamp NULL DEFAULT NULL COMMENT '最后登陆时间',
  `u_status` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '0禁用1正常',
  `account_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '平台联合id',
  PRIMARY KEY (`u_id`),
  KEY `platform` (`u_platform`,`u_mobile`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

2. 前端request 類別

一個較不錯的request 類別

luch-request ,支援動態修改配置、攔截器,在uni-app 插件市場可以找到。
uni-app 小程式 Laravel+jwt 權限認證系列其中 request.js 不需要更改。自訂邏輯在 index.js。

index.js

import Request from './request';import jwt from '@/utils/auth/jwt.js'; // jwt 管理 见下文const http = new Request();const baseUrl = 'http://xxx'; // api 地址var platform = ''; // 登陆时需知道来自哪个平台的小程序用户// #ifdef MP-BAIDUplatform = 'MP-BAIDU';// #endif/* 设置全局配置 */http.setConfig((config) => { 
  config.baseUrl = baseUrl; //设置 api 地址
  config.header = {
    ...config.header  }
  return config})/* 请求之前拦截器 */http.interceptor.request((config, cancel) => {
    if (!platform) {cancel('缺少平台参数');}
    config.header = {
        ...config.header,
        platform:platform    } 
  if (config.custom.auth) {
      // 需要权限认证的路由 需携带自定义参数 {custom: {auth: true}}
    config.header.Authorization = jwt.getAccessToken();
  }
  return config})http.interceptor.response(async (response) => { /* 请求之后拦截器 */
    console.log(response);
    // 如果是需要权限认证的路由
    if(response.config.custom.auth){

            if(response.data.code == 4011){
                // 刷新 token
                jwt.setAccessToken(response.data.data.access_token);
                // 携带新 token 重新请求
                let repeatRes = await http.request(response.config);
                if ( repeatRes ) {
                    response = repeatRes;
                }
            }

    }
    return response}, (response) => { // 请求错误做点什么
    if(response.statusCode == 401){
        getApp().globalData.isLogin = false;
        uni.showToast({icon:'none',duration:2000,title: "请登录"})
    }else if(response.statusCode == 403){
        uni.showToast({
            title: "您没有权限进行此项操作,请联系客服。",
            icon: "none"
        });
    }
  return response})export {
  http}

全域掛載

import Vue from 'vue'import App from './App'import { http } from '@/utils/luch/index.js' //这里Vue.prototype.$http = http

Vue.config.productionTip = falseApp.mpType = 'app'const app = new Vue({
    ...App})app.$mount()

3.有關權限認證的js 封裝

authorize.js

篇幅原因,沒有貼完整的程式碼,其他並沒有使用到。例如 uni.checkSession(),由於使用 jwt 接手了小程式的登陸態,所以目前沒有用到這個方法。

// #ifndef H5const loginCode = provider => {
    return new Promise((resolve, reject) => {
        uni.login({
            provider: provider,
            success: function(loginRes) {
                if (loginRes && loginRes.code) { resolve(loginRes.code) } else { reject("获取code失败") }
            },
            fail:function(){ reject("获取code失败")}
        });
    })}// #endifexport {
    loginCode //登录获取code}

jwt.js

專門管理 access_token 的,程式碼不多,同時將 userinfo 的管理也放在裡面。

const tokenKey = 'accessToken';//键值const userKey    = 'user'; // 用户信息// tokenconst getAccessToken = function(){
    let token='';
    try {token = 'Bearer '+ uni.getStorageSync(tokenKey);} catch (e) {}
    return token;}const setAccessToken = (access_token) => {
    try {uni.setStorageSync(tokenKey, access_token);return true;} catch (e) {return false;}}const clearAccessToken = function(){
    try {uni.removeStorageSync(tokenKey);} catch (e) {}}// userinfoconst setUser = (user)=>{
    try {uni.setStorageSync(userKey, user);return true;} catch (e) {return false;}}const getUser = function(){
    try {return uni.getStorageSync(userKey)} catch (e) {return false;}}const clearUser = function(){
    try {uni.removeStorageSync(userKey)} catch (e) {}}export default {
  getAccessToken,setAccessToken,clearAccessToken,getUser,setUser,clearUser}

auth.js

只處理login ,為什麼單獨放在一個文件,沒別的,因為到處都用到

import {loginCode} from '@/utils/auth/authorize.js';import jwt from '@/utils/auth/jwt.js';import {http} from '@/utils/luch/index.js';const login=function(detail){
    return new Promise((resolve, reject) => {
        loginCode().then(code=>{
            detail.code = code;
            return http.post('/v1/auth/login',detail);
        })
        .then(res=>{
            jwt.setAccessToken(res.data.data.access_token);
            jwt.setUser(res.data.data.user);
            getApp().globalData.isLogin = true;
            resolve(res.data.data.user);
        })
        .catch(err=>{
            reject('登陆失败')
        })
    })}export default {login}

4. laravel auth 中間件

這裡叨一點jwt-auth 方面的。 1,當一個token過期並進行了刷新token,那麼原token會被列在“黑名單”,即失效了。實際上 jwt-auth 也維護了一個檔案來儲存黑名單,而達到刷新時間上限才會清理失效的token。例如過期時間為10分鐘,刷新上限為一個月,這段期間會產生大量的黑名單,影響效能,所以盡量的調整,例如過期時間為60分鐘,刷新上限為兩週,或過期時間一周,刷新上限一個月都沒有問題的。 2,關於無痛刷新方案,當token過期時,我採用的前端兩次請求完成刷新,其中用戶是無感知的,網上有直接一次請求自動刷新並登陸的方案,我沒有採用,至於為什麼,沒別的,看不懂。不過我整理了各種 jwt 各種 exception ,需要的同學可以自訂。 TokenExpiredException 過期、TokenInvalidException 無法解析令牌、UnauthorizedHttpException 未攜帶令牌、JWTException 令牌失效或達到刷新上限或jwt內部錯誤。

<?phpnamespace  App\Http\Middleware;use App\Library\Y;use Closure;use Exception;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;class ApiAuth extends BaseMiddleware{

    public function handle($request, Closure $next, $guard = &#39;api&#39;)
    {
        // 在排除名单中 比如登录
        if($request->is(...$this->except)){
            return $next($request);
        }

        try {
            $this->checkForToken($request);// 是否携带令牌
            if ( $this->auth->parseToken()->authenticate() ) {
                return $next($request); //验证通过
            }
        }catch(Exception $e){
            // 如果token 过期
            if ($e instanceof TokenExpiredException) {
                try{
                    // 尝试刷新 如果成功 返给前端 关于前端如何处理的 看前边 index.js
                    $token = $this->auth->refresh();
                    return Y::json(4011, $e->getMessage(),['access_token'=>$token]);
                }catch(JWTException $e){
                    // 达到刷新时间上限
                    return Y::json(401, $e->getMessage());
                }
            }else{
                // 其他各种 直接返回 401 状态码 不再细分
                return Y::json(401, $e->getMessage());
            }
        }
    }

    protected $except = [
        'v1/auth/login',
    ];}
筆者認為這種刷新很不好維護,直接使用一次性token,過期直接重新登入比較好,視小程式或網站是否要求極強的安全性而定,一般不需求很高的安全性,https請求下一次性token更好,這裡的中間件只需要auth()->check(),true 即登錄狀態,false 即未登入。

#5. 取得手機號碼登陸
<template>
    <view>
        <button>获取手机号</button>
        <button>获取用户数据</button>
        <button>清除用户数据</button>
    </view></template><script>
    import auth from &#39;@/utils/auth/auth.js&#39;;
    import jwt from &#39;@/utils/auth/jwt.js&#39;;
    var _self;
    export default{
        data() {return {}},
        onLoad(option) {},
        onShow(){},
        methods: {
            decryptPhoneNumber: function(e){
                // console.log(e.detail);
                if( e.detail.errMsg == "getPhoneNumber:ok" ){ //成功
                    auth.login(e.detail);
                }
            },
            me: function(){
                this.$http.get(&#39;/v1/auth/me&#39;,{custom: {auth: true}}).then(res=>{
                    console.log(res,&#39;success&#39;)
                }).catch(err=>{
                    console.log(err,&#39;error60&#39;)
                })
            },
            clear: function(){
                jwt.clearAccessToken();
                jwt.clearUser();
                uni.showToast({
                    icon: &#39;success&#39;,
                    title: &#39;清除成功&#39;,
                    duration:2000,
                });
            }
        },
        components: {}
    }</script><style></style>

1 後端

// 登陆
    public function login(Request $request)
    {
        $platform = $request->header('platform');
        if(!$platform || !in_array($platform,User::$platforms)){
            return Y::json(1001, '不支持的平台类型');
        }
        $post = $request->only(['encryptedData', 'iv', 'code']);
        $validator = Validator::make($post, [
            'encryptedData' => 'required',
            'iv'            => 'required',
            'code'          => 'required'
        ]);
        if ($validator->fails()) {return Y::json(1002,'非法请求');}
        switch ($platform) {
            case 'MP-BAIDU':
                $decryption = (new BdDataDecrypt())->decrypt($post['encryptedData'],$post['iv'],$post['code']);
                break;
            default:
                $decryption = false;
                break;
        }
        // var_dump($decryption);
        if($decryption !== false){
            $user = User::where('u_platform',$platform)->where('u_mobile',$decryption['mobile'])->first();
            if($user){
                $user->u_login_time = date('Y-m-d H:i:s',time());
                $user->save();
            }else{
                $user = User::create([
                    'u_username'=> substr_replace($decryption['mobile'],'******',3,6),
                    'u_nickname'=> User::crateNickName(),
                    'u_platform'=> $platform,
                    'u_mobile'   => $decryption['mobile'],
                    'u_openid'  => $decryption['openid'],
                    'u_regtime' => date('Y-m-d H:i:s',time())
                ]);
            }

            $token = auth()->login($user);
            return Y::json(
                array_merge(
                    $this->respondWithToken($token),
                    ['user'=>['nickName'=>$user->u_nickname]]
                )
            );
        }
        return Y::json(1003,'登录失败'); 
    }
    // 返回 token
    protected function respondWithToken($token)
    {
        return ['access_token' => $token];
    }

手機號碼解密

<?phpnamespace  App\Library;use App\Library\Y;class BdDataDecrypt{

    private $_appid;
    private $_app_key;
    private $_secret;
    private $_session_key;

    public function __construct()
    {
        $this->_appid       = env('BD_APPID');
        $this->_app_key     = env('BAIDU_KEY');
        $this->_secret      = env('BD_SECRET');
    }

    public function decrypt($encryptedData, $iv, $code){
        $res = $this->getSessionKey($code);
        if($res === false){return false;}
        $data['openid'] = $res['openid'];
        $res = $this->handle($encryptedData,$iv,$this->_app_key,$res['session_key']);
        if($res === false){return false;}
        $res = json_decode($res,true);
        $data['mobile'] = $res['mobile'];
        return $data;

    }

    public function getSessionKey($code)
    {
        $params['code']         = $code;
        $params['client_id']     = $this->_app_key;
        $params['sk']             = $this->_secret;
        $res = Y::curl("https://spapi.baidu.com/oauth/jscode2sessionkey",$params,0,1);
        // var_dump($res);
        /**
         * 错误返回
         * array(3) {
            ["errno"]=>
            int(1104)
            ["error"]=>
            string(33) "invalid code , expired or revoked"
            ["error_description"]=>
            string(33) "invalid code , expired or revoked"
            }
            成功返回:
            array(2) {
                ["openid"]=>
                string(26) "z45QjEfvkUJFwYlVcpjwST5G8w"
                ["session_key"]=>
                string(32) "51b9297ababbcf43c1a099256bf82d75"
            }
         */
        if( isset($res['error']) ){
            return false;
        }
        return $res;
    }

    /**
     * 官方 demo
     * return string(24) "{"mobile":"18288881111"}" or false
     */
    private function handle($ciphertext, $iv, $app_key, $session_key)
    {
        $session_key = base64_decode($session_key);
        $iv = base64_decode($iv);
        $ciphertext = base64_decode($ciphertext);

        $plaintext = false;
        if (function_exists("openssl_decrypt")) {
            $plaintext = openssl_decrypt($ciphertext, "AES-192-CBC", $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        } else {
            $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null);
            mcrypt_generic_init($td, $session_key, $iv);
            $plaintext = mdecrypt_generic($td, $ciphertext);
            mcrypt_generic_deinit($td);
            mcrypt_module_close($td);
        }
        if ($plaintext == false) {
            return false;
        }
        // trim pkcs#7 padding
        $pad = ord(substr($plaintext, -1));
        $pad = ($pad  32) ? 0 : $pad;
        $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);
        $plaintext = substr($plaintext, 16);
        $unpack = unpack("Nlen/", substr($plaintext, 0, 4));
        $content = substr($plaintext, 4, $unpack['len']);
        $app_key_decode = substr($plaintext, $unpack['len'] + 4);
        return $app_key == $app_key_decode ? $content : false;
    }}

6. 無痛刷新access_token 想法

先說我使用的方法是,後端判斷token 過期後,自動嘗試刷新,刷新成功返回新的token,前端在回應攔截器裡,捕獲到後端回應的約定code,把新的token 存儲,並且緊接著二次請求,最終感知上是一次正常的請求。

另一個思路,後端嘗試刷新成功後,自動為當前用戶登陸,並在 header 中返回新 token,前端只負責儲存。

7. 小程序如何判断登陆状态

其实思路也很简单,非前后端分离怎么做的,前后端分离就怎么做,原理一样。非前后端分离,在每次请求时都会读取 session ,那么前后端分离,更好一些,有些公开请求不走中间件,也就无需判断登陆态,只有在需要权限认证的页面,在页面初始化时发出一次请求走中间件,以此判断登陆状态。
定义全局登陆检查函数

import jwt from '@/utils/auth/jwt.js';Vue.prototype.checkLogin = function(){
    var TOKEN  = jwt.getAccessToken();
    return new Promise((resolve, reject) => {
        if(TOKEN){
            http.get('/v1/auth/check',{custom: {auth: true}}).then(res=>{
                // 通过中间件 一定是登陆态
                resolve(true);
            }).catch(err=>{
                resolve(false);
                console.log(err) // 这里是401 403 后端500错误或者网络不好
            })
        }else{
            resolve(false) //没有token 一定是未登陆
        }
    })}

笔者最终放弃上面的这种检查登录的方式,直接检验storage中有user和token即视为登录状态。以被动的验证代替主动去验证,就是说用户执行一个请求,返回401,那么就改变登录状态。以后再补充。

前端

<script>
    export default {
        data() {
            return {
                isLogin:null
            }
        },
        onLoad() {
            this.checkLogin().then(loginStatus=>{
                this.isLogin = loginStatus;
            });
        },
        methods: {
        },
        components: {}
    }</script>

                                                       

以上是uni-app 小程式 Laravel+jwt 權限認證系列的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除