本文主要使用spring boot + shiro + vue來實現先後端分離的認證登錄和權限管理,適合和我同樣剛開始接觸先後端徹底分離項目的同窗,可是你必須本身搭建過前端項目和後端項目,本文主要是介紹他們之間的互通,若是不知道這麼搭建前端項目的同窗能夠先找別的blog看一下。
本身摸索了一下,可能會有一些問題,也有可能有更好的實現方式,但這個demo主要是用來記錄本身搭建系統,獨立完成先後端分離項目的過程,而且做爲本身的畢業設計框架。因此有問題的話歡迎提出,共同交流。源碼在github上,有須要的同窗能夠本身去取(地址在結尾)。css
1.前端登錄頁面輸入http://localhost:8080/#/login會跳轉到前端登錄界面,輸入用戶名密碼後向後端 localhost:8888 發送驗證請求
2.後臺接受輸入信息後,經過shiro認證,向前臺返回認證結果,密碼是經過md5加密的
3.登錄成功後,權限認證,有些頁面只能管理員才能進入,有些按鈕只能擁有某項權限的人才能看到,後臺有些接口只能被有權限的人訪問。前端
這麼解決上面的問題?我這裏的思路是(注*思路最重要,代碼只會貼關鍵代碼,所有代碼請上git上取):vue
1.前端技術棧node
框架:vue+elementui+axios 語言:es6,js 環境:node8 + yarn 打包工具: webpack 開發工具:vscode
2.後端mysql
框架:spring Boot多模塊+ maven + shiro + jpa + mysql8.0 開發工具:intellij idea
1.後端開發流程webpack
·搭建spring boot多模塊項目(本文不會介紹) ·建立shiro角色和權限的數據表 ·集成shiro框架和md5加密 ·開發登錄認證接口
2.前端開發流程ios
·搭建前端運行環境和webpack項目(本文不會介紹) ·開發登錄頁面組件 ·跨域——來支持請求後端接口 ·路由開發,鉤子函數(頁面跳轉控制),cookieUtil開發(存儲後臺roles和permissions信息),自定義指令(前端細粒度控制) ·啓動項目,測試登錄及權限驗證
1.建立shiro角色和權限的數據表nginx
2.集成shiro框架和md5加密git
<!-- 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>
/** * 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; } }
@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(); }*/ }
@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); }
@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>
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); };
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>
// 在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;