Spring Boot——集成Shiro框架

1,447 阅读15分钟

1、Shiro简介

1.1、什么是Shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理,Web集成,缓存等。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

Shiro官网

Shiro官方文档

1.2、shiro的功能

Shiro 的 API 也是非常简单;其基本功能点如下图所示:

在这里插入图片描述

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

==注:Shiro 不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。==

1.3、Shiro外部框架

Shiro外部框架具有非常简单易于使用的 API,且 API 契约明确。下图从应用程序角度展示了如何使用Shiro完成工作:

在这里插入图片描述

可以看到:应用代码直接交互的对象是 Subject,==也就是说 Shiro 的对外 API 核心就是 Subject==;其每个 API 的含义:

  • Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

  • SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出==SecurityManager是 Shiro 的核心==,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

  • Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

  • 也就是说对于我们而言,最简单的一个 Shiro 应用:

    1. 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;

    2. 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。

1.4、Shiro内部框架

Shiro内部框架是一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。

在这里插入图片描述

  • Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
  • SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;==所以我们一般在应用中都需要实现自己的 Realm;==
  • SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
  • SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
  • Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。

2、Shiro的快速开始

2.1、环境搭建

新建一个maven项目,导入依赖:

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.7.1</version>
    </dependency>

    <!-- configure logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>1.7.26</version>
        
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.26</version>

    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.12</version>
    </dependency>
</dependencies>

log4j.properties:

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

Shiro配置文件shiro.ini:

[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

2.2、测试类

测试类里面测试了一些API方法,主要是Subject的使用。

import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);

    public static void main(String[] args) {


        /*  设置环境:
        * 使用配置创建 Shiro SecurityManager 的最简单方法:
        * 通过ini文件(shiro.ini)加载领域,用户,角色,和权限配置
        * IniSecurityManagerFactory通过读取.ini配置文件,返回
        */
        // Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        // SecurityManager securityManager = factory.getInstance();
        SecurityManager securityManager = new IniSecurityManagerFactory("classpath:shiro.ini").getInstance();

        // 对于这个简单的示例快速入门,使 SecurityManager 可作为 JVM 单例访问。
        // 大多数应用程序不会这样做,而是依赖于它们的容器配置或 web.xml 的 webapps。
        // 这超出了这个简单快速入门的范围,所以我们只会做最起码的事情,这样你就可以继续感受事物。
        SecurityUtils.setSecurityManager(securityManager);


        // 现在已经设置了一个简单的 Shiro 环境,让我们看看可以做什么:
        // 获取当前正在执行的用户Subject:
        Subject currentUser = SecurityUtils.getSubject();

        // 通过当前用户拿到session(shiro里的session)
        Session session = currentUser.getSession();
        // session存值和取值
        session.setAttribute("someKey", "万里顾一程");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("万里顾一程")) {
            log.info("Subject--》session [" + value + "]");//Subject--》session [万里顾一程]
        }

        // 判断当前用户是否认证
        if (!currentUser.isAuthenticated()) {

            //验证用户的用户名和密码(和.ini配置文件里的用户信息对比),如果认证成功就给用户生成一个token(令牌)
            UsernamePasswordToken token = new UsernamePasswordToken("root", "secret");
            token.setRememberMe(true);//开启记住我功能

            try {
                currentUser.login(token);//执行登录操作
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // 总的异常
            catch (AuthenticationException ae) {
            }
        }
        // 打印当前用户名
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //  测试用户的角色
        if (currentUser.hasRole("admin")) {//如果当前用户有admin这个角色
            log.info("May the admin be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        // 测试角色权限(非实例级),粗粒度
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        // 一个(非常强大的)实例级权限,细粒度
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        // 注销
        currentUser.logout();

        // 结束
        System.exit(0);
    }
}

启动测试类测试,查看日志输出:

在这里插入图片描述

上面只是一个简单的控制台输出的测试,下面我们用Springboot来集成shiro。

获取测试代码

3、Springboot集成Shiro

3.1、搭建环境

新建一个springboot项目,导入依赖:

<dependencies>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--导入tomcat解析jsp的依赖-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.7.1</version>
        </dependency>
</dependencies>

自定义Realm

package com.cheng.shiro.realm;

import jdk.nashorn.internal.ir.CallNode;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {     
        return null;
    }
}

编写Shiro配置类

package com.cheng.config;

import com.cheng.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
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.HashMap;


@Configuration
public class ShiroConfig {

    //1.创建ShiroFilter,拦截所有的请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
      
        return bean;
    }

    //2.创建SecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(realm);

        return securityManager;
    }
    //3.创建自定义的Realm
    @Bean(name = "realm")
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }
}

3.2、实现登录拦截

登录页面

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<h1>登录页面</h1>
<form action="" method="post">
    用户名<input type="text" name="username"><br>
    密码<input type="text" name="password"><br>
    <input type="submit" value="登录">

</form>

</body>
</html>

用户主页

<%--解决乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<h1 style="color: red">用户主页v1.0</h1>
    <ul>
        <li><a href="">用户管理</a></li>
        <li><a href="">商品管理</a></li>
        <li><a href="">订单管理</a></li>
        <li><a href="">职工管理</a></li>
    </ul>

</body>
</html>

controller

@Controller
@RequestMapping("/user")
public class UserController {
}

我们要实现过滤功能,就在配置类添加相应的过滤器即可。

package com.cheng.config;

import com.cheng.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
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.HashMap;

@Configuration
public class ShiroConfig {

    //1.创建ShiroFilter,拦截所有的请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
      
        bean.setSecurityManager(securityManager);

        /*添加过滤器
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须拥有 记住我 功能才能使用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色权限才能访问
         * */
        //创建一个map,map中决定哪些资源是受限的,哪些资源是公共的
        HashMap<String, String> map = new HashMap<String, String>();

        //访问index.jsp需要认证
        map.put("/index.jsp","authc");

        //将拦截的请求放入过滤器
        bean.setFilterChainDefinitionMap(map);

        //默认认证的界面路径,当登录失败时自动跳转到该页面进行认证
        bean.setLoginUrl("/login.jsp");

        return bean;
    }

    //2.创建SecurityManager
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(realm);

        return securityManager;
    }
    //3.创建自定义的Realm
    @Bean(name = "realm")
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }
}

启动程序测试,当我们尝试访问主页时,跳转到登录界面,拦截成功!

3.3、实现用户认证和退出

用户认证功能需要使用我们自定义的Realm

首先用户在登录界面提交用户信息,再用Controller接收。

Controller接收

    @RequestMapping("/login")
    public String login(String username,String password){

        Subject subject = SecurityUtils.getSubject();

        //封装前端提交的用户信息
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);//执行登录方法,执行成功就跳转主页面
            return "redirect:/index.jsp";
            //捕获可能出现的异常
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "redirect:/login.jsp";
    }

登录界面

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
    用户名<input type="text" name="username"><br>
    密码<input type="text" name="password"><br>
    <input type="submit" value="登录">

</form>
</body>
</html>

接收到用户提交的信息后,封装成token,然后把token和自定义的Realm中保存的用户信息进行比对,比对成功就进行登录操作,比对失败就抛出异常。

CustomerRealm.java

package com.cheng.shiro.realm;

import jdk.nashorn.internal.ir.CallNode;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("====进入了认证方法====");

        //获得身份信息
        String principal = (String) token.getPrincipal();
        //虚拟一个用户数据用来测试,
        if ("wanli".equals(principal)){//如果身份信息认证成功
            return new SimpleAuthenticationInfo(principal,"123",this.getName());
        }

        return null;
    }
}

启动程序进行测试:

访问登录界面,输入错误的用户名,测试用户名认证功能

在这里插入图片描述

然后输入错误的密码,测试密码认证功能

在这里插入图片描述

最后输入正确的用户名和密码,进行登录,成功进入首页!

用户注销

在主页添加

<a href="${pageContext.request.contextPath}/user/logout">注销登录</a>

然后编写controller

@RequestMapping("/logout")
public String logout(){
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/login.jsp";
}

3.4、连接数据库实现基于MD5盐值加密的注册功能

为了防止用户密码被获取,在用户注册时,我们就应该将用户提交的明文密码在保存到数据库之前,使用shiro为我们提供的MD5算法对明文密码进行加密,并且加盐处理之后,我们才能保存到数据库,日后在认证的时候,我们才能在数据库中读取加密的密码。

连接数据库

创建一个shiro数据库,然后建一张user表

CREATE DATABASE `shiro`CHARACTER SET utf8 COLLATE utf8_general_ci; 

CREATE TABLE `shiro`.`t_user
( `id` INT(6) NOT NULL AUTO_INCREMENT, 
`username` VARCHAR(60), `password` VARCHAR(60), 
`salt` VARCHAR(30), 
 PRIMARY KEY (`id`) 
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci; 

导入依赖

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<!--导入mysql的依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
</dependency>

编写数据库配置文件

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=19990802

mybatis.type-aliases-package=pojo
mybatis.mapper-locations=classpath:mapper/*.xml

编写生成随机盐的工具类

package com.cheng.utils;

import java.util.Random;

//生成随机盐的工具类
public class SaltUtils {
    public static String getSalt(int n){
        //定义一个数组,随机盐从这里生成
        char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-=+.?".toCharArray();

        /*StringBuffer与StringBuilder之间区别
        * StringBuffer 字符串变量(线程安全)多线程操作字符串
        * StringBuilder 字符串变量(非线程安 单线程操作字符串
        * */
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            //每次都在在chars范围内随机返回一个数字,执行n次,Random的区间是[a,b)
            char aChar = chars[new Random().nextInt(chars.length)];
            sb.append(aChar);
        }
        return sb.toString();
    }
    //测试一下
    public static void main(String[] args) {
        System.out.println(getSalt(6));//.8mv+#
    }
}

实现注册业务

dao层接口

@Mapper
@Repository
public interface UserDao {

    //用户注册
   public void save(User user);

}

service层接口

public interface UserService {
   public void register(User user);
}

service层实现类,实现具体的业务

package com.cheng.service;

import com.cheng.dao.UserDao;
import com.cheng.pojo.User;
import com.cheng.utils.SaltUtils;
import lombok.experimental.Accessors;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional//声明式事务管理,开启正常提交事务,异常回滚事务
public class UserServiceImpl implements UserService{

    //service调用dao层
    @Autowired
    private UserDao userDao;

    @Override
    public void register(User user) {
        //处理业务
        //1.生成随机盐 8位
        String salt = SaltUtils.getSalt(8);
        //2.将随机盐保存到数据库
        user.setSalt(salt);
        //3.将明文密码进行MD5 + salt随机盐 + hash散列加密, 散列次数1024
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
        //将加密后的密码保存到数据库
        user.setPassword(md5Hash.toHex());//toHex()字符串转换为十六进制编码
        
        userDao.save(user);
    }
}

启动程序,实现用户注册,用户信息保存到数据库中,注册成功!

在这里插入图片描述

3.5、连接数据库实现基于MD5盐值加密的认证功能

实现认证功能,需要通过用户提交的用户名查询数据库中是否有这个用户,所以我们需要写一个根据用户名查询用户的方法:

dao层

//根据用户名查询用户
User queryUserByName(String username);

service层

//根据用户名查询用户
User queryUserByName(String username);

service层实现类

@Override
public User queryUserByName(String username) {
    return userDao.queryUserByName(username);
}

因为我们在上面对明文密码进行了MD5盐值加密,当shrio进行密码匹配时会非对称匹配,所以我们要在配置类中修改密码校验匹配器

//3.创建自定义的Realm
@Bean(name = "realm")
public CustomerRealm getRealm(){
    CustomerRealm customerRealm = new CustomerRealm();
    //修改密码凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    //设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    //设置散列次数
    credentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(credentialsMatcher);
    return customerRealm;
}

在自定义的realm中进行用户认证的实现

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

        //获得身份信息
        String principal = (String) token.getPrincipal();

        User user = userService.queryUserByName(principal);

        if (!ObjectUtils.isEmpty(user)){//ByteSource提供了一个内部方法,可以将字符串转换为对应的盐值信息
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
        }

        return null;
    }

启动程序测试,认证成功!

3.6、授权的基本使用

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。

在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role):

  • 主体,即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。
  • 资源, 在应用中用户可以访问的URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。
  • 权限,代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro 不会去做这件事情,而是由实现人员提供。
  • 角色,角色代表了操作集合,可以理解为权限的集合,一般情况下我们赋予用户的是角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。不同的角色拥有一组不同的权限。

shiro授权流程分析

在这里插入图片描述

流程如下:

  1. 首先调用 Subject.isPermitted / hasRole接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
  2. Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:update”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
  3. 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
  4. Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。

授权方式

Shiro 支持三种方式的授权:

1.编程式:通过写 if/else 授权代码块完成:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
    //有权限
} else {
    //无权限
}

2.注解式:通过在执行的 Java 方法上放置相应的注解完成:

@RequiresRoles("admin")
public void hello() {
    //有权限
}

没有权限将抛出相应的异常;

3.JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:

<shiro:hasRole name="admin">
<!— 有权限 —>
</shiro:hasRole>

3.6.1、使用Jsp标签实现授权

在页面头部引入shiro标签

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

1.基于角色的权限管理

先给用户添加一个角色:

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //获取用户认证时的主身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();
    //根据主身份信息获得角色 和 权限信息
    if ("xiaowei".equals(primaryPrincipal)){

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //给用户添加角色
        simpleAuthorizationInfo.addRole("user");

        return simpleAuthorizationInfo;
    }
    return null;
}

然后在jsp页面定义角色授权规则

<ul>
    <shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
        <li><a href="">用户管理</a></li>
    </shiro:hasAnyRoles>
    <shiro:hasRole name="admin"><%--里面的资源只有admin角色才能访问--%>
    <li><a href="">商品管理</a></li>
    <li><a href="">订单管理</a></li>
    <li><a href="">职工管理</a></li>
    </shiro:hasRole>
</ul>

我们给当前用户赋予了一个user角色,启动程序访问主页面:

在这里插入图片描述

因为当前用户角色是user,只有访问用户管理的权限。

下面我们将当前用户的角色修改为admin,再启动程序访问主页面:

在这里插入图片描述

2.基于权限字符串的授权管理

权限字符串编写规则:“资源标识符:操作:对象实例 ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例。

给当前用户授予权限:

//权限字符串书写规则   user:*:* user表示模块,第一个*表示所有操作,第二个*表示所有资源
simpleAuthorizationInfo.addStringPermission("user:*:*");

然后在jsp页面定义权限字符串授权规则:

<li><a href="">用户管理</a>
  <ul>
      <%--下面资源需要对应的权限才能访问--%>
      <shiro:hasPermission name="user:add:*">
          <li><a href="">添加</a></li>
      </shiro:hasPermission>

      <shiro:hasPermission name="user:delete:*">
          <li><a href="">删除</a></li>
      </shiro:hasPermission>

      <shiro:hasPermission name="user:update:*">
          <li><a href="">修改</a></li>
      </shiro:hasPermission>

      <shiro:hasPermission name="user:find:*">
          <li><a href="">查询</a></li>
      </shiro:hasPermission>
  </ul>
</li>

==注:在页面中对权限字符串的控制,shiro不提供像shiro:hasAnyRoles这样的多个控制,因为权限字符串可以写通配符。==

当前用户为user,权限为user:*:*,启动程序访问主页:

在这里插入图片描述

下面我们修改用户的权限为user:add:*user:delete:*

simpleAuthorizationInfo.addStringPermission("user:add:*");
simpleAuthorizationInfo.addStringPermission("user:delete:*");

再启动程序访问主页:此时用户只对添加和删除操作有访问权限

在这里插入图片描述

3.Shiro 对权限字符串缺失部分的处理

  • 如“user:view”等价于“user:view:”;而“organization”等价于“organization:”或者“organization::”。可以这么理解,这种方式实现了前缀匹配。

  • 另外如“``user:”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user::1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即可以匹配所有,不加可以进行前缀匹配;但是如“:view”不能匹配“system:user:view”,需要使用“::view`”,即后缀匹配必须指定前缀(多个冒号就需要多个来匹配)。

3.6.2、使用编程式实现授权

编程式:通过写 if/else 授权代码块完成:

1.基于角色的权限管理

给当前用户赋予admin角色,然后在代码里面判断是否有该角色

@RequestMapping("save")
public String saveOrder(){
    //获得当前主体对象
    Subject subject = SecurityUtils.getSubject();
    //如果当前主体对象有admin角色
    if (subject.hasRole("admin")){
        //处理业务
        System.out.println("保存订单成功");
    }else{
        System.out.println("无权访问");
    }
    return "redirect:/index.jsp";
}

2.基于权限字符串的授权管理

给当前用户赋予admin角色,并赋予user:update:*权限,编写一个修改用户的controller来进行测试

@RequestMapping("save")
public String addUser(){
    //获得当前主体对象
    Subject subject = SecurityUtils.getSubject();

    //判断当前主体对象是否有user:update:*权限
    if (subject.isPermitted("user:update:*")){
        //处理业务
        return "redirect:/update.jsp";
    }else{
        return "redirect:/index.jsp";
    }

}

编写修改用户的jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>

<h1>修改用户</h1>

</body>
</html>

在首页面控制跳转

<shiro:hasPermission name="user:update:*">
    <li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
</shiro:hasPermission>

启动程序测试,因为当前用户有user:update:*权限,所以我们点击修改链接时,成功跳转!

在这里插入图片描述

shiro也提供了需要多个权限才能访问一个资源的方法isPermittedAll;

subject.isPermittedAll("user:delete:*","user:update:*")

3.6.3、使用注解式实现授权

通过在执行的 Java 方法上放置相应的注解完成

1.基于角色的授权管理

给当前用户赋予admin角色

simpleAuthorizationInfo.addRole("admin");

controller:

@RequestMapping("/update")
@RequiresRoles("admin")//判断当前用户是否有该角色,如果有才能执行下面操作
public String updateUser(){
     //处理业务
     return "redirect:/update.jsp";

我们再写一个controller,这个方法需要有user角色才能执行

@RequestMapping("/delete")
@RequiresRoles("user")//如果当前用户没有该角色,则会报异常
public String deleteUser(){
    //处理业务
    return "redirect:/delete.jsp";
}

启动程序进行测试,访问该请求:不能访问!

在这里插入图片描述

@RequiresRoles注解还可以指定多个role,如

@RequiresRoles(value={"user","admin"})//必须拥有全部角色才能访问

2.基于权限字符串的权限管理

给当前用户赋予admin角色,并赋予user:update:*权限,编写一个修改用户的controller来进行测试

simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:update:*");

controller

   @RequestMapping("/update")
   @RequiresPermissions("user:update:01")//拥有该权限才能执行下面操作,
    public String updateUser(){
            //处理业务
            return "redirect:/update.jsp";
    }

@RequiresPermissions的使用与@RequiresRoles注解类似

3.7、授权数据持久化

我们上面的授权数据都是写死了都,但在正常开发环境下,我们的授权数据肯定是来源于数据库,并且是持久化的。下面我们就把授权数据持久化到数据库中。

权限模型的库表关系:

3.7.1、搭建环境

根据上面的库表关系来构建对应的库表结构

先给上面的user表注册两个用户

用户名:xiaowei 密码:123,id为1

用户名:xiaopeng 密码:123,id为2

建角色表t_role

CREATE TABLE `shiro`.`t_role`( 
    `id` INT(6) NOT NULL AUTO_INCREMENT, 
    `name` VARCHAR(60), 
    PRIMARY KEY (`id`) 
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci; 

t_role实体类Role

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)//开启链式编程
public class Role {
    private int id;
    private String name;
}

插入数据

 INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('1', 'admin');
 INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('2', 'user'); 
 INSERT INTO `shiro`.`t_role` (`id`, `name`) VALUES ('3', 'product'); 

建权限表t_perms

CREATE TABLE `shiro`.`t_perms`( 
    `id` INT(6) NOT NULL AUTO_INCREMENT, 
    `name` VARCHAR(60), 
    `url` VARCHAR(255), 
    PRIMARY KEY (`id`) 
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci; 

t_perms实体类perms

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)//开启链式编程
public class Perms {
    private int id;
    private String name;
    private String url;
}

插入数据

INSERT INTO `shiro`.`t_perms` (`id`, `name`, `url`) VALUES ('1', 'user:*:*', 'user/*'); 
INSERT INTO `shiro`.`t_perms` (`id`, `name`, `url`) VALUES ('2', 'product:*:*', 'product/*'); 

建用户角色表t_user_role

CREATE TABLE `shiro`.`t_user_role`( 
    `id` INT(6) NOT NULL, 
    `userid` INT(6), 
    `roleid` INT(6), 
    PRIMARY KEY (`id`) 
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci; 

插入数据

INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('1', '1', '1'); 
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('2', '1', '2'); 
INSERT INTO `shiro`.`t_user_role` (`id`, `userid`, `roleid`) VALUES ('3', '2', '2'); 

一号用户xiaowei角色是admin和user

二号用户xiaopeng角色是user

建角色权限表t_role_perms

CREATE TABLE `shiro`.`t_role_perms`( 
    `id` INT(6), 
    `roleid` INT(6), 
    `permsid` INT(6) 
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci; 

插入数据

INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('1', '1', '1');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('2', '1', '2');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('3', '2', '1');
INSERT INTO `shiro`.`t_role_perms` (`id`, `roleid`, `permsid`) VALUES ('4', '3', '2');

一号角色admin有user:*:*product:*:*权限

二号角色user有user:*:*权限

三号角色product有product:*:*权限

3.7.2、角色信息在数据库中的获取

因为用户和角色是一对多关系,再给User实体类增加一个属性:

//定义角色的的集合
private List<Role> roles;

定义资源的角色授权规则:

<ul>
    <shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
        <li><a href="">用户管理</a>
          <ul>
              <%--下面资源需要对应的权限才能访问--%>
              <shiro:hasPermission name="user:add:*">
                  <li><a href="">添加</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:delete:*">
                  <li><a href="${pageContext.request.contextPath}/user/delete">删除</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:update:*">
                  <li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:find:*">
                  <li><a href="${pageContext.request.contextPath}/user/find">查询</a></li>
              </shiro:hasPermission>
          </ul>
        </li>
    </shiro:hasAnyRoles>
    <shiro:hasRole name="admin"><%--里面的资源需要admin角色才能访问--%>
    <li><a href="">商品管理</a></li>
    <li><a href="">订单管理</a></li>
    <li><a href="">职工管理</a></li>
    </shiro:hasRole>
</ul>

根据用户名查询数据库中用户对应的角色:

dao层

//根据用户名查询角色
User queryRoleByName(String username);

UserDao.xml

<resultMap id="userMap" type="User">
    <result column="uid" property="id"></result>
    <result column="username" property="username"></result>
    <!--角色信息-->
    <collection property="roles" ofType="Role" javaType="list">
        <result column="ird" property="id"/>
        <result column="rname" property="name"/>
    </collection>
</resultMap>

<select id="queryRoleByName" resultMap="userMap" parameterType="String">
    select tu.id uid,tu.username,tr.id rid,tr.name rname
    from shiro.t_user tu left join shiro.t_user_role tur on tu.id=tur.userid
    left join shiro.t_role tr on tr.id=tur.roleid
    where username=#{username}
</select>

service层

//根据用户名查询角色
User queryRoleByName(String username);

service层实现类

@Override
public User queryRoleByName(String username) {
    return userDao.queryRoleByName(username);
}

在自定义的Realm中实现从数据库获取用户角色,并进行认证

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //获取用户认证时的主身份信息
    String primaryPrincipal = (String) principals.getPrimaryPrincipal();

    User user = userService.queryRoleByName(primaryPrincipal);
    List<Role> roles = user.getRoles();
    //如果角色信息不为空
    if (!CollectionUtils.isEmpty(roles)){
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        roles.forEach(role->{     //遍历出所有的role
            simpleAuthorizationInfo.addRole(role.getName());
        });
        return simpleAuthorizationInfo;
    }
    return null;
}

启动程序,先登录用户xiaowei,角色是admin和user

在这里插入图片描述

再登录用户xiaopeng,角色是user

在这里插入图片描述

3.7.3、权限字符串在数据库中的获取

因为用户和角色是一对多关系,再给Role实体类增加一个属性:

//定义权限的集合
private List<Perms> perms;

定义资源的权限字符串授权规则:

<ul>
    <shiro:hasAnyRoles name="user,admin"><%--里面的资源可以被多个角色访问--%>
        <li><a href="">用户管理</a>
          <ul>
              <%--下面资源需要对应的权限才能访问--%>
              <shiro:hasPermission name="user:add:*">
                  <li><a href="">添加</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:delete:*">
                  <li><a href="${pageContext.request.contextPath}/user/delete">删除</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:update:*">
                  <li><a href="${pageContext.request.contextPath}/user/update">修改</a></li>
              </shiro:hasPermission>

              <shiro:hasPermission name="user:find:*">
                  <li><a href="${pageContext.request.contextPath}/user/find">查询</a></li>
              </shiro:hasPermission>
          </ul>
        </li>
    </shiro:hasAnyRoles>
    <shiro:hasRole name="admin"><%--里面的资源需要admin角色才能访问--%>
    <li><a href="">商品管理</a>
        <ul>
                <%--下面资源需要对应的权限才能访问--%>
            <shiro:hasPermission name="product:add:*">
                <li><a href="">添加</a></li>
            </shiro:hasPermission>

            <shiro:hasPermission name="product:delete:*">
                <li><a href="${pageContext.request.contextPath}/product/delete">删除</a></li>
            </shiro:hasPermission>

            <shiro:hasPermission name="product:update:*">
                <li><a href="${pageContext.request.contextPath}/product/update">修改</a></li>
            </shiro:hasPermission>

            <shiro:hasPermission name="product:find:*">
                <li><a href="${pageContext.request.contextPath}/product/find">查询</a></li>
            </shiro:hasPermission>
        </ul>
    </li>

    <li><a href="">订单管理</a></li>
    <li><a href="">职工管理</a></li>
    </shiro:hasRole>
</ul>

根据用户角色查询权限信息:

dao层

//根据角色id查询权限信息
List<Perms> findAllPermsByRoleId(int id);

dao层的xml文件

<select id="findAllPermsByRoleId" resultType="Perms"  parameterType="int">
    select tp.id,tp.name,tp.url,tr.name tname from t_role tr
    left join t_role_perms trp on tr.id=trp.roleid
    left join t_perms tp on trp.permsid = tp.id
    where tr.id=#{id}
</select>

service层

//根据角色id查询权限信息
List<Perms> findAllPermsByRoleId(int id);

service实现类

@Override
public List<Perms> findAllPermsByRoleId(int id) {
    return userDao.findAllPermsByRoleId(id);
}

自定义的realm

 //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取用户认证时的主身份信息
        String primaryPrincipal = (String) principals.getPrimaryPrincipal();
        System.out.println("primaryPrincipal-->"+primaryPrincipal);

        User user = userService.queryRoleByName(primaryPrincipal);
        System.out.println(user);
        List<Role> roles = user.getRoles();
        //如果角色信息不为空
        if (!CollectionUtils.isEmpty(roles)){
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            roles.forEach(role -> {//遍历出所有的role
                simpleAuthorizationInfo.addRole(role.getName());//把角色信息放进Authorizer进行比对

                //获取权限信息
                List<Perms> perms = userService.findAllPermsByRoleId(role.getId());

                System.out.println(perms);

                if (!CollectionUtils.isEmpty(perms)&& perms.get(0)!=null ){
                    perms.forEach(perm -> {
                        simpleAuthorizationInfo.addStringPermission(perm.getName());

                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }

启动程序测试:

先登录用户xiaowei,角色为admin和user,admin权限为user/*/*product/*/*,user为user/*/*

在这里插入图片描述

再登录用户xiaopeng,角色为user,权限为user/*/*

在这里插入图片描述

打这里,我们就完成了权限数据持久化的功能!

3.8、整合springboot缓存

从shiro的内部框架图可以很清晰地看到,CacheManager也是Shiro架构中的主要组件之一,Shiro正是通过``CacheManager`组件实现权限数据缓存。 当权限信息存放在数据库中时,对于每次前端的访问请求都需要进行一次数据库查询。特别是在大量使用shiro的jsp标签的场景下,对应前端的一个页面访问请求会同时出现很多的权限查询操作,这对于权限信息变化不是很频繁的场景,每次前端页面访问都进行大量的权限数据库查询是非常不经济的。因此,非常有必要对权限数据使用缓存方案。

Shiro只提供了一个可以支持具体缓存实现(如:Hazelcast, Ehcache, OSCache, Terracotta, Coherence, GigaSpaces, JBossCache等)的抽象API接口,这样就允许Shiro用户根据自己的需求灵活地选择具体的CacheManager。

缓存简单流程:

在这里插入图片描述

3.8.1、shiro中使用Ehcache实现缓存

导入依赖

<!--shiro和ehcache整合的依赖-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.3</version>
</dependency>

在shiro配置(shiroConfig)里面设置缓存管理器

//3.创建自定义的Realm
@Bean(name = "realm")
public Realm getRealm(){
    CustomerRealm customerRealm = new CustomerRealm();
    //修改密码凭证校验匹配器
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    //设置加密算法为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    //设置散列次数
    credentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(credentialsMatcher);

    
    //开启缓存ehcache管理
    customerRealm.setCacheManager(new EhCacheManager());
    //开启全局缓存管理
    customerRealm.setCachingEnabled(true);
    //开启认证缓存
    customerRealm.setAuthenticationCachingEnabled(true);
    //设置认证缓存名
    customerRealm.setAuthenticationCacheName("authenticationCache");
    //开启授权缓存
    customerRealm.setAuthorizationCachingEnabled(true);
    //设置授权缓存名
    customerRealm.setAuthorizationCacheName("authorizationCache");
    
    return customerRealm;
}

设置了缓存后,以后只有在第一次查询时会操作数据库,再次查询将不会操作数据库,有效的减轻了数据库的负担。但是如果我们的应用程序重启后,再次查询的时候第一次还是要操作数据库。

3.9、图片验证码实现

1.配置验证码工具类

package com.cheng.utils;

import java.io.IOException;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;

public class VerifyCodeUtils {
    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();


    /**
     * 使用系统默认字符源生成验证码
     * @param verifySize    验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize){
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }
    /**
     * 使用指定源生成验证码
     * @param verifySize    验证码长度
     * @param sources   验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources){
        if(sources == null || sources.length() == 0){
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for(int i = 0; i < verifySize; i++){
            verifyCode.append(sources.charAt(rand.nextInt(codesLen-1)));
        }
        return verifyCode.toString();
    }

    /**
     * 生成随机验证码文件,并返回验证码值
     * @param w
     * @param h
     * @param outputFile
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException{
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, outputFile, verifyCode);
        return verifyCode;
    }

    /**
     * 输出随机验证码图片流,并返回验证码值
     * @param w
     * @param h
     * @param os
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException{
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }

    /**
     * 生成指定验证码图像文件
     * @param w
     * @param h
     * @param outputFile
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException{
        if(outputFile == null){
            return;
        }
        File dir = outputFile.getParentFile();
        if(!dir.exists()){
            dir.mkdirs();
        }
        try{
            outputFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(outputFile);
            outputImage(w, h, fos, code);
            fos.close();
        } catch(IOException e){
            throw e;
        }
    }

    /**
     * 输出指定验证码图片流
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException{
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[] { Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW };
        float[] fractions = new float[colors.length];
        for(int i = 0; i < colors.length; i++){
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h-4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h-4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for(int i = 0; i < verifySize; i++){
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize/2, h/2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w-10) / verifySize) * i + 5, h/2 + fontSize/2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }

        }

    }
    public static void main(String[] args) throws IOException {
        //获取验证码
        String s = generateVerifyCode(4);
        //将验证码放入图片中
        outputImage(260,60,new File("/Users/chenyannan/Desktop/安工资料/aa.jpg"),s);
        System.out.println(s);
    }
}

2.实现验证码方法

//验证码方法
@RequestMapping("/getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
    //调用验证码工具类生成验证码,4位验证码
    String code = VerifyCodeUtils.generateVerifyCode(4);
    //把验证码存入session,登录时进行验证码比较
    session.setAttribute("code",code);
    //把验证码放入图片
    ServletOutputStream os = response.getOutputStream();
    //设置响应类型
    response.setContentType("image/png");
    VerifyCodeUtils.outputImage(220,60,os,code);
}

3.在登录页面添加验证码输入框

请输入验证码<input type="text" name="code" ><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>

4.在shiro配置shiroConfig中放行验证码请求

map.put("/user/getImage","anon");//公共资源

5.在认证方法中比较验证码

@RequestMapping("/login")
    public String login(String username,String password,String code,HttpSession session){

        //验证码比较
        String code1 = (String)session.getAttribute("code");//拿到session中的验证码
        //如果验证码比较正确,进行登录操作
        try {
        if (code1.equals(code)){
            Subject subject = SecurityUtils.getSubject();
            //封装前端提交的用户信息
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
                subject.login(token);//执行登录方法,执行成功就跳转主页面
                return "redirect:/index.jsp";
                //捕获可能出现的异常
        }else {
            throw new RuntimeException("验证码错误");
        }
        } catch (UnknownAccountException e) {
            e.printStackTrace();
            System.out.println("用户名错误");
            return "redirect:/login.jsp";
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            System.out.println("密码错误");
        }catch (Exception e){
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
        return "redirect:/login.jsp";
    }

启动程序,测试:

在这里插入图片描述

成功登录,OK!