基于SpringBoot/Vue/ElementUI 构建权限系统
后端系统 前端系统
基于 SpringBoot/Vue/ElementUI 构建权限系统
记录一下前几天写的一个权限系统。
整个系统基于RBAC模型构建,提供了前端权限控制(动态路由生成)和后端权限控制(接口访问权限),其实还应该做一层数据访问权限,但是这和具体的业务结合比较紧凑,因此就没实现。
User
表存储用户登录信息
Role
表存储角色
UserRoleRelation
表存储用户角色关系
BackendPermission
表存储后端权限(当服务器启动时会将所有路径都自动保存在该表里)
RoleBackendPermissionRelation
表存储角色拥有的后端权限
FrontendPermission
表存储前端路由信息.
RoleFrontendPermissionRelation
表存储角色拥有的前端路由信息
后端系统 后端系统是基于Springboot+shiro构建的,整个系统的核心就在shiro的配置上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @Configuration public class ShiroConfig { private static final Logger LOGGER = LoggerFactory.getSystemLogger(ShiroConfig.class); @Autowired private ApplicationContext applicationContext; @Resource private SecurityManager securityManager; @PostConstruct private void initStaticSecurityManager () { SecurityUtils.setSecurityManager(securityManager); } @Bean(name = "securityManager") public DefaultWebSecurityManager defaultWebSecurityManager (@Autowired DatabaseRealm shiroDatabaseRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroDatabaseRealm); securityManager.setSessionManager(buildSessionManager()); return securityManager; } private SessionManager buildSessionManager () { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionIdCookie(buildCookie()); sessionManager.setSessionIdCookieEnabled(true ); sessionManager.setSessionIdUrlRewritingEnabled(false ); sessionManager.setGlobalSessionTimeout(AuthConstant.GlobalSessionTimeout); sessionManager.setDeleteInvalidSessions(true ); sessionManager.setSessionValidationSchedulerEnabled(true ); sessionManager.setSessionValidationInterval(AuthConstant.SessionValidationInterval); return sessionManager; } public SimpleCookie buildCookie () { SimpleCookie simpleCookie = new SimpleCookie(TOKEN_NAME); simpleCookie.setPath("/" ); simpleCookie.setHttpOnly(true ); simpleCookie.setMaxAge(-1 ); return simpleCookie; } @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter (SecurityManager securityManager) { LOGGER.info("start shiroFilter setting" ); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/" ); shiroFilterFactoryBean.setSuccessUrl("/#/dashboard" ); shiroFilterFactoryBean.setUnauthorizedUrl("/403" ); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); Map<String, Filter> filtersMap = new LinkedHashMap<>(); filtersMap.put("apiAccessControlFilter" , new ApiAccessControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); filterChainDefinitionMap.put("/static/**" , "anon" ); filterChainDefinitionMap.put("/#/login/**" , "anon" ); filterChainDefinitionMap.put("/api/user/auth/login" , "anon" ); filterChainDefinitionMap.put("/logout" , "logout" ); filterChainDefinitionMap.put("/api/**" , "apiAccessControlFilter" ); filterChainDefinitionMap.put("/**" , "logFilter" ); filterChainDefinitionMap.put("/**" , "authc" ); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); LOGGER.info("shirFilter config fineshed" ); return shiroFilterFactoryBean; } @Bean public CorsFilter corsFilter () { CorsConfiguration config = new CorsConfiguration(); if (!SpringUtil.isInProduction(applicationContext)) { LOGGER.info("进行非生产模式CORS配置" ); config.addAllowedOrigin("*" ); config.setAllowCredentials(true ); config.addAllowedMethod("*" ); config.addAllowedHeader("*" ); config.addExposedHeader("Set-Cookie" ); } UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**" , config); return new CorsFilter(configSource); } }
整个配置有几个关键的地方
1 2 SimpleCookie simpleCookie = new SimpleCookie(TOKEN_NAME); simpleCookie.setHttpOnly(true );
token是存储在cookie中,由后端传给前端的。而且这个cookie前端是不可读的,避免xss攻击。
1 2 3 4 Map<String, Filter> filtersMap = new LinkedHashMap<>(); filtersMap.put("apiAccessControlFilter" , new ApiAccessControlFilter()); filterChainDefinitionMap.put("/api/user/auth/login" , "anon" ); filterChainDefinitionMap.put("/api/**" , "apiAccessControlFilter" );
定义了一个ApiAccessControlFilter
,只有当访问以/api/
开头的接口时才会受到后端权限控制。
1 2 3 4 5 6 7 8 if (!SpringUtil.isInProduction(applicationContext)) { LOGGER.info("进行非生产模式CORS配置" ); config.addAllowedOrigin("*" ); config.setAllowCredentials(true ); config.addAllowedMethod("*" ); config.addAllowedHeader("*" ); config.addExposedHeader("Set-Cookie" ); }
这里做了一个是否在生产环境中的判断,因为在开发模式中,前端工程是直接运行在node服务中的,因此要做跨域访问,所以在非生产环境中允许跨域访问。
后端还有一些其他的功能,比如日志记录,参数校验,请求统计等等,这些非核心功能可以参考最后的工程代码。
前端系统 前端系统是基于vue-element-admin 进行二次开发的。
主要修改的就是cookie的存储,vue-element-admin 默认是通过http response body获取token,但是我修改成了通过header cookie返回,并且cookie默认不可读。
vue-element-admin 自带
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 login ({ commit }, userInfo ) { const { username, password } = userInfo return new Promise ((resolve, reject ) => { login({ username : username.trim(), password : password }).then(response => { const { data } = response commit('SET_TOKEN' , data.token) setToken(data.token) resolve() }).catch(error => { reject(error) }) }) service.interceptors.request.use( config => { return config }, error => { console .log(error) return Promise .reject(error) } )
还有一点修改就是前端路由的修改。
vue-element-admin 默认的是权限控制是在前端代码中控制的,后端只需要返回用户拥有的角色,然后前端根据角色信息找到权限然后生成路由。我修改成了整个前端的路由都是由后端控制返回的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const actions = { generateRoutes ({ commit }, roles ) { return new Promise (resolve => { getUserFrontendPermissions().then(response => { let routeNodes = response.data.routeNodes importComponent(routeNodes) commit('SET_ROUTES' , routeNodes) resolve(routeNodes) }) }) } } function importComponent (routeNodes ) { for (var rn of routeNodes) { if (rn.component == "Layout" ) { rn.component = Layout } else { let componentPath = rn.component rn.component = () => import (`@/views/${componentPath} ` ) } if (rn.children && rn.children.length > 0 ) { importComponent(rn.children) } } }
主要的函数就是importComponent(routeNodes), 采用递归的方式import组件.
这里遇到一点小问题,webpack 编译es6 动态引入 import() 时不能传入变量, 但一定要用变量的时候,可以通过字符串模板来提供部分信息给webpack;例如import(./path/${myFile}), 这样编译时会编译所有./path下的模块. 参考在vue中import()语法为什么不能传入变量?
整体的代码就是这么多吧,当然在整个调试过程中还是花了些时间的,具体的可以参考我的提交记录 admin-solution