SpringBoot+Vue从零开始做网站6-集成shiro实现登录和权限控制

19 篇文章 3 订阅
12 篇文章 2 订阅

到上一篇已经把前后端的项目底子搭好了,今天开始做功能,首先就是后台管理系统登录功能。

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("退出成功");
    }

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

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

在博客中查看:从零开始做网站6-springboot集成shiro+vue实现登录和权限控制 - ZJBLOG 

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: springboot+vue+shiro是一种常见的Web应用程序开发架构,其中springboot作为后端框架,vue作为前端框架,shiro作为安全框架。使用这种架构可以快速开发出高效、安全的Web应用程序。其中,springboot提供了快速开发的能力,vue提供了现代化的前端交互体验,shiro提供了安全认证和授权的能力。这种架构已经被广泛应用于企业级Web应用程序的开发中。 ### 回答2: Spring Boot是一个开箱即用的框架,可用于快速开发基于Spring的Web应用程序,它简化了Spring应用程序的搭建和部署过程,可以帮助程序员避免一些开发中的繁琐细节。Vue.js是一个流行的JavaScript框架,可用于构建现代单页Web应用程序(SPA)。它提供了一个灵活和高效的数据绑定机制,优化了前端开发和用户体验。Shiro是一个Java的安全框架,提供了身份认证,授权,会话管理和加密等功能,可用于保护Web应用程序的安全性。 在一个现代Web应用程序中,身份验证和授权是非常重要的,Spring Boot结合Shiro可以帮助我们快速实现身份认证和授权功能,Shiro支持多种认证方式,包括基于表单的身份验证和基于JSON Web Token(JWT)认证。此外,Shiro还支持基于角色的授权和细粒度的访问控制Spring BootVue.js配合使用可以大大提高Web应用程序的交互性和响应性,Vue.js的虚拟DOM机制可以支持SPA的快速加载,减少页面切换延迟,用户可以实现无缝的操作。 另外,Spring Boot还可以支持异步编程和非阻塞I/O模型,在高并发场景下可以提高应用程序的性能和吞吐量。同时,Spring Boot还有丰富的插件生态系统,比如Mybatis和Hibernate等ORM框架,可以帮助程序员快速访问数据库。总的来说,Spring Boot结合Vue.js和Shiro可以大大提高应用程序的开发效率和可扩展性,同时保证Web应用程序的安全性和稳定性。 ### 回答3: SpringBoot是一款让开发者更容易使用Spring框架的工具,它提供了很多开箱即用的特性和自动化配置,可以帮助我们更快速地搭建和构建Java Web应用。Vue.js是一款流行的JavaScript框架,可以帮助我们构建交互性强、效果美观的前端应用。而Shiro是一款开源的安全框架,可以为我们的应用提供身份验证、授权、会话管理、加密等能力。 SpringBootVue.js的结合可以让我们更快速地构建现代化的Web应用,其中前端页面使用Vue.js实现,后端服务则可以使用SpringBoot提供。在进行权限控制时,可以采用Shiro框架进行身份验证和权限控制,这能够让我们的应用更安全可靠。Shiro提供的安全过滤器可以根据请求路径,判断当前用户是否有访问权限,以及需要使用哪种授权方式。同时,Shiro还提供了很多默认的安全模块,如密码加密、会话管理等,这些模块可以为我们的应用提供更加全面的安全保护。 总体来说,SpringBootVue.js和Shiro是一组非常强大的开发工具,通过它们的结合,我们可以更快速地构建高效、安全、现代化的Web应用。在实际开发中,我们可以充分挖掘这些工具的优势,同时也要时刻关注它们的发展和变化,以便更好地应对新的需求和挑战。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值