生活不易、且行且珍惜。网站首页 程序人生
从零开始做网站6-springboot集成shiro+vue实现登录和权限控制
发布时间:2022-05-23 15:45编辑:zj 阅读:文章分类: 网站互动QQ群:170915747
到上一篇已经把前后端的项目底子搭好了,今天开始做功能,首先就是后台管理系统登录功能。
Shiro简介
Apache Shiro是一个轻量级的身份验证与授权Java安全框架。对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用简单易用的Shiro就足够了,灵活性高。springboot本身是提供了对security的支持。springboot暂时没有集成shiro,这得自己配。
Shiro三个核心概念:Subject、SecurityManager 和 Realms,还有四大功能——Authentication(认证)、Authorization(授权)、Session Management(会话管理)、Cryptography(加密)
Subject一词是一个安全术语
狭指: 当前的操作用户(用户主体—把操作交给securityManager)
泛指:当前跟软件交互的东西(人,第三方进程、后台帐户(Daemon Account)、定时作业(Corn Job)等等)
在程序中你都能轻易的获得Subject,允许在任何需要的地方进行安全操作。每个Subject对象都必须与一个SecurityManager进行绑定,你访问Subject对象其实都是在与SecurityManager里的特定Subject进行交互。
SecurityManager
Subject的“幕后”推手是SecurityManager(安全管理器,关联realm)。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。它是Shiro框架的核心,充当“保护伞”,引用了多个内部嵌套安全组件,它们形成了对象图。但是,一旦SecurityManager及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在Subject API调用上。
Realms
Shiro的第三个也是最后一个概念是Realm(连接数据的桥梁)。Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
详细就每个点去看些文章了解吧,不做过多描述。
本系统密码加密使用md5+盐加密
加盐,是提高 hash 算法的安全性的一个常用手段。下面是加盐加密与验证的逻辑:
用户注册时,输入用户名密码(明文),向后台发送请求
后台将密码加上随机生成的盐并 hash,再将 hash 后的值作为密码存入数据库,盐也作为单独的字段存起来
用户登录时,输入用户名密码(明文),向后台发送请求
后台根据用户名查询出盐,和密码组合并 hash,将得到的值与数据库中存储的密码比对,若一致则通过验证
然后就是开搞---实现登录功能 直接上代码
添加依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.5</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.5</version> </dependency>
shiro配置的顺序如下:
创建 Realm 并重写获取认证与授权信息的方法
创建配置类,包括创建并配置 SecurityManager 等
创建shiro包、在shiro包下创建ShiroRealm类
package com.zjlovelt.shiro; import com.zjlovelt.entity.SysUser; import com.zjlovelt.service.SysUserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; public class ShiroRealm extends AuthorizingRealm { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SysUserService userService; //重写获取授权信息方法 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { logger.info("doGetAuthorizationInfo+"+principalCollection.toString()); SysUser user = userService.getByUserName((String) principalCollection.getPrimaryPrincipal()); //把principals放session中 key=userId value=principals SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getId()),SecurityUtils.getSubject().getPrincipals()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //赋予角色 /* for(Role userRole:user.getRoles()){ info.addRole(userRole.getName()); } //赋予权限 for(Permission permission:permissionService.getByUserId(user.getId())){ // if(StringUtils.isNotBlank(permission.getPermCode())) info.addStringPermission(permission.getName()); }*/ //设置登录次数、时间 // userService.updateUserLogin(user); return info; } // 获取认证信息,即根据 token 中的用户名从数据库中获取密码、盐等并返回 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("doGetAuthenticationInfo +" + authenticationToken.toString()); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = token.getUsername(); logger.info(userName+token.getPassword()); SysUser user = userService.getByUserName(token.getUsername()); if (user != null) { /* byte[] salt = Encodes.decodeHex(user.getSalt()); ShiroUser shiroUser=new ShiroUser(user.getId(), user.getLoginName(), user.getName());*/ String salt = user.getSalt(); //用户盐值 最后需转byte[] //设置用户session Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("user", user); return new SimpleAuthenticationInfo(userName,user.getPassword(), ByteSource.Util.bytes(salt),getName()); } else { return null; } } }
在shiro包下创建ShiroConfiguration
package com.zjlovelt.shiro; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.filter.authc.LogoutFilter; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.Filter; /** * shiro配置类 * Created by zj on 2022/4/19. */ @Configuration public class ShiroConfiguration { /** * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类, * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。 * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。 */ @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * HashedCredentialsMatcher,这个类是为了对密码进行编码的, * 防止密码在数据库里明码保存,当然在登陆认证的时候, * 这个类也负责对form里输入的密码进行编码。 */ @Bean(name = "hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("MD5"); credentialsMatcher.setHashIterations(2); credentialsMatcher.setStoredCredentialsHexEncoded(true); return credentialsMatcher; } /** * ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm, * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。 */ @Bean(name = "shiroRealm") @DependsOn("lifecycleBeanPostProcessor") public ShiroRealm shiroRealm() { ShiroRealm realm = new ShiroRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } // /** // * EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来, // * 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。 // */ // @Bean(name = "ehCacheManager") // @DependsOn("lifecycleBeanPostProcessor") // public EhCacheManager ehCacheManager() { // return new EhCacheManager(); // } /** * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。 * // */ @Bean(name = "securityManager") public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); // securityManager.setCacheManager(ehCacheManager()); return securityManager; } /** * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。 * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。 */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //Shiro的核心安全接口,这个属性是必须的 shiroFilterFactoryBean.setSecurityManager(securityManager()); Map<String, Filter> filters = new LinkedHashMap<String, Filter>(); LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setRedirectUrl("/login"); shiroFilterFactoryBean.setFilters(filters); //anon:没有参数,表示可以匿名使用。例子:/admin/**=anon //authc:没有参数,表四需要认证(登录)才能使用。例子:/user/**=authc //roles:角色过滤器,判断当前用户是否拥有指定角色。例子:admins/**=roles[“admin,guest”] Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>(); filterChainDefinitionManager.put("/logout", "logout"); filterChainDefinitionManager.put("/api/**", "authc"); filterChainDefinitionManager.put("/**", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager); shiroFilterFactoryBean.setSuccessUrl("/"); shiroFilterFactoryBean.setUnauthorizedUrl("/403"); return shiroFilterFactoryBean; } /** * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。 */ @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; } /** * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类, * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor(); aASA.setSecurityManager(securityManager()); return aASA; } }
最后使用 shiro 验证登录,编写登录接口方法
@Autowired private SysUserService userService; @RequestMapping(value = "/admin/login", method = RequestMethod.POST) public Result login(SysUser user) { String username = user.getUsername(); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, user.getPassword()); try { subject.login(usernamePasswordToken); return Result.ok("登录成功").setData(usernamePasswordToken ); } catch (IncorrectCredentialsException e) { return Result.fail("密码错误"); } catch (UnknownAccountException e) { return Result.fail("账号不存在"); } }
因为博客暂不需要注册功能,就后端直接生成用户名和密码吧,如果需要注册改成接口即可
public static void main(String[] args) { SysUser user = new SysUser(); String username = "admin"; String password = "123456"; username = HtmlUtils.htmlEscape(username); // 生成盐,默认长度 16 位 String salt = new SecureRandomNumberGenerator().nextBytes().toString(); // 设置 hash 算法迭代次数 int times = 2; // 得到 hash 后的密码 String encodedPassword = new SimpleHash("md5", password, salt, times).toString(); // 存储用户信息,包括 salt 与 hash 后的密码 System.out.println("salt:" + salt); System.out.println("password:"+ encodedPassword); }
后端开发好了,然后就是前端了
首先是登录页代码,.vue页面分为三个模块,template是组件的模板结构页面元素,script是组件的 JavaScript 行为,style是组件的样式
<template> <div class="login-wrap"> <div class="ms-login"> <div class="ms-title">ltBlog-甜宝快更系统</div> <el-form :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content"> <el-form-item prop="username"> <el-input v-model="param.username" placeholder="用户名"> <template #prepend> <el-button icon="el-icon-user"></el-button> </template> </el-input> </el-form-item> <el-form-item prop="password"> <el-input type="password" placeholder="密码" v-model="param.password" @keyup.enter="submitForm()"> <template #prepend> <el-button icon="el-icon-lock"></el-button> </template> </el-input> </el-form-item> <div class="login-btn"> <el-button type="primary" @click="submitForm()">登录</el-button> </div> <p class="login-tips">Tips : 甜宝登陆后记得发文章呀。</p> </el-form> </div> </div> </template> <script> import { ref, reactive,getCurrentInstance } from "vue"; import { useStore } from "vuex"; import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; export default { setup() { const router = useRouter(); const param = reactive({ username: "", password: "", }); const rules = { username: [ { required: true, message: "请输入用户名", trigger: "blur", }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" }, ], }; const login = ref(null); const $http = getCurrentInstance()?.appContext.config.globalProperties.$http; const submitForm = () => { console.log(param); login.value.validate((valid) => { if (valid) { $http({method:'post',url:'/admin/login',params: param}).then(data => { console.log(data) if (data.success === true) { ElMessage.success(data.msg); localStorage.setItem("ms_token", data.data); //记住登入状态,将用户信息放到localStorage localStorage.setItem("ms_username", username); router.push("/****"); //登入成功后跳转到后台首页 } else { ElMessage.error(data.msg); } }) } else { ElMessage.error("登录失败"); return false; } }); }; const store = useStore(); store.commit("clearTags"); return { param, rules, login, submitForm, }; }, }; </script> <style scoped> .login-wrap { position: relative; width: 100%; height: 100%; background-image: url(src/assets/img/login-bg.jpg); background-size: cover; background-repeat: no-repeat; background-position: center; } .ms-title { width: 100%; line-height: 50px; text-align: center; font-size: 20px; color: #fff; border-bottom: 1px solid #ddd; } .ms-login { position: absolute; left: 44%; top: 50%; width: 550px; margin: -190px 0 0 -175px; border-radius: 5px; background: rgba(255, 255, 255, 0.3); overflow: hidden; } .ms-content { padding: 30px 30px; } .login-btn { text-align: center; } .login-btn button { width: 100%; height: 36px; margin-bottom: 10px; } .login-tips { font-size: 12px; line-height: 30px; color: #fff; } </style>
页面的样子
登入成功的样子
登入失败的样子
就这样,springboot+shiro+vue的登录功能就开发好了
使用 Web Storage 存储键值对比存储 Cookie 方式更直观,而且容量更大,它包含两种:localStorage 和 sessionStorage 。cookie 和 local/session Storage 分工又有所不同,cookie 可以作为传递的参数,并可通过后端进行控制,local/session Storage 则主要用于在客户端中保存数据,其传输需要借助 cookie 或其它方式完成。
cookie :
一般由服务器生成,可设置失效时间。如果在浏览器端生成cookie,默认是关闭浏览器后失效。每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题。
sessionStorage
临时存储,为每一个数据源维持一个存储区域,在浏览器打开期间存在,包括页面重新加载。仅在客户端(即浏览器)中保存,不参与和服务器的通信。
localStorage
长期存储,与 sessionStorage 一样,但是浏览器关闭后,数据依然会一直存在。仅在客户端(即浏览器)中保存,不参与和服务器的通信。
1 保存 // Json对象 const user = {name: 'sugar', 'cnt': '22'}; localStorage.setItem('userJson', JSON.stringify(user)); // 字符串 const str = "sugar"; localStorage.setItem('userString', str); 2 获取 // Json对象 var data1 = JSON.parse(localStorage.getItem('userJson')); // 字符串 var data2 = localStorage.getItem('userString'); 3 删除 // 删除一个 localStorage.removeItem('userJson'); // 删除所有 localStorage.clear();
不过用localStorage存储用户数据,然后路由再根据localStorage是否有用户信息校验用户是否登录还是有问题的,在控制台输入window.localStorage.setItem('user', JSON.stringify({"name":"admin"})); 就可以伪造信息从而避过登录了。
通常来说,在可以使用 cookie 的场景下,作为验证用途进行传输的用户名密码、sessionId、token 直接放在 cookie 里即可。而后端传来的其它信息则可以根据需要放在 local/session Storage 中,作为全局变量之类进行处理。
不过我们还是选择使用localStorage来存储用户信息,但是存入的信息是根据用户信息在后台生成的token,然后再修改下router/index.js的beforeEach方法,每次页面跳转都不再是判断localStorage中是否有用户信息,而是是否有token,如果有再去请求后台校验这个token是否正确,是否过期,如果错误或已过期就需要跳转到login重新登陆。
登入成功后还得有个退出登登入的功能
直接上代码
前端:
<el-dropdown-item divided command="loginout">退出登录</el-dropdown-item> if (command == "loginout") { $http({method:'post',url:'/logout'}).then(data => { if (data.success === true) { ElMessage.success(data.msg); localStorage.removeItem("ms_token"); localStorage.removeItem("ms_username"); //去掉localStorage中的用户信息 router.push("/login"); } else { ElMessage.error(data.msg); } }) }
后端:
@RequestMapping(value = "/logout", method = RequestMethod.POST) public Result logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); //shiro提供的方法,该方法会清除 session、principals,并把 authenticated 设置为 false return Result.ok("退出成功"); }
遗留问题,用户认证问题下篇解决。
关于菜单、按钮授权,菜单、角色管理,是个大工程,要搞比较久就先不做了,最主要的是对这个系统来说无用,个人博客,后台管理系统也就一个人用,一个账号所有权限都有就够了。等博客问世后面有时间再搞吧。
#去评论一下
标签:#Springboot#Vue
版权声明:本博客的所有原创内容皆为作品作者所有
转载请注明:来自ZJBLOG 链接:www.zjhuiwan.cn
「万物皆有时,比如你我相遇」
感谢大佬打赏【请选择支付宝或微信,再选择金额】
使用微信扫描二维码完成支付