Bootstrap

Spring Boot多模块+ Shiro + Vue:前后端分离登陆整合,权限认证

前言
本文主要使用spring boot + shiro + vue来实现前后端分离的认证登陆和权限管理,适合和我一样刚开始接触前后端完全分离项目的同学,但是你必须自己搭建过前端项目和后端项目,本文主要是介绍他们之间的互通,如果不知道这么搭建前端项目的同学可以先找别的blog看一下。
自己摸索了一下,可能会有一些问题,也有可能有更好的实现方式,但这个demo主要是用来记录自己搭建系统,独立完成前后端分离项目的过程,并且作为自己的毕业设计框架。所以有问题的话欢迎提出,共同交流。源码在github上,有需要的同学可以自己去取(地址在结尾)。

Demo功能描述

1.前端登陆页面输入http://localhost:8080/#/login会跳转到前端登陆界面,输入用户名密码后向后端 localhost:8888 发送验证请求
2.后台接受输入信息后,通过shiro认证,向前台返回认证结果,密码是通过md5加密的
3.登陆成功后,权限认证,有些页面只能管理员才能进入,有些按钮只能拥有某项权限的人才能看到,后台有些接口只能被有权限的人访问。

Demo难点思考

  • 前端工程在8080接口,发送的请求如何转发到后台8888接口
  • 传统的前后端未分离项目可以通过shiro标签在前台进行细粒度按钮控制,独立的前端vue项目如何做到这样的控制
  • 同上,前端项目如何实现带权限的页面跳转,因为跳转页面的请求不会走后台,后台只提供数据

解决思路:

这么解决上面的问题?我这里的思路是(注*思路最重要,代码只会贴关键代码,全部代码请上git上取):

  • 080端口请求8888端口本质上是跨域问题,两种解决方式,1是在前端vue项目里面配置proxy,2是使用nginx反向代理,先采用第一种。nginx反向代理之后在介绍
  • 登陆之后,后台将roles和permissions信息传给前台,前台将持有登陆人的角色和权限信息(使用cookie和localstorage都可以,我结合了两者使用)
  • 使用router,绑定路由,访问权限绑定到对应组件上,实现页面级别的权限控制
    使用指令,来控制细粒度级别的按钮显示等

Demo技术栈描述

1.前端技术栈

框架:vue+elementui+axios
语言:es6,js
环境:node8 + yarn
打包工具: webpack
开发工具:vscode

2.后端

框架:spring Boot多模块+ maven + shiro + jpa + mysql8.0
开发工具:intellij idea

开发流程

1.后端开发流程

 ·搭建spring boot多模块项目(本文不会介绍)
 ·创建shiro角色和权限的数据表
 ·集成shiro框架和md5加密
 ·开发登陆认证接口

2.前端开发流程

·搭建前端运行环境和webpack项目(本文不会介绍)
·开发登陆页面组件
·跨域——来支持请求后端接口
·路由开发,钩子函数(页面跳转控制),cookieUtil开发(存储后台roles和permissions信息),自定义指令(前端细粒度控制)
·启动项目,测试登陆及权限验证

后端开发详细流程

1.创建shiro角色和权限的数据表

  • 结构
    图片描述
  • 用户表(注意盐的存在,为了md5加密用)
    在这里插入图片描述
  • 权限表
    在这里插入图片描述
  • 剩余两张是用户角色关联表和角色权限关联表,不展出了

2.集成shiro框架和md5加密

  • 项目结构(我们在security模块中集成shiro)
    在这里插入图片描述
    在这里插入图片描述
  • maven包(全部的包看源码,只贴核心的)
 <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
            <scope>compile</scope>
        </dependency>
  • 配置Realm类(shiro框架手动配置的关键,用来登陆和权限认证)
/**
 * Created by WJ on 2019/3/28 0028
 * 自定义权限匹配和密码匹配
 */
public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private UserRepository userRepository;

    @Resource
    private SysPermissionService sysPermissionService;

    @Resource
    private UserService userService;

    @Override
    public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User User = (User) principals.getPrimaryPrincipal();
        try {
            List<SysRole> roles = sysRoleService.selectRoleByUserId(User.getId());
            for (SysRole role : roles) {
                authorizationInfo.addRole(role.getRole());//角色存储
            }
            //此处如果多个角色都拥有某项权限,bu会数据重复,内部用的是Set
            List<SysPermission> sysPermissions = sysPermissionService.selectPermByRole(roles);
            for (SysPermission perm : sysPermissions) {
                authorizationInfo.addStringPermission(perm.getPermission());//权限存储
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return authorizationInfo;
    }

    /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
        //获取用户的输入的账号.
        String username = (String) token.getPrincipal();
//       System.out.println(token.getCredentials());
        //通过username从数据库中查找 User对象,如果找到,没找到.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        User user = userRepository.findByUsername(username).get();//*
        if (user == null) {
            return null;
        }

        if (user.getState() == 0) { //账户冻结
            throw new LockedAccountException();
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user, //用户名
                user.getPassword(), //密码
                ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }
}
  • shiroConfig(集成到spring框架中,拦截链及md5配置,md5配置完成之后,数据库中存的应该是加密过后的代码,还有一些工具类请去源码里面拿,这边不贴)
@Configuration
public class ShiroConfig
{
    @Value("${sessionOutTime}")
    private String serverSessionTimeout;

    /**
     * 密码校验规则HashedCredentialsMatcher,也就是密码比对器
     * 这个类是为了对密码进行编码的 ,
     * 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
     * 这个类也负责对form里输入的密码进行编码
     * 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
     */
    @Bean("credentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次数
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }


    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName("shiroFilter");
        filterRegistrationBean.setFilter(proxy);
        return filterRegistrationBean;
    }


    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
       // shiroFilterFactoryBean.setLoginUrl("/login");
        //设置成功跳转的页面
        //shiroFilterFactoryBean.setSuccessUrl("/index");
        // 设置无权限时跳转的 url;
        //shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //游客,开发权限
        //filterChainDefinitionMap.put("/**", "anon");
        filterChainDefinitionMap.put("/guest/**", "anon");
        //用户,需要角色权限 “user”
        filterChainDefinitionMap.put("/user/**", "roles[user]");
        //管理员,需要角色权限 “admin”
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");
        //开放登陆接口
        filterChainDefinitionMap.put("/api/ajaxLogin", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        //其余接口一律拦截
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
        filterChainDefinitionMap.put("/**", "authc");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/unauth");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }
    /*
    注入securityManager
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置REALM
        securityManager.setRealm(customRealm());
        return securityManager;
    }
    /*
    自定义身份认证realm
    必须写上这个类,并加上@Bean注解,目的是注入CustomRealm
    否则会影响CustomRealm类中其他类的依赖注入
     */
    @Bean
    public MyShiroRealm customRealm(){
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());// 将md5密码比对器传给realm
        return  myShiroRealm;
    }
    /*
    开启注解支持
     */
    @Bean
    //@DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public FilterRegistrationBean shiroSessionFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new ShiroSessionFilter());
        filterRegistrationBean.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.addUrlPatterns("/*");
        Map<String, String> initParameters = new HashMap<>();
        initParameters.put("serverSessionTimeout", serverSessionTimeout);
        initParameters.put("excludes", "/favicon.ico,/images/*,/js/*,/css/*,/static/*,/upload/*");
        filterRegistrationBean.setInitParameters(initParameters);
        return filterRegistrationBean;
    }

    /*@Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }*/

}

  • md5加密Test代码,将结果存到数据库,salt值是 用户名 + ‘salt’
 @Test
    public void md5Test() {
        String hashAlgorithName = "MD5";
        String password = "123456";
        int hashIterations = 1024;
        ByteSource byteSource = ByteSource.Util.bytes("wujiesalt");
        Object obj = new SimpleHash(hashAlgorithName, password, byteSource, hashIterations);
        System.out.println("加密之后的密码" + obj);
    }
  • 开发登陆接口(注意这个接口是在shiroconfig中配置开放的)
@Controller
public class ShiroController {
   @Resource
   private LoginService loginService;
    /**
     * 登录方法
     * @param userInfo
     * @return
     */
    @RequestMapping(value = "/api/ajaxLogin", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public Result ajaxLogin(@RequestBody User userInfo) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userInfo.getUsername(), userInfo.getPassword());

        try {
            subject.login(token);
            LoginInfo loginInfo = loginService.getLoginInfo(userInfo.getUsername());
            return ResultFactory.buildSuccessResult(loginInfo);// 将用户的角色和权限发送到前台
        } catch (IncorrectCredentialsException e) {
            return ResultFactory.buildFailResult("密码错误");
        } catch (LockedAccountException e) {
            return ResultFactory.buildFailResult("登录失败,该用户已被冻结");
        } catch (AuthenticationException e) {
            return ResultFactory.buildFailResult("该用户不存在");
        } catch (Exception e) {
            e.printStackTrace();
        }

        return ResultFactory.buildFailResult("登陆失败");
    }

    /**
     * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
     * @return
     */
    @RequestMapping(value = "/unauth")
    @ResponseBody
    public Object unauth() {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("code", "1000000");
        map.put("msg", "未登录");
        return map;
    }
}
@Service
public class LoginService {
    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private UserRepository userRepository;

    @Resource
    private SysPermissionService sysPermissionService;

    public LoginInfo getLoginInfo(String username) {
        User user = userRepository.findByUsername(username).get();

        List<SysRole> roles = sysRoleService.selectRoleByUserId(user.getId());

        Set<String> roleList = new HashSet<>();
        Set<String> permissionList = new HashSet<>();
        for (SysRole role : roles) {
            roleList.add(role.getRole());//角色存储
        }
        //此处如果多个角色都拥有某项权限,bu会数据重复,内部用的是Set
        List<SysPermission> sysPermissions = sysPermissionService.selectPermByRole(roles);
        for (SysPermission perm : sysPermissions) {
            permissionList.add(perm.getPermission());//权限存储
        }

        return  new LoginInfo(roleList,permissionList);
    }
}
请输入代码/**
 * Created by WJ on 2019/3/26 0026
 */
public class ResultFactory {
    public static Result buildSuccessResult(LoginInfo data) {
        return buidResult(ResultCode.SUCCESS, "成功", data);
    }

    public static Result buildFailResult(String message) {
        return buidResult(ResultCode.FAIL, message, null);
    }

    public static Result buidResult(ResultCode resultCode, String message, LoginInfo data) {
        return buidResult(resultCode.code, message, data);
    }

    public static Result buidResult(int resultCode, String message, LoginInfo data) {
        return new Result(resultCode, message, data);
    }
}
public class Result {
    /**
     * 响应状态码
     */
    private int code;
    /**
     * 响应提示信息
     */
    private String message;
    /**
     * 响应结果对象
     */
    private LoginInfo loginInfo;

    public Result(int code, String message, LoginInfo loginInfo) {
        this.code = code;
        this.message = message;
        this.loginInfo = loginInfo;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public LoginInfo getLoginInfo() {
        return loginInfo;
    }

    public void setLoginInfo(LoginInfo loginInfo) {
        this.loginInfo = loginInfo;
    }
}
  • 好啦!到这里后台的工作基本完成了,现在去开发前台

前台开发流程

  • 登陆页面的开发
<template>
  <div class="login-wrap">
    <div class="ms-login">
      <div class="ms-title">土地经营管理系统</div>
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="0px" class="ms-content">
        <el-form-item prop="username">
          <el-input v-model="ruleForm.username" placeholder="username">
            <el-button slot="prepend" icon="el-icon-lx-people"></el-button>
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            type="password"
            placeholder="password"
            v-model="ruleForm.password"
            @keyup.enter.native="login"
          >
            <el-button slot="prepend" icon="el-icon-lx-lock"></el-button>
          </el-input>
        </el-form-item>
        <div class="login-btn">
          <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
        </div>
        <p class="login-tips">Tips : 用户名和密码随便填。</p>
      </el-form>
    </div>
  </div>
</template>

<script>
import {setCookie,getCookie} from '../../assets/js/cookie';
export default {
  data: function() {
    return {
      ruleForm: {
        username: "",
        password: ""
      },
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" }
        ],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate(valid => {
        if (valid) {
          this.$axios
            .post("/api/ajaxLogin", {// 请求后台登陆接口
              username: this.ruleForm.username,
              password: this.ruleForm.password
            })
            .then(successResponse => {
              this.responseResult = JSON.stringify(successResponse.data);
              if (successResponse.data.code === 200) {
               console.log("登陆信息" + successResponse.data.loginInfo.roleList);
               setCookie('roles',successResponse.data.loginInfo.roleList);// 使用cookie来记录是否登陆,这边跨域
               let roles = getCookie('roles');
               console.log('cookie' + roles);
               localStorage.setItem("ms_username", this.ruleForm.username);// 使用localstoage来记录登陆信息
                localStorage.setItem("roles", successResponse.data.loginInfo.roleList);
                localStorage.setItem("permissions", successResponse.data.loginInfo.permissionList);
                this.$router.push("/");// 跳转路由
              }
              if (successResponse.data.code === 400) {
                  let warnMessage = successResponse.data.message;
                  this.$message({
                      message: warnMessage,
                      type: 'warning'
                  })
              }
            });
         
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    }
  }
};
</script>
  • cookie.js,用来设置cookie,存储后台传过来的数据
export function setCookie(key,value) {
    var exdate = new Date();//获取时间
    exdate.setTime(exdate.getTime() + 24 * 60 *60); //保存的天数,一天
    //字符串拼接cookie
    window.document.cookie = key + "=" + value + ";path=/;expires=" + exdate.toGMTString();
}

//读取cookie
export function getCookie(param) {
    var c_param = '';
    if (document.cookie.length > 0) {
        console.log("原document cookie: " + document.cookie);
        var arr = document.cookie.split('; '); //获取key value数组
        for (var i = 0; i < arr.length; i++) {
            var arr2 = arr[i].split('='); //获取该key 下面的 value数组
            if(arr2[0] == param) {
                c_param = arr2[1];

            }
        }

        return c_param;
    }
}

function padLeftZero (str) {
    return ('00' + str).substr(str.length);
  };
  • 请求成功后,使用钩子函数结合router路由跳转页面,(每次跳转页面都会走钩子函数,配合路由配置,而且这时候我们已经拿到了当前用户的角色和权限,结合实现页面权限跳转),以下为main.js
import axios from 'axios';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css'; // 默认主题
// import '../static/css/theme-green/index.css';       // 浅绿色主题
import './assets/css/icon.css';
import './components/common/directives';
import "babel-polyfill";
import {setCookie,getCookie} from './assets/js/cookie';

Vue.config.productionTip = false
Vue.use(ElementUI, {
    size: 'small'
});
axios.default.baseURL = 'https://localhost:8888'
Vue.prototype.$axios = axios;

//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
    const roles = localStorage.getItem('roles');
    const permissions = localStorage.getItem('permissions');
    //这边可以用match()来判断所有需要权限的路径,to.matched.some(item => return item.meta.loginRequire)
    let cookieroles = getCookie('roles');
    console.log('cookie' + cookieroles);
    if (!cookieroles && to.path !== '/login') { // cookie中有登陆用户信息跳转页面,否则到登陆页面
        next('/login');
    } else if (to.meta.permission) {// 如果该页面配置了权限属性(自定义permission)
        // 如果是管理员权限则可进入
        roles.indexOf('admin') > -1 ? next() : next('/403');
    } else {
        // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容
        if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') {
            Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', {
                confirmButtonText: '确定'
            });
        } else {
            next();
        }
    }
})
// 在管理员页面配置 permission = true
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export default new Router({
    routes: [
        {
            path: '/',
            redirect: '/dashboard'
        },
        {
            path: '/',
            component: resolve => require(['../components/common/Home.vue'], resolve),
            meta: { title: '自述文件' },
            children:[
                {
                    path: '/dashboard',
                    component: resolve => require(['../components/page/Dashboard.vue'], resolve),
                    meta: { title: '系统首页' }
                },
                {
                    path: '/icon',
                    component: resolve => require(['../components/page/Icon.vue'], resolve),
                    meta: { title: '自定义图标' }
                },
                {
                    path: '/table',
                    component: resolve => require(['../components/page/BaseTable.vue'], resolve),
                    meta: { title: '基础表格' }
                },
                {
                    path: '/tabs',
                    component: resolve => require(['../components/page/Tabs.vue'], resolve),
                    meta: { title: 'tab选项卡' }
                },
                {
                    path: '/form',
                    component: resolve => require(['../components/page/BaseForm.vue'], resolve),
                    meta: { title: '基本表单' }
                },
                {
                    // 富文本编辑器组件
                    path: '/editor',
                    component: resolve => require(['../components/page/VueEditor.vue'], resolve),
                    meta: { title: '富文本编辑器' }
                },
                {
                    // markdown组件
                    path: '/markdown',
                    component: resolve => require(['../components/page/Markdown.vue'], resolve),
                    meta: { title: 'markdown编辑器' }    
                },
                {
                    // 图片上传组件
                    path: '/upload',
                    component: resolve => require(['../components/page/Upload.vue'], resolve),
                    meta: { title: '文件上传' }   
                },
                {
                    // vue-schart组件
                    path: '/charts',
                    component: resolve => require(['../components/page/BaseCharts.vue'], resolve),
                    meta: { title: 'schart图表' }
                },
                {
                    // 拖拽列表组件
                    path: '/drag',
                    component: resolve => require(['../components/page/DragList.vue'], resolve),
                    meta: { title: '拖拽列表' }
                },
                {
                    // 拖拽Dialog组件
                    path: '/dialog',
                    component: resolve => require(['../components/page/DragDialog.vue'], resolve),
                    meta: { title: '拖拽弹框' }
                },
                {
                    // 权限页面
                    path: '/permission',
                    component: resolve => require(['../components/page/Permission.vue'], resolve),
                    meta: { title: '权限测试', permission: true } // 配合钩子函数实现权限认证
                },
                {
                    path: '/404',
                    component: resolve => require(['../components/page/404.vue'], resolve),
                    meta: { title: '404' }
                },
                {
                    path: '/403',
                    component: resolve => require(['../components/page/403.vue'], resolve),
                    meta: { title: '403' }
                }
            ]
        },
        {
            path: '/login',
            component: resolve => require(['../components/page/Login.vue'], resolve)
        },
        {
            path: '*',
            redirect: '/404'
        }
    ]
})

  • 自定义指令实现细粒度的按钮显示等控制(例:如果我们想控制某个角色或者拥有某项权限才能看到编辑按钮)
Vue.directive('hasAuthorization',{
    bind: (el) => {
        const roles = localStorage.getItem('roles');
        console.log(roles);
        if(!(localStorage.getItem('roles').indexOf('admin') > -1)){
            el.setAttribute('style','display:none')
        }
    }
})
//在按钮中设置指令,这样只有管理员才能看到这个按钮并使用,配置权限同理
<el-button type="text" icon="el-icon-edit" @click="handleEdit(scope.$index, scope.row)" v-hasAuthorization >编辑</el-button>
  • 配置proxy来支持跨域,向后台请求登陆和数据
// 在vue.config.js中配置profxy
module.exports = {
    baseUrl: './',
    productionSourceMap: false,
    devServer: {
        proxy: {
            '/api':{
                target: 'http://127.0.0.1:8888',// 这里设置调用的域名和端口号,需要http,注意不是https!
                changeOrigin: true,
                pathRewrite: {
                    '^/api': '/api' //这边如果为空的话,那么发送到后端的请求是没有/api这个前缀的
                }
            }
        }
    }
}

//还要在man.js中配置axios
axios.default.baseURL = 'https://localhost:8888'
Vue.prototype.$axios = axios;

运行效果

  • 管理员账号登入
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 非管理员用户
    在这里插入图片描述
    在这里插入图片描述

总结

  • 与传统的项目最大的区别就是,我们使用了vue
    router控制页面跳转,使用指令来细粒度控制,使用了cookie和localstorage(其实选择一个来记录就可以了,这边有小Bug待解决)记录了用户信息。
  • 主要提供了这样一个思路,设计到vue中不懂的知识点可以直接取官网上面找,比我在这边讲清楚
  • 后端地址:https://github.com/Attzsthl/land-mange前端地址:https://github.com/Attzsthl/l…
  • 欢迎交流,有问题和不清楚的地方我会解答,谢谢观看!