登录鉴权

登录鉴权常见业务处理,包括基本登录流程、记住密码、验证码登录、权限控制

Posted by page on February 13, 2022

登录鉴权

基本登录处理

登录流程

  1. 校验并提交登录表单

    { 
        username: "xxx", 
        password: "xxx" 
    }
    
  2. 登录请求(request)成功,返回auth_token

  3. 存储autn_token串(localStorage),跳转至首页

  4. 所有请求的header头的PcAuthToken字段值为auth_token值

  5. 检测登录状态

    auth.js:Vue Router添加前置路由(beforeEach)处理,对白名单(whiteList)之外路径跳转时判断登录状态

    request.js:响应错误判断,返回http状态码403或其它约定登录失效码,检测为’登录过期’

注: auth_token也可以由服务端直接存取至cookie中,前端只需检测响应错误判断;

独立登录处理

将登录/退出登录处理逻辑独立到Vuex中供全局调用

// ./store/user.js

// getters
userInfo: state => state.userInfo,
roleInfo: state => state.roleInfo,
loginState: () => !!window.localStorage.getItem("auth_token") ? "online" : "offline",

// mutations
SET_USER_INFO(state, userInfo) {
    state.userInfo = userInfo;
},
SET_ROLE_INFO(state, { roleNames, roleList }) {
    // ...other roleInfo handle
    state.roleInfo = { roleNames, roleList: list };
    setAdminRoutes($router);
},
CLAER_ALL_INFO() {
    window.localStorage.removeItem('auth_token');
    clearAdminRoutes($router);
    state.userInfo = null;
    state.roleInfo = null;
}

// actions
login(state,token){
    return api.login(form).then((res) => {
      // ...other userInfo handle
      window.localStorage.setItem('auth_token', jwtToken);
      context.commit('SET_USER_INFO', {...});
      context.commit('SET_ROLE_INFO', {...});
    });
},
logout(state, token){
    // ...other userInfo handle
    api.logout().finally(() => context.commit('CLAER_ALL_INFO'));
}

相关概念

session与token区别

session:为了维护用户登录状态,服务端在每个用户登录后存一个唯一sessionId至服务器,每次业务请求时查询sessionId是否存在,存在即用户已登录,可进行下一步处理;

token:服务端不再存储用户登录标识,用户登录成功后生成一个包含用户id、有效期、签名的加密串,每次业务请求时解密token,校验token,校验通过则进行下一步处理;

JWT(Json Web Token):以json格式传递token的规范,标准token由3部分拼接而成:header(base64转换的签名声明),payload(base64转换的自定义数据),signature(对前面2个内容生成加密串的base64编码)

记住密码

// 2. 进入登录,查询cookies
created(){
    if(this.$cookies.isKey("userName") && this.$cookies.isKey("password")){
        this.username = this.$cookies.get('username');
        this.password = Base64.decode(this.$cookies.get('password'));
    }
}
// 1. 登录成功,判断勾选
login(){
    ...
    if(this.isRemberPassword){
        // VueCookies 与 js-base64
        this.$cookies.set("username", this.loginForm.username, "3d");
        this.$cookies.set("password", Base64.encode(this.$cookies.get('password')), "3d");
    }else{
        this.$cookies.remove("username");
        this.$cookies.remove("password");
    }
}

注: 纯前端方式记住密码有风险,谨慎使用;

权限控制

路由权限

以通过后端单独接口返回用户权限信息情况下为例

  1. router.config.js 声明adminRoutes(所有权限路由)
  2. 获取用户role标识,调用addAdminRoutes生成最终权限路由
// ./router/auth.js
const setAdminRoutes = function (router) {
  const list = $store.getters['user/roleList'];
  const adminRouteTree = deepClone(adminRoutes);
  // 1. 根据权限id列表 生成 路由列表
  const rootRoutes = [];
  adminRouteTree.forEach((route) => {
    // 检测一级路由权限
    const rootId = route.meta.rootId;
    if (rootId && !list.find((item) => item.id === rootId)) return;
    // 检测二级路由权限
    const childRouteArr = route.children.filter((secondRoute) => {
      // 比较角色权限与路由rootId
      const rootId = secondRoute.meta.rootId;
      if (rootId && !list.find((item) => item.id === rootId)) return false;
      return true;
    });
    route.children = childRouteArr;
    router.addRoute(route);
    rootRoutes.push(route);
  });
  router.addRoute(errorRoute);
  $store.commit('SET_ADMIN_ROUTES', rootRoutes);
};

// ./router/index.js beforeEach拦截
if (!$store.getters["user/userInfo"]) {
    $store
      .dispatch('user/getRoleInfo')
      .then(() => next(to.fullPath))
      .catch((e) => {
        next('/login');
      });
}

```js
const setAdminRoutes = function (router) {
  let list = $store.getters['user/roleList'];
  let adminRouteTree = deepClone(adminRoutes);
  // 递归遍历所有层级
  let rootRoutes = (function filterRoutesByRoleList(routeTree) {
    let temp = [];
    routeTree.forEach((route) => {
      // 此处对比权限根据实际需求确定
      let rootId = route.meta.rootId;
      if (rootId && !list.find((item) => item.id === rootId)) return;
      if (route.children) route.children = filterRoutesByRoleList(route.children);
      temp.push(route);
    });
    return temp;
  })(adminRouteTree);
  // VueRouter的addRoutes旧方法,一次添加多个路由
  router.addRoutes([...rootRoutes, errorRoute]);
  $store.commit('SET_ADMIN_ROUTES', rootRoutes);
};

控件权限

权限控制(rootId)表

// constants/rootId.js
export const ROOT_MAP = new Map([
  ['权限1名称', 100],  
  ['权限2名称', 100],
  ['权限3名称', 100],
  ...
]);
// 多模块嵌套
export const MODULE_ROOT_MAP = new Map([
  [1, new Map([
      ['模块1权限1名称', 100],  
      ['模块1权限2名称', 100],
      ['模块1权限3名称', 100],
      ...
  ])],  
  ...
]);

自定义指令(directive)快速控制

import $store from '@/store';
const detectPermission = {};
detectPermission.install = (Vue) => {
  Vue.directive('detect-permission', (el, binding, vnode) => {
    // 匹配权限
    if ($store.getters['user/roleList'].find((item) => item.id === binding.value)) return;
    if (el.parentNode) el.parentNode.removeChild(el);
  });
};
export default detectPermission;

// Vue.use(detectPermission)

权限判断(hasRoot、hasSomeRoot)

// store/modules/user.js
hasRoot: (state) => (rootId) => !!getters.roleList(state).find((item) => item.id === rootId),
hasSomeRoots: (state) => (rootMap, keys) => keys.some((key) => getters.hasRoot(state)(rootMap.get(key)))

指令/hasRoot使用

<img v-detect-permission="ROOT_MAP.get('封面图')"/>
<div v-detect-permission="MODULE_ROOT_MAP.get(moduleIndex).get('编辑')" >编辑</div>
<div v-if="hasSomeRoot(MODULE_ROOT_MAP.get(moduleIndex), ['添加','删除','新增'])">
    <div v-detect-permission="MODULE_ROOT_MAP.get(moduleIndex).get('添加')" >添加</div>
    <div v-detect-permission="MODULE_ROOT_MAP.get(moduleIndex).get('删除')" >删除</div>
    <div v-detect-permission="MODULE_ROOT_MAP.get(moduleIndex).get('新增')" >新增</div>
</div>
if(this.hasRoot(ROOT_MAP.get('查询列表'))) this.$req.getList();

思考:

  • 可以将权限控制独逻辑立为一个组件容器,在需要控制权限内容上包一层;
  • 请求的权限控制不应该通过单独判断,可以请求上注明权限信息,全局请求拦截器统一判断拦截;