摘要:前言本文主要使用來實現前后端分離的認證登陸和權限管理,適合和我一樣剛開始接觸前后端完全分離項目的同學,但是你必須自己搭建過前端項目和后端項目,本文主要是介紹他們之間的互通,如果不知道這么搭建前端項目的同學可以先找別的看一下。
前言
本文主要使用spring boot + shiro + vue來實現前后端分離的認證登陸和權限管理,適合和我一樣剛開始接觸前后端完全分離項目的同學,但是你必須自己搭建過前端項目和后端項目,本文主要是介紹他們之間的互通,如果不知道這么搭建前端項目的同學可以先找別的blog看一下。
自己摸索了一下,可能會有一些問題,也有可能有更好的實現方式,但這個demo主要是用來記錄自己搭建系統,獨立完成前后端分離項目的過程,并且作為自己的畢業設計框架。所以有問題的話歡迎提出,共同交流。源碼在github上,有需要的同學可以自己去取(地址在結尾)。
1.前端登陸頁面輸入http://localhost:8080/#/login會跳轉到前端登陸界面,輸入用戶名密碼后向后端 localhost:8888 發送驗證請求
2.后臺接受輸入信息后,通過shiro認證,向前臺返回認證結果,密碼是通過md5加密的
3.登陸成功后,權限認證,有些頁面只能管理員才能進入,有些按鈕只能擁有某項權限的人才能看到,后臺有些接口只能被有權限的人訪問。
前端工程在8080接口,發送的請求如何轉發到后臺8888接口
傳統的前后端未分離項目可以通過shiro標簽在前臺進行細粒度按鈕控制,獨立的前端vue項目如何做到這樣的控制
同上,前端項目如何實現帶權限的頁面跳轉,因為跳轉頁面的請求不會走后臺,后臺只提供數據
解決思路:這么解決上面的問題?我這里的思路是(注*思路最重要,代碼只會貼關鍵代碼,全部代碼請上git上取):
8080端口請求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包(全部的包看源碼,只貼核心的)
org.apache.shiro shiro-spring ${shiro.version} org.apache.shiro shiro-core 1.4.0 compile
配置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 { Listroles = sysRoleService.selectRoleByUserId(User.getId()); for (SysRole role : roles) { authorizationInfo.addRole(role.getRole());//角色存儲 } //此處如果多個角色都擁有某項權限,bu會數據重復,內部用的是Set List 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"); // 設置攔截器 MapfilterChainDefinitionMap = 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 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() { Mapmap = new HashMap (); 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(); Listroles = sysRoleService.selectRoleByUserId(user.getId()); Set roleList = new HashSet<>(); Set permissionList = new HashSet<>(); for (SysRole role : roles) { roleList.add(role.getRole());//角色存儲 } //此處如果多個角色都擁有某項權限,bu會數據重復,內部用的是Set List 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; } }
好啦!到這里后臺的工作基本完成了,現在去開發前臺
前臺開發流程登陸頁面的開發
土地經營管理系統登錄 Tips : 用戶名和密碼隨便填。
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") } } })
//在按鈕中設置指令,這樣只有管理員才能看到這個按鈕并使用,配置權限同理編輯
配置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中不懂的知識點可以直接取官網上面找,比我在這邊講清楚
后端地址:git@github.com:Attzsthl/land-mange.git前端地址:git@github.com:Attzsthl/land-mange-fronted.git
歡迎交流,有問題和不清楚的地方我會解答,謝謝觀看!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/77578.html
摘要:開公眾號差不多兩年了,有不少原創教程,當原創越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章系列處理登錄請求前后端分離一使用完美處理權限問題前后端分離二使用完美處理權限問題前后端分離三中密碼加鹽與中異常統一處理 開公眾號差不多兩年了,有不少原創教程,當原創越來越多時,大家搜索起來就很不方便,因此做了一個索引幫助大家快速找到需要的文章! Spring Boo...
摘要:此文章僅僅說明在整合時的一些坑并不是教程增加依賴集成依賴配置三個必須的用于授權和登錄創建自己的實例用于實現權限三種方式實現定義權限路徑第一種使用角色名定義第二種使用權限定義第三種使用接口的自定義配置此處配置之后需要在對應的 此文章僅僅說明在springboot整合shiro時的一些坑,并不是教程 增加依賴 org.apache.shiro shiro-spring-...
摘要:雖然,直接用和進行全家桶式的合作是最好不過的,但現實總是欺負我們這些沒辦法決定架構類型的娃子。并非按輸入順序。遍歷時只能全部輸出,而沒有順序。設想以下,若全局劫持在最前面,那么只要在襠下的,都早早被劫持了。底層是數組加單項鏈表加雙向鏈表。 雖然,直接用Spring Security和SpringBoot 進行全家桶式的合作是最好不過的,但現實總是欺負我們這些沒辦法決定架構類型的娃子。 Apa...
摘要:框架具有輕便,開源的優點,所以本譯見構建用戶管理微服務五使用令牌和來實現身份驗證往期譯見系列文章在賬號分享中持續連載,敬請查看在往期譯見系列的文章中,我們已經建立了業務邏輯數據訪問層和前端控制器但是忽略了對身份進行驗證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API 重拾后端之Spring Boot(一):REST API的搭建...
閱讀 1415·2021-11-25 09:43
閱讀 2273·2021-09-27 13:36
閱讀 1127·2021-09-04 16:40
閱讀 1965·2019-08-30 11:12
閱讀 3323·2019-08-29 14:14
閱讀 576·2019-08-28 17:56
閱讀 1336·2019-08-26 13:50
閱讀 1260·2019-08-26 13:29