小兔网

uni-app开发教程栏目介绍系列权限认证的方法。

知识兔

uni

推荐:uni-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
其中 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 = httpVue.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 = 'api')    {        // 在排除名单中 比如登录        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 type="default" open-type="getPhoneNumber" @getphonenumber="decryptPhoneNumber">获取手机号</button>        <button @tap="me">获取用户数据</button>        <button @tap="clear">清除用户数据</button>    </view></template><script>    import auth from '@/utils/auth/auth.js';    import jwt from '@/utils/auth/jwt.js';    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('/v1/auth/me',{custom: {auth: true}}).then(res=>{                    console.log(res,'success')                }).catch(err=>{                    console.log(err,'error60')                })            },            clear: function(){                jwt.clearAccessToken();                jwt.clearUser();                uni.showToast({                    icon: 'success',                    title: '清除成功',                    duration:2000,                });            }        },        components: {}    }</script><style></style>

后端

// 登陆    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 < 1 || $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 权限认证系列的知识。速戳>>知识兔学习精品课!