赞
踩
后台:
前端:
项目:
前端选用的lin-xin大神的前端模板(github地址)。该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统(Web Management System)开发。基于 vue.js,使用 vue-cli3 脚手架,引用 Element UI 组件库,方便开发快速简洁好看的组件。分离颜色样式,支持手动切换主题色,而且很方便使用自定义主题色。
<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="userId"> <el-input v-model="ruleForm.userId" placeholder="userId"> <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="submitForm('ruleForm')"> <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> export default { data: function(){ return { ruleForm: { userId: 'admin', password: '1234' }, rules: { userId: [ { required: true, message: '请输入用户名', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' } ] } } }, methods: { submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { this.$axios.post('/sys/login', this.ruleForm ).then((res) => { if(res.data.success){ localStorage.setItem('ms_userId',this.ruleForm.userId); this.$router.push('/dashboard'); } else { this.$message.error(res.data.msg); } }) } else { console.log('error submit!!'); return false; } }); } } } </script> <style scoped> .login-wrap{ position: relative; width:100%; height:100%; background-image: url(../../assets/img/login-bg.jpg); background-size: 100%; } .ms-title{ width:100%; line-height: 50px; text-align: center; font-size:20px; color: #fff; border-bottom: 1px solid #ddd; } .ms-login{ position: absolute; left:50%; top:50%; width:350px; margin:-190px 0 0 -175px; border-radius: 5px; background: rgba(255,255,255, 0.3); overflow: hidden; } .ms-content{ padding: 30px 30px; } .login-btn{ text-align: center; } .login-btn button{ width:100%; height:36px; margin-bottom: 10px; } .login-tips{ font-size:12px; line-height:30px; color:#fff; } </style>
import Vue from 'vue'; import Router from 'vue-router'; Vue.use(Router); export default new Router({ mode: 'hash', base: __dirname, routes: [ { path: '/', redirect: '/login' }, { 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: '/user', component: resolve => require(['../components/page/User.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: '/i18n', component: resolve => require(['../components/page/I18n.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' } ] })
axios.interceptors.response.use(res => { if(!res.data.success && res.data.code == 1000001){ router.replace({ path: 'login' }) return Promise.reject(res); } else { // 对响应数据做些什么 return res } }, err => { // 对响应错误做些什么 console.log('err', err.response) // 修改后 return Promise.resolve(errsresponse) // 可在组件内获取到服务器返回信息 }) //使用钩子函数对路由进行权限跳转 router.beforeEach((to, from, next) => { debugger const userId = localStorage.getItem('ms_userId'); if (!userId && to.path !== '/login') { next('/login'); } else if(to.path==="/login"){ next(); }else if (to.meta.permission) { // 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已 userId === 'admin' ? next() : next('/403'); } else { let userMenu = localStorage.getItem('user_menu'); if(!userMenu){ var params ={ userId : userId } axios.post('/sys/selectUserPermission', params ).then((res) => { console.log(res.data) if(res.data.success){ console.log(res.data.data) userMenu = res.data.data; localStorage.setItem('user_menu',res.data.data); // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容 if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') { Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', { confirmButtonText: '确定' }); } else { if(to.path !== '/403' && to.path !== '/login'&& userMenu){ let result = userMenu.indexOf(to.path); if(result>=0){ next(); } else{ next('/403'); } } else { next(); } } } else { Vue.prototype.$alert(res.data.msg, '错误提示', { confirmButtonText: '确定' }); } }) } else { // 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容 if (navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor') { Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,请使用更高版本的浏览器查看', '浏览器不兼容通知', { confirmButtonText: '确定' }); } else { if(to.path !== '/403' && to.path !== '/login'&& userMenu){ let result = userMenu.indexOf(to.path); if(result>=0){ next(); } else{ next('/403'); } } else { next(); } } } } })
后端是自己搭建的springboot项目,数据库用的是mysql。数据库连接池用的druid,mybatis持久层框架,pagehelper分页插件。
<properties> <java.version>1.8</java.version> <mybatis.version>1.3.2</mybatis.version> <pagehelper.version>1.2.3</pagehelper.version> <shiro.version>1.4.0</shiro.version> <fastjson.version>1.2.31</fastjson.version> <druid.version>1.1.10</druid.version> <swagger.version>2.5.0</swagger.version> <swagger.ui.version>2.5.0</swagger.ui.version> <commons.lang3.version>3.4</commons.lang3.version> <commons.beanutils.version>1.9.3</commons.beanutils.version> <shiro.redis.version>3.1.0</shiro.redis.version> </properties> <dependencies> <!--SpringBoot Web支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--数据库相关--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- alibaba的druid数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!-- alibaba的json格式化对象 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- pagehelper分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper.version}</version> </dependency> <!-- swagger2自动生成API文档 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.ui.version}</version> </dependency> <!-- 工具类--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons.lang3.version}</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>${commons.beanutils.version}</version> </dependency> <!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!-- shiro整合redis做session共享 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>${shiro.redis.version}</version> <exclusions> <exclusion> <artifactId>shiro-core</artifactId> <groupId>org.apache.shiro</groupId> </exclusion> </exclusions> </dependency> <!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> </dependencies>
Apache的Shiro是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与Spring Security一样属基于权限的安全框架,但是与Spring Security 相比,Shiro使用了比较简单易懂易于使用的授权方式。Shiro属于轻量级框架,相对于Spring Security简单很多,并没有security那么复杂。
- HopeShiroRealm 继承了AuthorizingRealm,这个类的作用是两处获取信息,一处是Subject即用户传过来的信息;一处是通过我们提供给shiro的SysUserService接口从数据库获取权限信息和角色信息。拿这两个信息之后AuthorizingRealm会自动进行比较,判断用户名密码,用户权限等等。拿用户凭证信息的是doGetAuthenticationInfo接口,拿角色权限信息的是doGetAuthorizationInfo接口。两个重要参数,AuthenticationToken是我们可以自己实现的用户凭证/密钥信息,PrincipalCollection是用户凭证信息集合。配置完成之后Subject.login(token)的时候就会调用doGetAuthenticationInfo方法;涉及到Subject.hasRole或者Subject.hasPermission的时候就会调用doGetAuthorizationInfo方法。
在配置shiro的拦截信息的时候,要忽略拦截"/" ,由于前端路由配置了根路径的路由,忽略拦截,会访问前端路由。
package com.lh.config.shiro; import com.lh.common.HopeExceptionHandler; import com.lh.config.properties.RedisInfo; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerExceptionResolver; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; @Slf4j @Configuration public class ShiroConfig { /** * Filter工厂,设置对应的过滤条件和跳转条件 */ @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { log.info("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); //注意过滤器配置顺序 不能颠倒 //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl filterChainDefinitionMap.put("/logout", "logout"); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/static/**", "anon"); // 首页 filterChainDefinitionMap.put("/", "anon"); // 登录 filterChainDefinitionMap.put("/sys/login", "anon"); filterChainDefinitionMap.put("/**", "corsAuthenticationFilter"); //配置 shiro 默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 shiroFilterFactoryBean.setLoginUrl("/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); //自定义过滤器 Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); return shiroFilterFactoryBean; } /** * 将自己的用户认证验证方式加入容器 */ @Bean public HopeShiroRealm hopeShiroRealm() { return new HopeShiroRealm(); } /** * 权限管理,配置主要是Realm的管理认证 和session管理 * * @param redisSessionManager redis session共享 * @param redisCacheManager redis 缓存 */ @Bean public SecurityManager securityManager( @Qualifier("redisSessionManager") DefaultWebSessionManager redisSessionManager, @Qualifier("redisCacheManager") RedisCacheManager redisCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(hopeShiroRealm()); securityManager.setSessionManager(redisSessionManager); securityManager.setCacheManager(redisCacheManager); return securityManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 */ @Bean public RedisManager redisManager(RedisInfo redisConfig) { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisConfig.getHost()); redisManager.setPort(redisConfig.getPort()); redisManager.setTimeout(redisConfig.getTimeout()); return redisManager; } /** * redisSession相关配置 * 自定义session持久化 * 为啥session也要持久化? * 重启应用,用户无感知,可以继续以原先的状态继续访问 * 注意点: * DO对象需要实现序列化接口 Serializable * logout接口和以前一样调用,请求logout后会删除redis里面的对应的key,即删除对应的token */ @Bean public RedisSessionDAO redisSessionDAO(RedisManager redisManager) { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager); redisSessionDAO.setKeyPrefix("HOPE_SHIRO_SESSION:");// key redisSessionDAO.setExpire(1000); // 过期时间 return redisSessionDAO; } /** * session的管理 用redis实现session共享 */ @Bean public DefaultWebSessionManager redisSessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO); return sessionManager; } /** * 配置具体cache实现类RedisCacheManager * 为什么要使用缓存: * 缓存组件位于SecurityManager中,在HopeShiroRealm数据域中,由于授权方法中每次都要查询数据库,性能受影响,因此将数据缓存起来,提高查询效率 * 除了使用Redis缓存,还能使用shiro-ehcache */ @Bean public RedisCacheManager redisCacheManager(RedisManager redisManager) { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager); return redisCacheManager; } /** * 加入注解的使用,不加入这个注解不生效 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 注册全局异常处理 */ @Bean(name = "exceptionHandler") public HandlerExceptionResolver handlerExceptionResolver() { return new HopeExceptionHandler(); } }
package com.lh.config.shiro; import com.lh.entity.sys.SysPermission; import com.lh.entity.sys.SysRole; import com.lh.entity.sys.SysUser; import com.lh.service.SysUserService; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import javax.annotation.Resource; /** * 实现AuthorizingRealm接口用户认证 */ @Slf4j public class HopeShiroRealm extends AuthorizingRealm { @Resource private SysUserService sysUserService; /** * 角色权限和对应权限添加 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info("权限配置-->HopeShiroRealm.doGetAuthorizationInfo()"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); SysUser sysUser = (SysUser) principals.getPrimaryPrincipal(); for (SysRole role : sysUser.getSysRoles()) { // 添加角色 authorizationInfo.addRole(role.getRoleId()); for (SysPermission p : role.getSysPermissions()) { // 添加权限 authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } /** * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { log.info("HopeShiroRealm.doGetAuthenticationInfo()"); //获取用户的输入的账号. String userId = (String) token.getPrincipal(); log.info("用户密码 ={}", token.getCredentials()); //通过username从数据库中查找 User对象,如果找到,没找到. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 SysUser sysUser = sysUserService.selectSysUserByUserId(userId); log.info("----->>sysUser=" + sysUser); if (sysUser == null) { return null; } if (sysUser.getStatus() == 1) { //账户冻结 throw new LockedAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( sysUser, //用户名 sysUser.getPassword(), //密码 null, //ByteSource.Util.bytes(sysUser.getCredentialsSalt()),//salt=username+salt getName() //realm name ); return authenticationInfo; } }
前后端分离项目中,由于跨域,会导致复杂请求,即会发送preflighted request,这样会导致在GET/POST等请求之前会先发一个OPTIONS请求,但OPTIONS请求并不带cookie,即OPTIONS请求不能通过shiro验证,会返回未认证的信息。
package com.lh.config.shiro; import com.alibaba.fastjson.JSON; import com.lh.entity.common.Result; import com.lh.common.enums.TransactionCode; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; @Slf4j public class CORSAuthenticationFilter extends FormAuthenticationFilter { @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { //Always return true if the request's method is OPTIONSif (request instanceof HttpServletRequest) { if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) { return true; } return super.isAccessAllowed(request, response, mappedValue); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse) response; res.setHeader("Access-Control-Allow-Origin", "*"); res.setStatus(HttpServletResponse.SC_OK); res.setCharacterEncoding("utf-8"); PrintWriter writer = res.getWriter(); writer.write(JSON.toJSONString(Result.error(TransactionCode.NO_LOGION.getCode(), TransactionCode.NO_LOGION.getMsg()))); writer.close(); return false; } }
package com.lh.controller; import com.lh.entity.sys.SysUser; import com.lh.entity.common.Result; import com.lh.common.enums.TransactionCode; import com.lh.entity.sys.common.UserMenu; import com.lh.service.SysService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.List; @Slf4j @RestController public class SysController { @Resource private SysService sysService; /** * 登录方法 * * @param sysUser * @return */ @RequestMapping(value = "/sys/login", method = RequestMethod.POST) public Result ajaxLogin(@RequestBody SysUser sysUser) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUserId(), sysUser.getPassword()); try { subject.login(token); return Result.ok(subject.getSession().getId()); } catch (IncorrectCredentialsException e) { return Result.error("密码错误!"); } catch (LockedAccountException e) { return Result.error("登录失败,该用户已被冻结!"); } catch (AuthenticationException e) { return Result.error("该用户不存在!"); } catch (Exception e) { log.info("用户登录时发生异常!e={}", e); return Result.error(); } } /** * 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面 * * @return */ @RequestMapping(value = "/unauth") public Result<String> unauth() { return Result.error(TransactionCode.NO_LOGION.getCode(), TransactionCode.NO_LOGION.getMsg()); } /** * 根据用户编号查询用户所有菜单 * * @param sysUser * @return */ @RequestMapping(value = "/sys/selectUserMenu", method = RequestMethod.POST) public Result<List<UserMenu>> selectUserMenu(@RequestBody SysUser sysUser) { // 根据用户编号查询用户所有可见菜单 if (StringUtils.isNotBlank(sysUser.getUserId())) { return sysService.selectUserMenu(sysUser.getUserId()); } else { return Result.error("未获取到当前登录人信息!"); } } @RequestMapping(value = "/sys/selectUserPermission", method = RequestMethod.POST) public Result<List<String>> selectUserPermission(@RequestBody SysUser sysUser) { if (StringUtils.isNotBlank(sysUser.getUserId())) { return sysService.selectUserPermission(sysUser.getUserId()); } else { return Result.error("未获取到当前登录人信息!"); } } }
编译前端项目,编译好的文件如下:
将构建好的dist下static文件夹拷贝到springboot的resource的static下,index.html也拷贝到springboot的resource的static下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。