基于开源系统 egg-beehive 移植的 小程序端鉴权记录

301 阅读3分钟

起因

  • 搭配的vue-beehive里的axios鉴权封装拷贝到uniapp时用不了

笔者最近在做一个用uniapp搭配uview开发的微信小程序,正在发愁是不是要重新搭建一个配套的权限管理系统时,突然想起很早之前在掘金收藏的一篇开源文章 【Node全栈项目开源】🚀 Vue + Egg.js + Mysql 的 JS全栈实践。动态菜单,RBAC权限模型,WebSocket实现站内信,这个系统很适合做二开,在鉴权这块做得还不错(对我来说,简直就是太棒了,想细致了解的可以在掘金搜下图的作者即可),然后就开始了魔改之路!

image.png

所遇问题

  • axios在小程序的宿主环境内不存在window,导致axios用不了
  • 按照beehive的作者方式去写时,session写入失败(原因是小程序端与浏览器的本地缓存不一致)
  • 小程序端加密时node-rsa加密报 Secure random number generation is not supported by this browser. Use Chrome
  • 改用JSEncrypt时遇到的问题与axios类似,也是缺失window环境,导致加密报错

问题1 axios 不能在小程序使用 寻找替代品 uview http库

  • 基于uview http 按照 vue-beehive封装
  • 各位大佬嘴下留情,几乎全部直接抄beehive的
import _ from 'lodash';
import qs from 'qs';
import { isArray, isNumber } from '@/utils/validate.js';
import store from '@/store/index.js';
const contentType =  'application/json;charset=UTF-8'
module.exports = (vm) => {
    // 初始化请求配置
    uni.$u.http.setConfig((config) => {
        /* config 为默认全局配置*/
        config.baseURL = `http://192.168.0.119:7001/api`; //宿舍
        // config.baseURL = `http://192.168.178.20:7001/api`; //教室
    	config.header = {
    		'Content-Type': contentType,
			'Authorization': '',
			'x-csrf-token': ''
    	};
		
		
        return config
    })
	const successCode = [200, 201, 204, 0];
	const noAuthenticationCode = 401;
	const invalidCode =  402;
	const invalidRequestCode = 400;
	const noPermissionCode =  403;
	const noFoundCode =  404;
	// 请求拦截
	uni.$u.http.interceptors.request.use((config) => { // 可使用async await 做异步操作
	
		
	    // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{}
	    config.data = config.data || {}
		if (store.getters['user/accessToken']) {
		      config.header['Authorization'] = store.getters['user/accessToken'];
		      config.header['x-csrf-token'] = store.getters['user/accessCsrf'];
		}
		// 如果config.data存在,且是Json
		if (config.data && Object.prototype.toString.call(config.data).slice(8, -1).toLowerCase() === 'object') {
		  //这里会过滤所有为null的key,如果不需要请自行注释
		  config.data = _.pickBy(config.data, item => !_.isNull(item));
		}
		//只针对get方式进行序列化
		if (config.method.toLowerCase() === 'get') {
		  //这里会过滤所有为null的key,如果不需要请自行注释
		  config.params = _.pickBy(config.params, item => !_.isNull(item));
		  // 对所有字符串参数做反斜杠处理,解决反斜杠参数搜索有误问题
		  for (const paramsKey in config.params) {
			if (_.isString(config.params[paramsKey])) {
			  config.params[paramsKey] = config.params[paramsKey].replace(/\\/g, '\\\\');
			}
		  }
		  config.paramsSerializer = function (params) {
			return qs.stringify(params, { arrayFormat: 'repeat' });
		  };
		}
		
		if (contentType === 'application/x-www-form-urlencoded;charset=UTF-8') {
			if (config.data && !config.data.param) {
			  config.data = qs.stringify(config.data);
			}
		}
	    return config 
	}, config => { // 可使用async await 做异步操作
	    return Promise.reject(config)
	})
	
	
	const errNotification = (
	  title = null,
	  duration = 2000
	) => {
	 uni.showToast({
	 	title,
		icon: 'error',
	 	duration
	 });
	};
	
	// 响应拦截
	uni.$u.http.interceptors.response.use(async (response) => { /* 对响应成功做点什么 可使用async await 做异步操作*/
		console.log(response,'响应拦截');
		const { statusCode, data, config } = response;
		// 如果是token过期,则刷新token并重试,如果刷新失败,则跳到登录
		if (statusCode === 202 && data.code === 401) {
		  try {
			await store.dispatch('user/refreshToken');
			return await uni.$u.http.request(config)
		  } catch (e) {
			await store.dispatch('user/resetAccessToken');
			uni.$u.route({
				type: 'switchTab',
				url: 'pages/my/my',
			})
			return Promise.reject();
		  }
		}
		const { code, msg } = data;
		let codeVerification = false;
		if (isNumber(successCode)) {
		  codeVerification = code !== successCode;
		}
		if (isArray(successCode)) {
		  for (let i = 0; i < successCode.length; i++) {
			if (code === i) {
			  codeVerification = code !== i;
			  break;
			}
		  }
		}
		
		if (codeVerification) {
		  switch (code) {
			case invalidCode:
			  // errNotification('错误', msg || `后端接口${code}异常`);
			  errNotification('错误',msg);
			  store.dispatch('user/resetAccessToken');
			  break;
			default:
			  errNotification('错误',msg);
			  break;
		  }
		  return Promise.reject('请求异常拦截:' + JSON.stringify({ url: config.url, code, msg }) || 'Error');
		} else {
		  return data;
		}
	}, (error) => { 
		// 对响应错误做点什么 (statusCode !== 200)
		console.log(error,'====错误');
		// console.log(error?.status,'====错误');
		console.log(noAuthenticationCode);
		console.log(error?.statusCode);
		switch (error?.statusCode) {
		      case invalidRequestCode: {
		        const data = error?.data;
		        switch (data.code) {
		          case 1451:
		            errNotification('操作失败,当前数据存在关联数据');
		            break;
		          case 1062:
		            data.data.detail.forEach(item => {
		              if (item.type === 'unique violation' && item.value) {
		                errNotification(`${item.value} 已经存在`);
		              }
		            });
		            break;
		          default:
		            if (data.error) {
		              errNotification(data.error);
		            } else {
		              errNotification(data.msg || `后端接口${error?.statusCode}异常`);
		            }
		            break;
		        }
		        break;
		      }
		      case noAuthenticationCode:
		        // await store.dispatch('user/resetAccessToken');
		        // router.push(`/login?redirect=${router.app.$route.fullPath}`);
		        if (error?.data?.msg) {
				  console.log(error?.data.msg);
		          errNotification('请登录');
				  setTimeout(() => {
					  uni.$u.route({
						type: 'switchTab',
						url: 'pages/my/my',
					  })
					}, 2000)
		        }
		        break;
		      case noPermissionCode:
			    errNotification('无权限');
		        break;
		      case noFoundCode:
				errNotification('找不到');
		        break;
		      default:
				console.log(error || `后端接口${error.request?.statusCode}异常`)
		        errNotification('错误11');
		        break;
		    }
		
		return Promise.reject(error)
	})
}

问题 把beehive 存储session的方式改为uniapp独有的 uni.getStorageSync

function getLocalJwt() {
    try {
      // return (localStorage.getItem(tokenTableName) &&      JSON.parse(localStorage.getItem(tokenTableName))[field]) || '';
      return (uni.getStorageSync(tokenTableName) && JSON.parse(uni.getStorageSync(tokenTableName))[field]) || '';
    } catch (e) {
      // localStorage.removeItem(tokenTableName);
      uni.removeStorageSync(tokenTableName);
      throw e;
    }
  }

问题3,4 解决node-rsa 加密报错

最好的解决办法,得不到就毁掉(没毛病,不兼容没法去处理,只能换库了)

更换为JSEncrypt时,报window is not defined,根据网上的提示去补环境,下图来源某客园 资深if-else侠 的文章截图

image.png 补充环境后小程序端 rsa加密可用,刷新token时一直提示 secret 不匹配,原因是后端解密用的node-rsa解密。前端都用了JSEncrypt,那为了支持小程序的鉴权登录,就只能再次修改后端的JSEncrypt源码做兼容写法了

总结:面向百度编程,cy工程师