3

SpringBoot集成shiro+vue实现登录和权限控

 1 year ago
source link: https://www.huhexian.com/45389.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SpringBoot集成shiro+vue实现登录和权限控

2022-05-2509:19:15评论

pache 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,将得到的值与数据库中存储的密码比对,若一致则通过验证

然后就是开搞---实现登录功能 直接上代码

添加依赖

  1.   <dependency>
  2.             <groupId>org.apache.shiro</groupId>
  3.             <artifactId>shiro-spring</artifactId>
  4.             <version>1.2.5</version>
  5.         </dependency>
  6.         <dependency>
  7.             <groupId>org.apache.shiro</groupId>
  8.             <artifactId>shiro-ehcache</artifactId>
  9.             <version>1.2.5</version>
  10.         </dependency>

shiro配置的顺序如下:

创建 Realm 并重写获取认证与授权信息的方法

创建配置类,包括创建并配置 SecurityManager 等

创建shiro包、在shiro包下创建ShiroRealm类

  1. package com.zjlovelt.shiro;
  2. import com.zjlovelt.entity.SysUser;
  3. import com.zjlovelt.service.SysUserService;
  4. import org.apache.shiro.SecurityUtils;
  5. import org.apache.shiro.authc.*;
  6. import org.apache.shiro.authz.AuthorizationInfo;
  7. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  8. import org.apache.shiro.realm.AuthorizingRealm;
  9. import org.apache.shiro.session.Session;
  10. import org.apache.shiro.subject.PrincipalCollection;
  11. import org.apache.shiro.util.ByteSource;
  12. import org.slf4j.Logger;
  13. import org.slf4j.LoggerFactory;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. public class ShiroRealm  extends AuthorizingRealm {
  16.     private Logger logger =  LoggerFactory.getLogger(this.getClass());
  17.     @Autowired
  18.     private SysUserService userService;
  19.     //重写获取授权信息方法
  20.     @Override
  21.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  22.         logger.info("doGetAuthorizationInfo+"+principalCollection.toString());
  23.         SysUser user = userService.getByUserName((String) principalCollection.getPrimaryPrincipal());
  24.         //把principals放session中 key=userId value=principals
  25.         SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getId()),SecurityUtils.getSubject().getPrincipals());
  26.         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  27.         //赋予角色
  28.        /* for(Role userRole:user.getRoles()){
  29.             info.addRole(userRole.getName());
  30.         //赋予权限
  31.         for(Permission permission:permissionService.getByUserId(user.getId())){
  32. //            if(StringUtils.isNotBlank(permission.getPermCode()))
  33.             info.addStringPermission(permission.getName());
  34.         //设置登录次数、时间
  35. //        userService.updateUserLogin(user);
  36.         return info;
  37.     // 获取认证信息,即根据 token 中的用户名从数据库中获取密码、盐等并返回
  38.     @Override
  39.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  40.         logger.info("doGetAuthenticationInfo +"  + authenticationToken.toString());
  41.         UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
  42.         String userName = token.getUsername();
  43.         logger.info(userName+token.getPassword());
  44.         SysUser user = userService.getByUserName(token.getUsername());
  45.         if (user != null) {
  46.            /* byte[] salt = Encodes.decodeHex(user.getSalt());
  47.             ShiroUser shiroUser=new ShiroUser(user.getId(), user.getLoginName(), user.getName());*/
  48.             String salt = user.getSalt(); //用户盐值 最后需转byte[]
  49.             //设置用户session
  50.             Session session = SecurityUtils.getSubject().getSession();
  51.             session.setAttribute("user", user);
  52.             return new SimpleAuthenticationInfo(userName,user.getPassword(), ByteSource.Util.bytes(salt),getName());
  53.         } else {
  54.             return null;

在shiro包下创建ShiroConfiguration 

  1. package com.zjlovelt.shiro;
  2. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
  3. import org.apache.shiro.spring.LifecycleBeanPostProcessor;
  4. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
  5. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  6. import org.apache.shiro.web.filter.authc.LogoutFilter;
  7. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  8. import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
  9. import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  10. import org.springframework.context.annotation.Bean;
  11. import org.springframework.context.annotation.Configuration;
  12. import org.springframework.context.annotation.DependsOn;
  13. import java.util.LinkedHashMap;
  14. import java.util.Map;
  15. import javax.servlet.Filter;
  16.  * shiro配置类
  17.  * Created by zj on 2022/4/19.
  18. @Configuration
  19. public class ShiroConfiguration {
  20.      * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
  21.      * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
  22.      * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
  23.     @Bean(name = "lifecycleBeanPostProcessor")
  24.     public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
  25.         return new LifecycleBeanPostProcessor();
  26.      * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
  27.      * 防止密码在数据库里明码保存,当然在登陆认证的时候,
  28.      * 这个类也负责对form里输入的密码进行编码。
  29.     @Bean(name = "hashedCredentialsMatcher")
  30.     public HashedCredentialsMatcher hashedCredentialsMatcher() {
  31.         HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
  32.         credentialsMatcher.setHashAlgorithmName("MD5");
  33.         credentialsMatcher.setHashIterations(2);
  34.         credentialsMatcher.setStoredCredentialsHexEncoded(true);
  35.         return credentialsMatcher;
  36.      * ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
  37.      * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
  38.     @Bean(name = "shiroRealm")
  39.     @DependsOn("lifecycleBeanPostProcessor")
  40.     public ShiroRealm shiroRealm() {
  41.         ShiroRealm realm = new ShiroRealm();
  42.         realm.setCredentialsMatcher(hashedCredentialsMatcher());
  43.         return realm;
  44. //    /**
  45. //     * EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,
  46. //     * 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
  47. //    @Bean(name = "ehCacheManager")
  48. //    @DependsOn("lifecycleBeanPostProcessor")
  49. //    public EhCacheManager ehCacheManager() {
  50. //        return new EhCacheManager();
  51.      * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
  52.     @Bean(name = "securityManager")
  53.     public DefaultWebSecurityManager securityManager() {
  54.         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  55.         securityManager.setRealm(shiroRealm());
  56. //        securityManager.setCacheManager(ehCacheManager());
  57.         return securityManager;
  58.      * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
  59.      * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
  60.     @Bean(name = "shiroFilter")
  61.     public ShiroFilterFactoryBean shiroFilterFactoryBean() {
  62.         ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  63.         //Shiro的核心安全接口,这个属性是必须的
  64.         shiroFilterFactoryBean.setSecurityManager(securityManager());
  65.         Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
  66.         LogoutFilter logoutFilter = new LogoutFilter();
  67.         logoutFilter.setRedirectUrl("/login");
  68.         shiroFilterFactoryBean.setFilters(filters);
  69. //anon:没有参数,表示可以匿名使用。例子:/admin/**=anon
  70. //authc:没有参数,表四需要认证(登录)才能使用。例子:/user/**=authc
  71. //roles:角色过滤器,判断当前用户是否拥有指定角色。例子:admins/**=roles[“admin,guest”]
  72.         Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>();
  73.         filterChainDefinitionManager.put("/logout", "logout");
  74.         filterChainDefinitionManager.put("/api/**", "authc");
  75.         filterChainDefinitionManager.put("/**", "anon");
  76.         shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
  77.         shiroFilterFactoryBean.setSuccessUrl("/");
  78.         shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  79.         return shiroFilterFactoryBean;
  80.      * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
  81.     @Bean
  82.     @ConditionalOnMissingBean
  83.     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
  84.         DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
  85.         defaultAAP.setProxyTargetClass(true);
  86.         return defaultAAP;
  87.      * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
  88.      * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
  89.     @Bean
  90.     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
  91.         AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
  92.         aASA.setSecurityManager(securityManager());
  93.         return aASA;

最后使用 shiro 验证登录,编写登录接口方法

  1.    @Autowired
  2.     private SysUserService userService;
  3.     @RequestMapping(value = "/admin/login", method = RequestMethod.POST)
  4.     public Result login(SysUser user) {
  5.         String username = user.getUsername();
  6.         Subject subject = SecurityUtils.getSubject();
  7.         UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, user.getPassword());
  8.             subject.login(usernamePasswordToken);
  9.             return Result.ok("登录成功").setData(usernamePasswordToken );
  10.         } catch (IncorrectCredentialsException e) {
  11.             return Result.fail("密码错误");
  12.         } catch (UnknownAccountException e) {
  13.             return Result.fail("账号不存在");

因为博客暂不需要注册功能,就后端直接生成用户名和密码吧,如果需要注册改成接口即可

  1.     public static void main(String[] args) {
  2.         SysUser user = new SysUser();
  3.         String username = "admin";
  4.         String password = "123456";
  5.         username = HtmlUtils.htmlEscape(username);
  6.         // 生成盐,默认长度 16 位
  7.         String salt = new SecureRandomNumberGenerator().nextBytes().toString();
  8.         // 设置 hash 算法迭代次数
  9.         int times = 2;
  10.         // 得到 hash 后的密码
  11.         String encodedPassword = new SimpleHash("md5", password, salt, times).toString();
  12.         // 存储用户信息,包括 salt 与 hash 后的密码
  13.         System.out.println("salt:" + salt);
  14.         System.out.println("password:"+ encodedPassword);

后端开发好了,然后就是前端了

首先是登录页代码,.vue页面分为三个模块,template是组件的模板结构页面元素,script是组件的 JavaScript 行为,style是组件的样式

  1. <template>
  2.     <div class="login-wrap">
  3.         <div class="ms-login">
  4.             <div class="ms-title">ltBlog-甜宝快更系统</div>
  5.             <el-form :model="param" :rules="rules" ref="login" label-width="0px" class="ms-content">
  6.                 <el-form-item prop="username">
  7.                     <el-input v-model="param.username" placeholder="用户名">
  8.                         <template #prepend>
  9.                             <el-button icon="el-icon-user"></el-button>
  10.                         </template>
  11.                     </el-input>
  12.                 </el-form-item>
  13.                 <el-form-item prop="password">
  14.                     <el-input type="password" placeholder="密码" v-model="param.password"
  15.                         @keyup.enter="submitForm()">
  16.                         <template #prepend>
  17.                             <el-button icon="el-icon-lock"></el-button>
  18.                         </template>
  19.                     </el-input>
  20.                 </el-form-item>
  21.                 <div class="login-btn">
  22.                     <el-button type="primary" @click="submitForm()">登录</el-button>
  23.                 </div>
  24.                 <p class="login-tips">Tips : 甜宝登陆后记得发文章呀。</p>
  25.             </el-form>
  26.         </div>
  27.     </div>
  28. </template>
  29. <script>
  30. import { ref, reactive,getCurrentInstance } from "vue";
  31. import { useStore } from "vuex";
  32. import { useRouter } from "vue-router";
  33. import { ElMessage } from "element-plus";
  34. export default {
  35.     setup() {
  36.         const router = useRouter();
  37.         const param = reactive({
  38.             username: "",
  39.             password: "",
  40.         const rules = {
  41.             username: [
  42.                     required: true,
  43.                     message: "请输入用户名",
  44.                     trigger: "blur",
  45.             password: [
  46.                 { required: true, message: "请输入密码", trigger: "blur" },
  47.         const login = ref(null);
  48.         const $http = getCurrentInstance()?.appContext.config.globalProperties.$http;
  49.         const submitForm = () => {
  50.             console.log(param);
  51.             login.value.validate((valid) => {
  52.                 if (valid) {
  53.                   $http({method:'post',url:'/admin/login',params: param}).then(data => {
  54.                     console.log(data)
  55.                     if (data.success === true) {
  56.                       ElMessage.success(data.msg);
  57.                       localStorage.setItem("ms_token", data.data);  //记住登入状态,将用户信息放到localStorage
  58.   localStorage.setItem("ms_username", username);
  59.                       router.push("/****"); //登入成功后跳转到后台首页
  60.                     } else {
  61.                       ElMessage.error(data.msg);
  62.                 } else {
  63.                     ElMessage.error("登录失败");
  64.                     return false;
  65.         const store = useStore();
  66.         store.commit("clearTags");
  67.         return {
  68.             param,
  69.             rules,
  70.             login,
  71.             submitForm,
  72. </script>
  73. <style scoped>
  74. .login-wrap {
  75.     position: relative;
  76.     width: 100%;
  77.     height: 100%;
  78.     background-image: url(src/assets/img/login-bg.jpg);
  79.     background-size: cover;
  80.     background-repeat: no-repeat;
  81.     background-position: center;
  82. .ms-title {
  83.     width: 100%;
  84.     line-height: 50px;
  85.     text-align: center;
  86.     font-size: 20px;
  87.     color: #fff;
  88.     border-bottom: 1px solid #ddd;
  89. .ms-login {
  90.     position: absolute;
  91.     left: 44%;
  92.     top: 50%;
  93.     width: 550px;
  94.     margin: -190px 0 0 -175px;
  95.     border-radius: 5px;
  96.     background: rgba(255, 255, 255, 0.3);
  97.     overflow: hidden;
  98. .ms-content {
  99.     padding: 30px 30px;
  100. .login-btn {
  101.     text-align: center;
  102. .login-btn button {
  103.     width: 100%;
  104.     height: 36px;
  105.     margin-bottom: 10px;
  106. .login-tips {
  107.     font-size: 12px;
  108.     line-height: 30px;
  109.     color: #fff;
  110. </style>

页面的样子

SpringBoot集成shiro+vue实现登录和权限控

登入成功的样子

SpringBoot集成shiro+vue实现登录和权限控

登入失败的样子

SpringBoot集成shiro+vue实现登录和权限控

就这样,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对象
  2. const user = {name: 'sugar', 'cnt': '22'};
  3. localStorage.setItem('userJson', JSON.stringify(user));
  4. // 字符串
  5. const str = "sugar";
  6. localStorage.setItem('userString', str);
  7. // Json对象
  8. var data1 = JSON.parse(localStorage.getItem('userJson'));
  9. // 字符串
  10. var data2 = localStorage.getItem('userString');
  11. // 删除一个
  12. localStorage.removeItem('userJson');
  13. // 删除所有
  14. 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重新登陆。

登入成功后还得有个退出登登入的功能

直接上代码

  1. <el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
  2.  if (command == "loginout") {
  3.               $http({method:'post',url:'/logout'}).then(data => {
  4.                 if (data.success === true) {
  5.                   ElMessage.success(data.msg);
  6.           localStorage.removeItem("ms_token"); 
  7.                   localStorage.removeItem("ms_username"); //去掉localStorage中的用户信息
  8.                   router.push("/login");
  9.                 } else {
  10.                   ElMessage.error(data.msg);
  1.     @RequestMapping(value = "/logout", method = RequestMethod.POST)
  2.     public Result logout() {
  3.         Subject subject = SecurityUtils.getSubject();
  4.         subject.logout(); //shiro提供的方法,该方法会清除 session、principals,并把 authenticated 设置为 false
  5.         return Result.ok("退出成功");

遗留问题,用户认证问题下篇解决。

关于菜单、按钮授权,菜单、角色管理,是个大工程,要搞比较久就先不做了,最主要的是对这个系统来说无用,个人博客,后台管理系统也就一个人用,一个账号所有权限都有就够了。等博客问世后面有时间再搞吧。

SpringBoot集成shiro+vue实现登录和权限控

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK