uniapp小程序laravel+jwt权限认证完整系列
环境说明
uni-app
laravel 5.7 + jwt-auth 1.0.0
权限认证整体说明
- 设计表结构
- 前端 request 类
- 有关权限认证的 js 封装 包含无感知刷新 token
- laravel auth 中间件 包含无感知刷新 token
- 获取手机号登陆
- 无痛刷新 access_token 思路
- 小程序如何判断登陆状态
设计表结构
和一般设计表没有什么区别,如果是多平台小程序,通过 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 插件市场可以找到。

其中 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-BAIDU
platform = '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 !== 0){if(response.data.code == 4011){// 刷新 tokenjwt.setAccessToken(response.data.data.access_token);// 携带新 token 重新请求let repeatRes = await http.request(response.config);if ( repeatRes ) {response = repeatRes;}}else if(response.data.code == 401){// 登陆态失效则清除token:没有携带token、token无法再刷新jwt.clearAccessToken();jwt.clearUser();}}}return response}, (response) => { // 请求错误做点什么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 = false
App.mpType = 'app'
const app = new Vue({...App
})
app.$mount()
3.有关权限认证的 js 封装
authorize.js
篇幅原因,没有贴完整的代码,其他并没有使用到。比如 uni.checkSession(),由于使用 jwt 接管了小程序的登陆态,所以目前没有用到这个方法。
// #ifndef H5
const 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失败")}});})
}
// #endif
export {loginCode //登录获取code
}
jwt.js
专门管理 access_token 的,代码不多,同时将 userinfo 的管理也放在里面。
const tokenKey = 'accessToken';//键值
const userKey = 'user'; // 用户信息// token
const 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) {}
}// userinfo
const 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,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);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内部错误。
namespace 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',];
}
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(){_self.$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,'登录失败'); }// 返回 tokenprotected function respondWithToken($token){return ['access_token' => $token];}
手机号码解密
namespace 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=>{// 走中间件 正常的响应只有三种状态码 未登陆返回 401未登陆 或者 4011 刷新token 其他自定义状态码一定是登陆态if(res.data.code == 401){resolve(false);return;}resolve(true);}).catch(err=>{resolve(false);console.log(err) // 这里是后端500错误或者网络不好})}else{resolve(false) //没有token 一定是未登陆}})
}
前端
<script>export default {data() {return {isLogin:null}},onLoad() {this.checkLogin().then(loginStatus=>{this.isLogin = loginStatus;});},methods: {},components: {}}
</script>
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
