摘要:前言本文主要使用来实现前后端分离的认证登陆和权限管理,适合和我一样刚开始接触前后端完全分离项目的同学,但是你必须自己搭建过前端项目和后端项目,本文主要是介绍他们之间的互通,如果不知道这么搭建前端项目的同学可以先找别的看一下。
前言
本文主要使用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
欢迎交流,有问题和不清楚的地方我会解答,谢谢观看!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/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的搭建...
阅读 1379·2021-11-25 09:43
阅读 2231·2021-09-27 13:36
阅读 1092·2021-09-04 16:40
阅读 1931·2019-08-30 11:12
阅读 3292·2019-08-29 14:14
阅读 547·2019-08-28 17:56
阅读 1298·2019-08-26 13:50
阅读 1228·2019-08-26 13:29