用到的技術:
后台: java (springboot+shiro) 。創建項目-可參考 寫一個簡易的java項目(一)
前台: vue-admin-template (前台權限參考vue-element-admin)。下載配置-可參考 寫一個簡易的java項目(三)
編輯器:
后台:IntelliJ IDEA
前台:Visual Studio Code
后台:
第一步:打印日志 &確認前台傳過來的參數:賬號密碼
這里我使用fastjson的方法 獲取用戶密碼,代碼如下
pom:

<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency>
登陸打印結果:
第二步: 這里我們先創建三張表:
因為只是登陸 還沒到權限所以 主要是用戶表: 存一些基本信息如 賬號 密碼 頭像 密碼鹽(如果需要的話) 角色id
sys_user 用戶
sys_role 角色
sys_permission 權限
用戶表:

CREATE TABLE `sys_user` ( `user_id` bigint NOT NULL COMMENT '主鍵id', `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '頭像', `account` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '賬號', `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密碼', `salt` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '密碼鹽', `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名字', `birthday` datetime DEFAULT NULL COMMENT '生日', `sex` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '性別', `email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '郵箱', `phone` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '電話', `role_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色id(多個逗號隔開)', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `create_user` bigint DEFAULT NULL COMMENT '創建人', `update_time` datetime DEFAULT NULL COMMENT '更新時間', `update_user` bigint DEFAULT NULL COMMENT '更新人', PRIMARY KEY (`user_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用戶表';
角色表:

CREATE TABLE `sys_role` ( `role_id` bigint NOT NULL COMMENT '主鍵id', `pid` bigint DEFAULT NULL COMMENT '父角色id', `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '角色名稱', `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '描述', `sort` int DEFAULT NULL COMMENT '序號', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `update_time` datetime DEFAULT NULL COMMENT '修改時間', `create_user` bigint DEFAULT NULL COMMENT '創建用戶', `update_user` bigint DEFAULT NULL COMMENT '修改用戶', `deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除', PRIMARY KEY (`role_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色表';
權限表:

CREATE TABLE `sys_permission` ( `id` int NOT NULL AUTO_INCREMENT, `role_id` int DEFAULT NULL COMMENT '角色ID', `permission` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '權限', `create_time` datetime DEFAULT NULL COMMENT '創建時間', `update_time` datetime DEFAULT NULL COMMENT '更新時間', `deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='權限表';
第三步:shiro 權限認證
1.pom:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.4.0</version> </dependency>
2.自定義Realm 主要作用有:驗證登陸人的賬號密碼是否正確、驗證賬號的權限信息等等
extends AuthorizingRealm 重寫兩個方法:
doGetAuthorizationInfo(PrincipalCollection principalCollection)
doGetAuthenticationInfo(AuthenticationToken authenticationToken)
KingRealm
第一個方法:授權:這里需要寫一些方法->通過角色id 獲取角色名稱 和 權限信息
/** * 權限認證 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 獲取用戶 User user = (User) principalCollection.getPrimaryPrincipal(); String roleIds = user.getRoleIds(); // 通過角色id獲取用戶權限 Set<String> roles = roleService.getRolesByRoleIds(roleIds); Set<String> permissions = permissionService.getPermissionsByRoleIds(roleIds); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(permissions); return info; }
service
public Set<String> getPermissionsByRoleIds(String roleIds) { Set<String> permissions = new HashSet<String>(); if (StringUtils.isEmpty(roleIds)) { return permissions; } List<Permission> permissionList = permissionMapper.getPermissionsByRoleIds(roleIds); for (Permission permission : permissionList) { permissions.add(permission.getPermission()); } return permissions; }
第二個方法:認證:這里需要一個方法-》就是通過賬號獲取用戶信息
/** * 登錄認證 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); String password = new String(token.getPassword()); if (StringUtils.isEmpty(username)) { throw new AccountException("用戶名不能為空"); } if (StringUtils.isEmpty(password)) { throw new AccountException("密碼不能為空"); } // 根據用戶名從數據庫中查詢該用戶 User user = userService.getByUsername(username); if(user == null) { throw new UnknownAccountException("賬號或密碼不正確");// 不存在該賬號 } // 驗證賬號密碼是否正確 這里使用 :Md5(token密碼+鹽) = 數據庫密碼 的方式 String requestPassword = SaltMd5Util.toMd5String(password, user.getSalt());// token 中的password String dbPassword = user.getPassword();// 數據庫中的 password if (dbPassword == null || !dbPassword.equalsIgnoreCase(requestPassword)) { throw new UnknownAccountException("賬號或密碼不正確"); } // 把當前用戶存到 Session 中 SecurityUtils.getSubject().getSession().setAttribute("user", user); // 傳入用戶名和密碼進行身份認證,並返回認證信息 AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user, password, getName()); return authcInfo; }
加密util:
public static String toMd5String(String password, String salt) { String secret = password+salt; return DigestUtils.md5DigestAsHex(secret.getBytes()); }
3.shiro 配置 ShiroConfig
首先把我們剛剛寫好的Realm 引進來:
@Configuration public class ShiroConfig { @Bean public KingRealm KingRealm() {return new KingRealm(); } }
加上shiro 過濾器:
/** * shiro過濾器 * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 攔截器 // anon 不會攔截 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/user/logout", "anon"); filterChainDefinitionMap.put("/user/login", "anon"); // authc 攔截 filterChainDefinitionMap.put("/**", "authc"); // 默認登錄頁面地址 shiroFilterFactoryBean.setLoginUrl("/user/login"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }
加上安全管理器:
/** * 安全管理器 * @return */ @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(KingRealm());return securityManager; }
注解權限控制:
切點:
@Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }
切面:
@Bean @DependsOn("lifecycleBeanPostProcessor") public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; }
第四步:寫登陸、獲取用戶信息、退出登錄 三個后台接口
登陸:
@ResponseBody @PostMapping("/login") public ResponseData login(@RequestBody String body) { log.info("===登陸請求===請求參數為body:{}",body); JSONObject json=JSONObject.parseObject(body); String username= (String) json.get("username"); String password= (String) json.get("password"); if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { return ResponseData.error("賬號、密碼不能為空"); } Subject currentUser = SecurityUtils.getSubject();// 獲取當前用戶信息 if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(username,password); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.error("===登陸請求===錯誤:{}", "賬號或密碼錯誤"); return ResponseData.error("賬號或密碼錯誤"); } catch (Exception e) { log.error("===登陸請求===錯誤:{}", "賬號或密碼錯誤"); return ResponseData.error("賬號或密碼錯誤"); } } log.info("返回結果:{}", JSONObject.toJSONString(currentUser.getSession().getId())); return ResponseData.success(currentUser.getSession().getId()); }
獲取用戶信息:
@ResponseBody @RequestMapping("/info") public ResponseData info() { Subject currentUser = SecurityUtils.getSubject(); User user = (User) currentUser.getPrincipal(); Map<String, Object> data = new HashMap<>(); data.put("name", user.getAccount()); data.put("avatar", user.getAvatar()); String roleIds = user.getRoleIds(); // 通過角色id獲取用戶權限 Set<String> roles = roleService.getRolesByRoleIds(roleIds); Set<String> permissions = permissionService.getPermissionsByRoleIds(roleIds); data.put("roles", roles); data.put("permissions", permissions); log.info("用戶信息:{}", JSONObject.toJSONString(data)); return ResponseData.success(data); }
退出登錄:
@ResponseBody @PostMapping("/logout") public ResponseData login() { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); log.info("===退出登錄===:{}", JSONObject.toJSONString(currentUser.getSession().getId())); return ResponseData.success(); }
看一下測試效果:
失敗:
控制台輸出:
成功:
控制台輸出:
點擊退出->
回到了登錄頁面
控制台輸出:
這是我之前的測試頁面,做了簡單的增刪改查:
怎樣給admin 這個用戶添加權限?
后台:
直接加注解看看:
###如果出現這種問題:
404
解決一下這個問題:原因是 在ShiroConfig 中少加了代碼:
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
###
前台:
###插曲 -了解一下vue-admin-template 的登陸驗證 > <。
它用到了vuex, 什么是vuex?-》》vuex 學習筆記
這里我改了個東西:
登錄的返回值。因為后台我直接返回了token ,所以這里把 .token 去掉了 。不需要盲目的改 看自己返回的結果。
這樣我們就可以通過 getToken() 獲取到token 了。
而我們在permission.js 的 router.beforeEach 方法中 (路由攔截) 調用了此方法,判斷用戶是否登陸過了。
#main.js 中可以看到引入了權限=》 permission.js
###
其實既然沒有權限就沒必要顯示出來 -》
菜單權限:
看一下permission.js 中路由攔截的方法:
router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // set page title document.title = getPageTitle(to.meta.title) // determine whether the user has logged in const hasToken = getToken() if (hasToken) {// 如果存在token if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = store.getters.name if (hasGetUserInfo) {// 如果store中存在用戶名 next() } else { try { // get user info 獲取用戶信息 await store.dispatch('user/getInfo') next() } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') // Message.error(error || 'Has Error') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else {// 沒有token /* has no token*/ if (whiteList.indexOf(to.path) !== -1) {// 白名單免登陸 // in the free login whitelist, go directly next() } else {// 重定向到首頁 // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } })
思路:獲取用戶信息后把權限信息也存起來,然后處理菜單只顯示有權限的菜單。
第一步:獲取后台的權限信息
看一下前台可以不可獲取用戶的權限信息,如果可以 我們需要把權限信息存起來。
用戶信息存放的位置在-vuex:src/store/modules/user.js
state中定義兩個變量roles 和permissions 分別存后台傳過來的角色名稱和權限。
mutations 寫好對應的方法,以便調用賦值。
getters
后台傳過來的值:
打印了一下 getInfo 返回的data,大概是這樣 :
找到getInfo方法 給這兩個參數賦值。在退出登錄時清空。
第二步:處理菜單只顯示有權限的部分
這里為了省事,就直接把 vue-element-admin 中的代碼粘過來,改一改好了。o.o
首先是:permission.js
然后是store->permission
然后是index
getter
改動:
第一步:由於我想用權限信息 permissions 來確定菜單,而不是用戶的角色。所以過濾菜單的方法傳參 傳permissions。
同理,permission.js 中所有的role 都改成了permission 也是為了代碼的可讀性。
第二步: permission.js 中傳的參數 asyncRoutes
不需要權限的:
需要權限的:為了測試,現在把測試菜單放到這下面:
現在 router 中的代碼:

import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) /* Layout */ import Layout from '@/layout' /** * Note: sub-menu only appear when route children.length >= 1 * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html * * hidden: true if set true, item will not show in the sidebar(default is false) * alwaysShow: true if set true, will always show the root menu * if not set alwaysShow, when item has more than one children route, * it will becomes nested mode, otherwise not show the root menu * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb * name:'router-name' the name is used by <keep-alive> (must set!!!) * meta : { roles: ['admin','editor'] control the page roles (you can set multiple roles) title: 'title' the name show in sidebar and breadcrumb (recommend set) icon: 'svg-name' the icon show in the sidebar breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) activeMenu: '/example/list' if set path, the sidebar will highlight the path you set } */ /** * constantRoutes * a base page that does not have permission requirements * all roles can be accessed */ export const constantRoutes = [ { path: '/login', component: () => import('@/views/login/index'), hidden: true }, { path: '/404', component: () => import('@/views/404'), hidden: true }, { path: '/', component: Layout, redirect: '/dashboard', children: [{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index'), meta: { title: '首頁', icon: 'dashboard' } }] } // 404 page must be placed at the end !!! // { path: '*', redirect: '/404', hidden: true } ] export const asyncRoutes = [ { path: '/example', component: Layout, redirect: 'noredirect', alwaysShow: true, name: 'Example', meta: { permissions: ['/example'], title: '測試', icon: 'example' }, children: [ { path: 'table', name: '表格', component: () => import('@/views/mytable/index'), meta: { permissions: ['/example/table'], title: '測試表格', icon: 'table' } }, { path: 'other', name: '其他', component: () => import('@/views/table/index'), meta: { permissions: ['/example/other'], title: '測試其他', icon: 'table' } } ] }, // 404 page must be placed at the end !!! { path: '*', redirect: '/404', hidden: true } ] const createRouter = () => new Router({ // mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), routes: constantRoutes }) const router = createRouter() // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher // reset router } export default router
@/permission.js 中代碼

import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css' // progress bar style import { getToken } from '@/utils/auth' // get token from cookie import getPageTitle from '@/utils/get-page-title' NProgress.configure({ showSpinner: false }) // NProgress Configuration const whiteList = ['/login'] // no redirect whitelist router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // set page title document.title = getPageTitle(to.meta.title) // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = store.getters.name if (hasGetUserInfo) { next() } else { try { // store.dispatch('user/getInfo') // next() store.dispatch('user/getInfo').then(res => { // generate accessible routes map based on roles store.dispatch('permission/generateRoutes', res.permissions).then(() => { // dynamically add accessible routes debugger router.addRoutes(store.getters.addRoutes) // hack method to ensure that addRoutes is complete // set the replace: true, so the navigation will not leave a history record next({ ...to, replace: true }) }) }) } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') // Message.error(error || 'Has Error') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
第三步:渲染 -》 改成自己的 ok!
數據庫:
用戶:
權限
效果:
按鈕權限:
同理為了方便,我們把vue-element-admin 中的utils/permission.js 粘貼過來,做個簡單修改。
第一步,粘貼
同樣,把roles 改成permissions
第二步:粘貼 @/directive/permission/index.js 權限判斷指令
同樣,把role 改成permission
main.js
import permission from '@/directive/permission/index.js' // 權限判斷指令 Vue.directive('permission', permission)
頁面:
沒有權限時:
有權限時:
數據庫:
解決問題&補充:
問題一:把前台打包放到項目下啟動后出現如下錯誤:
排查錯誤引發原因得出結論:是shiro 攔截引起的。
Uncaught SyntaxError: Unexpected token '<'
解決方案:放過static 下的靜態文件即可
filterChainDefinitionMap.put("/static/**", "anon");
問題二:在本地啟動沒有問題,打包后就出現如下問題:
可能導致這個問題的原因有很多。這里我的問題竟然是:。。。 clean 之后直接打包導致的。
2020-10-10 15:11:41.962 INFO 8784 --- [ost-startStop-1] ConditionEvaluationReportLoggingListener : Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2020-10-10 15:11:41.966 ERROR 8784 --- [ost-startStop-1] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class Action: Consider the following: If you want an embedded database (H2, HSQL or Derby), please put it on the classpath. If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
正確的操作應該是 clean ->build->package
問題三: 退出登錄之后,點擊菜單時:url 雖然改變了,但頁面空白。點擊 enter 刷新后 又可顯示頁面。。
解決方案:
代碼:
await this.$store.dispatch('user/logout').then(() => { location.reload() })
問題四:獲取用戶信息失敗時,跳到登錄頁避免bug
代碼:
store.dispatch('user/getInfo').then(res => { // generate accessible routes map based on roles store.dispatch('permission/generateRoutes', res.permissions).then(() => { // dynamically add accessible routes router.addRoutes(store.getters.addRoutes) // hack method to ensure that addRoutes is complete // set the replace: true, so the navigation will not leave a history record next({ ...to, replace: true }) }) }).catch((error) => { store.dispatch('user/resetToken').then(() => { Message.error(error || '請重新登陸') next({ path: '/' }) }) })
補充一:在控制台打印mybatis SQL 語句
logging:
level:
com.example.king: DEBUG
控制台輸出:
補充二:配置 swagger
自動生成在線開發文檔,方便測試等優點
pom
<!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
配置:
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.any())// 限制包 .paths(PathSelectors.any())// 限制控制器 .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("King") .description("king-接口文檔") .contact("DarGi") .version("1.0") .build(); } }
shiroConfig 中放行:
filterChainDefinitionMap.put("/swagger-ui.html", "anon"); filterChainDefinitionMap.put("/swagger/**", "anon"); filterChainDefinitionMap.put("/swagger-resources/**", "anon"); filterChainDefinitionMap.put("/v2/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/configuration/**", "anon");
頁面:http://localhost:8091/swagger-ui.html#/
做個測試:
返回結果
@