SpringBoot集成Shiro(超详细 -认证,动态授权,rememberMe)

雍志新
2023-12-01

SpringBoot集成Shiro(超详细)

本项目通过SpringBoot整合了shiro框架 使用MybatisPlus代码生成器生成简单的代码,项目包含shiro动态授权以及认证 ,rememberMe记住功能

一.项目准备

(1)导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.6.RELEASE</version>
		<relativePath/>
	</parent>
	<groupId>com.example</groupId>
	<artifactId>springboot-shiro</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-shiro</name>
	<description>Demo project for Spring Boot Shiro</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.46</version>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.1.0</version>
		</dependency>

		<!--代码生成器-->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-generator</artifactId>
			<version>3.1.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.velocity</groupId>
			<artifactId>velocity-engine-core</artifactId>
			<version>2.1</version>
		</dependency>

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>

		<!--shiro-->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>
       <!--热部署-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional>
		</dependency>
		<!--缓存-->
		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache-core</artifactId>
			<version>2.4.8</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>1.4.0</version>
		</dependency>
		<!-- fastjson阿里巴巴jSON处理器 -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.13</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
			<version>5.1.8.RELEASE</version>
		</dependency>


	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<!-- 配置 fork 进行热部署支持  -->
				<configuration>
					<fork>true</fork>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

(2)设置SpringBoot配置文件

server.port=80

spring.mvc.static-path-pattern=/static/**
spring.resources.static-locations=classpath:/static/

#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/crm?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mybatis-plus
mybatis-plus.type-aliases-package=com.example.leilei.entity

#热部署

#设置开启热部署
spring.devtools.restart.enabled=true
#页面不加载缓存,修改即时生效
spring.freemarker.cache=false

(3)设置SpringBoot启动类

@SpringBootApplication
@MapperScan(basePackages = "com.example.leilei.mapper")
public class SpringbootShiroApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringbootShiroApplication.class, args);
	}

}

(4)Mybatis-plus的代码生成器使用

合理使用工具可以帮我减少开发时间

public class CodeGenerator {

    public static void main(String[] args) throws InterruptedException {
        //用来获取Mybatis-Plus.properties文件的配置信息
        ResourceBundle rb = ResourceBundle.getBundle("springboot-shiro");
        AutoGenerator mpg = new AutoGenerator();
        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        gc.setOutputDir(rb.getString("OutputDir"));
        gc.setFileOverride(true);
        gc.setActiveRecord(true);// 开启 activeRecord 模式
        gc.setEnableCache(false);// XML 二级缓存
        gc.setBaseResultMap(true);// XML ResultMap
        gc.setBaseColumnList(false);// XML columList
        gc.setAuthor(rb.getString("author"));
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setDbType(DbType.MYSQL);
        dsc.setTypeConvert(new MySqlTypeConvert());
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername(rb.getString("jdbc.user"));
        dsc.setPassword(rb.getString("jdbc.pwd"));
        dsc.setUrl(rb.getString("jdbc.url"));
        mpg.setDataSource(dsc);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
       // strategy.setTablePrefix(new String[] { "t_" });// 此处可以修改为您的表前缀
        strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略
        strategy.setInclude(new String[]{"real_eseate"}); // 需要生成的表
        mpg.setStrategy(strategy);
        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent(rb.getString("parent"));
        pc.setController("controller");
        pc.setService("service");
        pc.setServiceImpl("service.impl");
        pc.setEntity("domain");
        pc.setMapper("mapper");
        mpg.setPackageInfo(pc);

        // 注入自定义配置,可以在 VM 中使用 cfg.abc 【可无】
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
            }
        };

        List<FileOutConfig> focList = new ArrayList<FileOutConfig>();

        // 调整 xml 生成目录演示
        focList.add(new FileOutConfig("/templates/mapper.xml.vm") {
            @Override
            public String outputFile(TableInfo tableInfo) {
                return rb.getString("OutputDirXml")+ "/cn/leilei/mapper/" + tableInfo.getEntityName() + "Mapper.xml";
            }
        });

        // 调整 query 生成目录
/*        focList.add(new FileOutConfig("/templates/query.java.vm") {
            @Override
            public String outputFile(TableInfo tableInfo) {
                return rb.getString("OutputDomainDir")+ "/cn/leilei/query/" + tableInfo.getEntityName() + "Query.java";
            }
        });*/

        // 调整 domain 生成目录
        focList.add(new FileOutConfig("/templates/entity.java.vm") {
            @Override
            public String outputFile(TableInfo tableInfo) {
                return rb.getString("OutputDomainDir")+ "/cn/leilei/domain/" + tableInfo.getEntityName() + ".java";
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 自定义模板配置,可以 copy 源码 mybatis-plus/src/main/resources/templates 下面内容修改,
        // 放置自己项目的 src/main/resources/templates 目录下, 默认名称一下可以不配置,也可以自定义模板名称
        TemplateConfig tc = new TemplateConfig();
        //tc.setController("/templates/controller.java.vm");
        // 如上任何一个模块如果设置 空 OR Null 将不生成该模块。
        tc.setEntity(null);
        tc.setXml(null);
        mpg.setTemplate(tc);

        // 执行生成
        mpg.execute();
    }


}

springboot-shiro.perproties

#输出路径
OutputDir=E://EEworkspac//springbootshiro//springboot-shiro//src//main//java
#query和domain的生成路径
OutputDomainDir=E://EEworkspac//springbootshiro//springboot-shiro//src//main//java


#作者
author=lei
#数据源
jdbc.user=root
jdbc.pwd=root
jdbc.url=jdbc:mysql:///crm?useUnicode=true&characterEncoding=utf8

#包配置
parent=cn.leilei
#xml的生成的resources目录
OutputDirXml=E://EEworkspac//springbootshiro//springboot-shiro//src//main//resources

二.后端代码编写

(1)配置shiro的生命周期

这个shiro生命周期的Bean一定要单独配置

/**
 * 这个shiro生命周期的Bean一定要单独配置
 */
@Configuration
public class ShiroLifecycleBeanPostProcessorConfig {

    /**
     * Shiro生命周期处理器
     *
     * @return
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

}

(2)编写Realm实现认证,授权

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private IEmployeeService employeeService;
    @Autowired
    private IPermissionService permissionService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        Employee employee = (Employee) principalCollection.getPrimaryPrincipal();
        Long empId = employee.getId();
        //根据员工id查询员工权限
        List<Permission> permissions = permissionService.getByEmpId(empId);
		//将查询出的权限交给shiro
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        for (Permission permission : permissions) {
            info.addStringPermission(permission.getSn());
        }
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        //根据用户名查询用户
        QueryWrapper<Employee> wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        Employee employee = employeeService.getOne(wrapper);
        if(employee==null){
            throw new UnknownAccountException(username);
        }
        //封装info对象
        return new SimpleAuthenticationInfo(employee,employee.getPassword(), ByteSource.Util.bytes(MD5Utils.SALT),getName());
    }
}

授权方法需要连表查询

根据elmplyee 查询到permission表

简单的写了下,但在实际开发中sql语句不要使用 *

    <select id="selectByEmpId" parameterType="long" resultType="com.example.leilei.entity.Permission">
        SELECT p.*
        from t_employee_role er
        left join t_role r on er.role_id = r.id
        left join t_role_permission rp on r.id = rp.role_id
        left join t_permission p on rp.permission_id = p.id
        where er.employee_id = #{empId}
    </select>

(3)shiro的配置类

目前项目中包含认证,授权,rememberMe

package com.example.leilei.config;

import com.example.leilei.entity.Permission;
import com.example.leilei.service.IEmployeeService;
import com.example.leilei.service.IPermissionService;
import com.example.leilei.shiro.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Configuration
@AutoConfigureAfter(ShiroLifecycleBeanPostProcessorConfig.class)//配置Bean加载的先后顺序
public class ShiroConfig {

    @Autowired
    private IPermissionService permissionService;

    /**
     * SecurityManager核心对象Bean
     * @return
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        //注入Realm
        securityManager.setRealm(myRealm());
        //注入记住我管理器
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * 凭证比较器-加密加盐加次数
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//加密算法
        hashedCredentialsMatcher.setHashIterations(10);//加密次数
        return hashedCredentialsMatcher;
    }
    /**
     * cookie对象;
     * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //System.out.println("ShiroConfiguration.rememberMeCookie()");
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
     * cookie管理对象;
     * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        //System.out.println("ShiroConfiguration.rememberMeManager()");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
        return cookieRememberMeManager;
    }

    /**
     * 自定义的Realm
     * @return
     */
    @Bean
    public MyRealm myRealm(){
        MyRealm myRealm = new MyRealm();
        //设置密码加密凭证,登录时会对密码进行加密匹配
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myRealm;
    }

    /**
     *过滤器配置 过滤所有权限
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置过滤器
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        filterChainDefinitionMap.put("/static/**", "anon");   //anon代表资源直接放行
        filterChainDefinitionMap.put("/logout", "logout");    //shiro的退出方法,会注销自己的认证

        //权限拦截,查出所有权限
        List<Permission> permissions = permissionService.list();
        for (Permission permission : permissions) {
            filterChainDefinitionMap.put(permission.getUrl(),"perms["+permission.getSn()+"]");
        }

        filterChainDefinitionMap.put("/**", "authc");

        //当访问需要认证才能访问的资源,如果没有认证,则跳转到这个资源
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //当访问需要授权才能访问的资源的时候,如果没有权限,则跳转到这个资源
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
}

(4)编写工具类

(1)Ajax

/**
 * 这个类用来返回ajax对象,并且私有化字段,只能通过构造方法赋值
 *
 */
public class AjaxResult {
    private Boolean success = true;
    private String msg;

    private AjaxResult() {
    }

    /**
     * 返回ajax
     * @return 没有错误就返回这个
     */
    public static AjaxResult success(){
        return new AjaxResult();
    }

    /**
     *  返回ajax
     * @param msg 错误信息
     * @return 有错误就返回这个
     */
    public static AjaxResult error(String msg){
        AjaxResult ajaxResult = success();;
        ajaxResult.setSuccess(false);
        ajaxResult.setMsg(msg);
        return ajaxResult;
    }

    public Boolean getSuccess() {
        return success;
    }

    private void setSuccess(Boolean success) {
        this.success = success;
    }

    public String getMsg() {
        return msg;
    }

    private void setMsg(String msg) {
        this.msg = msg;
    }
}

(2)Session域对象(登陆用户)存取值

public class SessionUtil {
    public static final String LOGINSESSION = "loginuser";

    //将登陆对象存入域对象之中
    public static void setSession(Employee employee){//将登陆用户存入域对象
        Subject subject = SecurityUtils.getSubject();//获取登陆对象
        subject.getSession().setAttribute(LOGINSESSION,employee);
    }

    //Session中获取当前登陆对象
    public static Employee getSession(){//获取登陆对象域对象信息
        Subject subject = SecurityUtils.getSubject();//获取登陆对象
        return (Employee) subject.getSession().getAttribute(LOGINSESSION);
    }
}

(3)MD5加密

public class MD5Utils {

    public static final String SALT = "fm";
    public static final int ITERATIONS = 10;

    /**
     * 加密
     * @param source
     * @return
     */
    public static String encrype(String source){
        SimpleHash simpleHash = new SimpleHash("MD5",source,SALT,ITERATIONS);
        return simpleHash.toString();
    }

    public static void main(String[] args) {

        System.out.println(encrype("admin"));

    }

}

(5)Controller层登陆注销方法

@Controller
public class LoginController {

    /**
     * 跳转到登录页面
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String login(){
    
        Subject subject = SecurityUtils.getSubject();
        //判断当前用户是否有使用rememberMe 
        if (subject.isRemembered()){
            return "main";
        //判断当前用户是否登录 
        }else if(subject.isAuthenticated()){
            return "main";
        }
        return "login";
    }

    /**
     * 登录请求
     * @param employee
     * @return
     */
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    @ResponseBody
    public AjaxResult login(@RequestBody Employee employee){
        //尝试获取获取用户信息
        Subject subject = SecurityUtils.getSubject();
        /*Boolean rememberMe = true;*/
        //将输入的账户密码封装为一个tocken对象
        UsernamePasswordToken token = new UsernamePasswordToken(employee.getUsername(),employee.getPassword(),employee.getRememberMe());
        try {
            //使用封装的tocken对象尝试通过shiro完成认证---->会调用自定义MyRealm类的AuthenticationInfo方法,尝试认证,
            if(!subject.isAuthenticated()){//判断当前用户是否登录 布尔值  取反
                subject.login(token);//认证登陆
            }
            //将登陆的用户信息存入域对象之中
            Employee logiuser = ((Employee) subject.getPrincipal());//获取当前用户
            SessionUtil.setSession(logiuser);
            return AjaxResult.success();
        }  catch (UnknownAccountException e) {
            e.printStackTrace();
            return AjaxResult.error("账户不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            return AjaxResult.error("密码错误");
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return AjaxResult.error("未知错误,检查后台");
        }
    }

    //退出登录
    @RequestMapping(value = "/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        return "redirect:/logout";
    }


}

三.基于VUE/ElementUI前端编写

前端所用的静态资源放在 resource/statc下

前端所用的页面放在 resource/templates下

(1)搭脚手架或者简单导入Elementui的js css

(2)登陆界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <base th:href="${#request.getContextPath()}+'/'">
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="static/plugins/vuejs/vue.min.js" type="text/javascript"></script>
    <link rel="stylesheet" href="static/plugins/elementui/lib/theme-chalk/index.css">
    <script src="static/plugins/elementui/lib/index.js"></script>
    <script src="static/plugins/vuejs/axios.js" type="text/javascript"></script>
    <style type="text/css">
        .login-container {
            /*box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.06), 0 1px 0px 0 rgba(0, 0, 0, 0.02);*/
            -webkit-border-radius: 5px;
            border-radius: 5px;
            -moz-border-radius: 5px;
            background-clip: padding-box;
            margin: 180px auto;
            width: 350px;
            padding: 35px 35px 15px 35px;
            background: #fff;
            border: 1px solid #eaeaea;
            box-shadow: 0 0 25px #cac6c6;
        }

        .title {
            margin: 0px auto 40px auto;
            text-align: center;
            color: #505458;
        }

        .rememberMe {
            margin: 0px 0px 35px 0px;
        }
    </style>
</head>
<body>

<div id="app">
    <el-form :model="loginUser" :rules="loginFormRules" ref="loginForm" label-position="left" label-width="0px"
             class="demo-ruleForm login-container">
        <h3 class="title">员工登录</h3>
        <el-form-item prop="username">
            <el-input type="text" v-model="loginUser.username" auto-complete="off" placeholder="账号"></el-input>
        </el-form-item>
        <el-form-item prop="password">
            <el-input type="password" v-model="loginUser.password" auto-complete="off" placeholder="密码"
                      @keyup.native.enter="handleLogin"></el-input>
        </el-form-item>
        <el-checkbox v-model="checked" class="rememberMe" name="rememberMe">记住密码</el-checkbox>
        <el-form-item style="width:100%;">
            <el-button type="primary" style="width:100%;" @click.native.prevent="handleLogin" :loading="logining">登录
            </el-button>
        </el-form-item>
    </el-form>
</div>
<script>
    new Vue({
        el: "#app",
        data: {
            logining: false,
            loginUser: {
                username: '',
                password: '',
                rememberMe:false
            },
            loginFormRules: {
                username: [
                    {required: true, message: '请输入账号', trigger: 'blur'},
                ],
                password: [
                    {required: true, message: '请输入密码', trigger: 'blur'},
                ]
            },
            checked: false,
        },
        methods: {
            handleLogin(ev) {
                var _this = this;
                this.$refs.loginForm.validate((valid) => {
                    if (valid) {
                        this.logining = true;
                        //发送登录请求
                        this.loginUser.rememberMe=this.checked
                        axios.post("login", this.loginUser)
                            .then((res) => {
                                let data = res.data;
                                if (data.success) {
                                    //登录成功
                                    this.logining = false;
                                    //跳转到首页
                                    location.href = "main";
                                } else {
                                    this.logining = false;
                                    this.$message.error(data.msg, "error");
                                }
                            })
                    } else {
                        this.$message.error('请完成用户名密码的输入');
                        return false;
                    }
                });
            }
        }
    });
</script>
</body>
</html>

其他页面按着编写就好 最终采用我的源码会完成动态授权以及认证以及rememberMe的功能实现

 类似资料: