SpringBoot整合Shiro(完整版)

写在前面: 从2018年末开始学习SpringBoot,也用SpringBoot写过一些项目。这里对学习Springboot的一些知识总结记录一下。若是你也在学习SpringBoot,能够关注我,一块儿学习,一块儿进步。css


以前写项目安全控件基本都是用的SpringSecurity,后来发现Shiro在实际开发中用的也挺多的。这里一块儿来看一下Shiro在Springboot中的基本用法吧!想了解SpringSecurity的请移步:SpringSecurity安全控件使用指南html

Shiro简介

一、简介

Apache Shiro 是 Java 的一个安全(权限)框架。 Shiro 能够很是容易的开发出足够好的应用,其不只能够用在 JavaSE 环境,也能够用在 JavaEE 环境。Shiro 能够完成:认证、受权、加密、会话管理、与Web 集成、缓存等。相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。java

二、shiro架构

在这里插入图片描述
Shiro三大功能模块mysql

  1. Subject:主体,通常指用户(把操做交给SecurityManager)。
  2. SecurityManager:安全管理器,管理全部Subject,能够配合内部安全组件(关联Realm)。
  3. Realms:用于进行权限信息的验证,shiro连接数据的桥梁。

细分功能git

  1. Authentication:身份认证/登陆,验证用户是否拥有相应的身份(帐号密码验证)。
  2. Authorization:受权,验证某个已认证的用户是否拥有某权限。
  3. Session Manager:会话管理,用户登陆后,用户信息保存在session会话中。
  4. Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  5. Web Support:Web支持,集成Web环境。
  6. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
  7. Concurrency:多线程并发验证,在一个线程中开启另外一个线程,能够把权限自动传播过去。
  8. Testing:测试支持;
  9. Run As:容许一个用户伪装为另外一个用户(若是他们容许)的身份进行访问。
  10. Remember Me:记住我,登陆后,下次再来的话不用登陆了。

数据库设计

一、表关系

在这里插入图片描述
菜单(TbMenu)=====> 页面上须要显示的全部菜单github

角色(SysRole)=====> 角色及角色对应的菜单web

用户(SysUser)=====> 用户及用户对应的角色redis

用户和角色中间表(sys_user_role)====> 用户和角色中间表
【注】 权限管理实现原理都差很少,虽然使用的安全控件不同,但数据库表的设计基本同样,这里数据库表的设计和SpringSecurity实现动态权限菜单控制基本一致。spring

二、数据库表结构

菜单表tb_menu
在这里插入图片描述
角色及菜单权限表sys_role,其中父节点parent 为null时为角色,不为null时为对应角色的菜单权限。
在这里插入图片描述
用户表sys_user。
在这里插入图片描述
用户和角色多对多关系,用户和角色中间表sys_user_role(由Spring-Data-Jpa自动生成)。
在这里插入图片描述sql

搭建项目

一、新建Springboot项目

建立一个SprintBoot项目,添加项目须要的依赖(这里持久层使用的是SpringDataJpa)。
Shiro依赖

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>

pom.xml完整依赖代码

<?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 https://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.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.mcy</groupId>
	<artifactId>springboot-shiro</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-shiro</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<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>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<!-- shiro与spring整合依赖 -->
		<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-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

二、项目结构

在这里插入图片描述

三、配置连接数据库属性

spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

server.port=80
server.servlet.context-path=/shiro

编写代码

一、编写实体类

菜单表实体类TbMenu,Spring-Data-Jpa能够根据实体类去数据库新建或更新对应的表结构,详情能够访问Spring-Data-Jpa入门

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 菜单表
 * @author
 */
@Entity
public class TbMenu {
    private Integer id;
    private String name;
    private String url;
    private Integer idx;
    @JsonIgnore
    private TbMenu parent;
    @JsonIgnore
    private List<TbMenu> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(unique = true)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }

    @ManyToOne
    @CreatedBy
    public TbMenu getParent() {
        return parent;
    }

    public void setParent(TbMenu parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    @OrderBy(value = "idx")
    public List<TbMenu> getChildren() {
        return children;
    }

    public void setChildren(List<TbMenu> children) {
        this.children = children;
    }
}

角色及权限表SysRole,parent 为null时为角色,不为null时为权限。

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
/***
 * 角色及角色对应的菜单权限
 * @author
 *parent 为null时为角色,不为null时为权限
 */
public class SysRole {
    private Integer id;
    private String name;    //名称
    @JsonIgnore
    private SysRole parent;
    private Integer idx;    //排序
    @JsonIgnore
    private List<SysRole> children = new ArrayList<>();

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToOne
    @CreatedBy
    public SysRole getParent() {
        return parent;
    }

    public void setParent(SysRole parent) {
        this.parent = parent;
    }

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    public List<SysRole> getChildren() {
        return children;
    }

    public void setChildren(List<SysRole> children) {
        this.children = children;
    }

    public Integer getIdx() {
        return idx;
    }

    public void setIdx(Integer idx) {
        this.idx = idx;
    }
}

最后实现的就是用户管理了,只须要对添加的用户分配对应的角色就能够了,用户登陆时,显示角色对应的权限。

import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 用户表
 */
@Entity
public class SysUser {
    private Integer id;
    private String username;    //帐号
    private String password;    //密码
    private String name;        //姓名
    @JsonIgnore
    private List<SysRole> roles = new ArrayList<>();    //角色

    @Id
    @GeneratedValue
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Column(length = 20, unique = true)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(length = 100)
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Column(length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @ManyToMany(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
    @JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
}

二、Shiro配置类

Shiro配置类ShiroConfig。

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @description Shiro配置类
 */
@Configuration
public class ShiroConfig {

    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次数
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean("userRealm")
    public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(matcher);
        return userRealm;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置 SecurityManager,安全管理器
        bean.setSecurityManager(securityManager);

        // 设置登陆跳转页面
        bean.setLoginUrl("/index");
        // 设置未受权提示页面(没有权限访问后的页面)
        bean.setUnauthorizedUrl("/unAuth");
        /**
         * Shiro内置过滤器,能够实现拦截器相关的拦截器
         *    经常使用的过滤器:
         *      anon:无需认证(登陆)能够访问
         *      authc:必须认证才能够访问
         *      user:若是使用rememberMe的功能能够直接访问
         *      perms:该资源必须获得资源权限才能够访问
         *      role:该资源必须获得角色权限才能够访问
         **/
        Map<String, String> filterMap = new LinkedHashMap<>();

        filterMap.put("/login","anon");
        filterMap.put("/user","authc");
        filterMap.put("/system","perms[system]");
        filterMap.put("/static/**","anon");

        filterMap.put("/**","authc");
        filterMap.put("/logout", "logout");

        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

    /**
     * 注入 securityManager
     */
    @Bean(name="securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联realm.
        securityManager.setRealm(userRealm(hashedCredentialsMatcher));
        return securityManager;
    }
}

自定义Realm用于查询用户的角色和权限信息并保存到权限管理器中。代码以下:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.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.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService userService;
    
    /**
     * 受权逻辑方法
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行受权");

        //获取当前登陆对象
        Subject subject = SecurityUtils.getSubject();
        SysUser user = (SysUser)subject.getPrincipal();
        if(user != null){
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 角色与权限字符串集合
            Collection<String> rolesCollection = new HashSet<>();

            //得到当前用户的角色
            List<SysRole> roles = user.getRoles();
            for(SysRole role : roles){
                rolesCollection.add(role.getName());
            }
            //添加当前用户的角色权限,用于判断能够访问那些功能
            info.addStringPermissions(rolesCollection);
            return info;
        }
        return null;
    }

    /**
     * 认证
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        System.out.println("执行认证");

        // 编写shiro判断逻辑,判断用户名和密码
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        // 判断用户名
        SysUser bean = userService.findByUsername(token.getUsername());

        //用户名不存在
        if(bean == null){
            throw new UnknownAccountException();
        }

        //以帐号做为加密的盐值
        ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getUsername());

        // 判断密码,
        return new SimpleAuthenticationInfo(bean, bean.getPassword(),
                credentialsSalt, getName());
    }
}

其中密码加密使用的是MD5加密,加密代码以下:

public static void main(String[] args){
    String hashAlgorithName = "MD5";
    //加密密码
    String password = "123456";
    //加密次数
    int hashIterations = 1024;
    //帐号做为加密的盐值
    ByteSource credentialsSalt = ByteSource.Util.bytes("admin");
    Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
    System.out.println(obj);
}

三、编写控制器

新建IndexController测试控制器。这里持久层框架使用的是SpringDataJpa,查询只需遵循查询方法命名规范便可,因此这里就直接写控制器代码了。想进一步了解SpringDataJpa的小伙伴,请移步:Spring-Data-Jpa入门。
IndexController控制器代码以下:

import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysRoleService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {

    @Autowired
    private SysRoleService roleService;

    @RequestMapping({"/index", "/"})
    public String index(){
        return "login";
    }

    @RequestMapping(value = "/login")
    public String login(String username, String password, Model model){
        //1.获取subject
        Subject subject = SecurityUtils.getSubject();

        //2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            //3.执行登陆方法
            subject.login(token);
            //登陆成功
            return "redirect:/mian";
        } catch (UnknownAccountException e) {
            //e.printStackTrace();
            //登陆失败:用户名不存在
            model.addAttribute("msg", "用户名不存在");
            return "login";
        } catch (IncorrectCredentialsException e) {
            //e.printStackTrace();
            //登陆失败:用户名不存在
            model.addAttribute("msg", "密码输入有误");
            return "login";
        }
    }

    @RequestMapping(value = "/mian")
    public String main(){
        SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
        System.out.println(user.getName());
        //登陆成功后,输出对应的角色和菜单
        for(SysRole role: user.getRoles()){
            System.out.println(role.getName()+"=====角色");
            for(SysRole roles : role.getChildren()){
                System.out.println(roles.getName()+"===="+role.getName()+"角色对应的菜单");
            }
        }
        return "main";
    }

    @RequestMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }
        return "redirect:/main";
    }

    @RequestMapping("/unAuth")
    public String unAuth(){
        return "unAuth";
    }

    @RequestMapping("/system")
    public String system(){
        return "system";
    }
    
    @RequestMapping("/user")
    public String user(){
        return "user";
    }
}

四、编写页面

页面内容都比较简单,主要为一个登陆页面,登陆成功后的页面和几个访问页面。
登陆页面login.html代码以下(测试页面写的比较简单,页面中font标签为登陆失败的提示信息,内容为后台传递过来的):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
    <style type="text/css">
        #main{
            width: 400px;
            margin: 100px auto;
        }
        input{
            border-radius: 5px;
            width: 200px;
            height: 20px;
        }
        button{
            margin-left: 30px;
            width: 80px;
            border-radius: 15px;
        }
    </style>
</head>
<body>
    <div id="main">
        <form action="login" method="post">
            <font color="red" th:text="${msg}"></font><br><br>
            帐号:<input type="text" name="username"><br><br>
            密码:<input type="password" name="password"><br><br>
            <button type="submit">登陆</button>
        </form>
    </div>
</body>
</html>

登陆成功main.html页面(几个页面跳转超连接)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <li><a href="user">登陆便可访问</a></li>
    <li><a href="system">管理员权限才能访问</a></li>
    <li><a href="logout">退出登陆</a></li> 
</body>
</html>

管理员权限才能访问的页面system.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    管理员权限,才能访问
</body>
</html>

登陆便可访问的页面user.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登陆后便可访问
</body>
</html>

没有权限访问的页面,提示页面unAuth.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>您暂时没有权限访问该页面</p>
</body>
</html>

测试

一、应用测试

运行项目后访问http://localhost/shiro/index,进入登陆页面,登陆拥有管理员权限的用户。
在这里插入图片描述
登陆成功后,能够从编辑器控制台中看出第一步进行了Shrio的认证。以后执行了登陆成功后跳转页面的后台方法,输出了对应的角色和角色对应的菜单。
【注】 此案例中前台菜单没有进行遍历,菜单,权限等功能也没有写对应的增删改查。直接在数据库中添加数据进行测试的。若是想了解菜单遍历,菜单权限等功能的实现,请访问SpringSecurity实现动态菜单加载。其中数据库设计,功能操做都是同样的。

点击“管理员权限才能访问”首先会进行执行受权,根据判断是否有管理员身份,肯定是否能够访问。由于是拥有管理员身份的用户登陆的,因此能够访问,效果以下:
在这里插入图片描述
正是system.html页面中的内容。若是没有管理员身份,则显示效果以下:
在这里插入图片描述

二、案例代码下载

案例代码下载地址:https://github.com/machaoyin/springboot-shrio

最后有什么不足之处,欢迎你们指出,期待与你的交流。