快速入门Shiro

一、什么是shiro

shiro是一种易于使用的Java安全框架,shiro可以用来完成:认证、授权、加密、会话管理、与Web集成、缓存等。

二、基本功能

在这里插入图片描述
(1)Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
(2)Authorization:授权,验证已认证的用户是否拥有某个权限,能否进行相关的操作;
(3)Seesion Manager:会话管理,用户登录之后就是一次会话,没有退出之前,它的所有信息都保存在会话中;会话可以是普通的JavaSE环境,也可以是WEB环境;
(4)Cryptography:加密,保护数据的安全性,如密码加密存储到数据库
(5)Web Support:Web支持,可以非常容易的集成到Web环境
(6)Caching : 缓存,可以缓存用户登录后的信息、角色以及权限
(7)Concurrency:Shiro支持多线程应用的并发验证
(8)Testing:提供测试支持
(9)Run As:允许用户假装为另外的用户的身份进行访问
(10)Remember Me:记住我,下次登录就不需要进行验证

三、Shiro架构

从外部来看,Shiro最主要的组件是Subject、SecurityManager以及Realm
在这里插入图片描述

(1)Subject:Shiro对外的接口,使用Shiro,就得拥有Subject对象,我们对Shiro的操作都是从Subject对象开始,subject对象可以理解为操作Shiro的媒介;
(2)SecurityManager:安全管理器,与安全相关的交互都会与SecurityManager交互,它同时也管理着所有的Subject,相当于Spring MVC中的DispatchServlet的角色
(3)Realm:Shiro从Realm获取安全数据,SecurityManager要想验证用户的身份信息,就需要从Realm中获取用户的信息,或者用户所拥有的权限/角色,SecurityManager才能判断该用户是否能够进行下一步的操作。
从Shiro内部来看:
在这里插入图片描述
(1)Subject:任何可以与应用交互的“用户”
(2)SecurityManager:相当于Shiro的心脏,所有的具体交互都要通过SecurityManager来进行管理,它管理着所有 Subject、且负责认证、授权、会话以及缓存的管理。
(3)Authentication:负责Subject认证,可以自定义实现,可以使用认证策略,及什么情况下该用户算通过认证
(4)Authorizer:授权器,控制着用户能够访问那些应用,是用户鉴权的核心
(5)Realm:可以有1个或者多个Realm,可以认为是安全的实体数据源,用与获取安全实体的,可以是JDBC实现,也可以是内存实现,由用户提供;所以一般在应用中都需要实现自己的Realm;
(6)SessionManager:管理Session生命周期的组件,
(7)CacheManager:缓存
(8)Cryptography:密码加密模块,对数据库中的用户密码进行加密处理

四、登录认证的概念

(1)身份验证:验证你是否是我的用户、一般通过用户名/密码来证明身份。
(2)在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,使应用能够验证用户身份:
principals:身份,能够唯一标识主体的属性,比如手机号、邮箱、用户名等
credentials:证明/凭证,只有主体知道,比如密码
最常见的principals和credentials组合就是用户名/密码。

五、登录认证的基本流程

(1)得到用户的登录信息,比如用户名、密码;
(2)调用调用Subject.login进行登录,如果失败则得到相应的AuthenticationException异常信息,否则登录成功
(3)创建自定义的Realm类,继承shiro中的AuthenticatingRealm类,实现doGetAuthenticationInfo()方法

登录认证基本流程
身份认证的基本流程:
(1)首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
(2)SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份
验证;
(3)Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此
处可以自定义插入自己的实现;
(4)Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份
验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm
身份验证;
(5) Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如
果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序
及策略进行访问。

使用Shiro登录案例

Shiro 获取权限相关信息可以通过数据库获取,也可以通过 ini 配置文件获取,我们通过ini文件获取。
创建简单的maven工程,在resource下创建shiro.ini文件,在该文件中定义登录用户的用户名和密码
注意shiro.ini的配置文件中,【user】更改为【users】,下图没有更改,后续测试会报错。
在这里插入图片描述
导入依赖:

<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-core</artifactId>
 <version>1.9.0</version>
 </dependency>
 <dependency>
 <groupId>commons-logging</groupId>
 <artifactId>commons-logging</artifactId>
 <version>1.2</version>
 </dependency>

创建测试类:

import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

public class ShiroLoginText {
    public static void main(String[] args) {
        //1.初始化获取SecurityManager
        IniSecurityManagerFactory factory=new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager instance = factory.getInstance();

        SecurityUtils.setSecurityManager(instance);
        //2.获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        //3.创建token对象,web应用从前端传递用户名和密码
        AuthenticationToken token=new UsernamePasswordToken("zhangsan","zss");
        //4.完成登录
        try {
            subject.login(token);
            System.out.println("登录成功" );
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("用户不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

输入正确的用户名、密码:
在这里插入图片描述

输入错误的用户名:
在这里插入图片描述
输入错误的密码:
在这里插入图片描述

六、角色、鉴权

判断用户是否拥有角色,使用subject.hasRole()方法完成、或者使用注解@RequiresRoles(“角色名称”)、或者在JSP/GSP标签中,使用<shiro:hasRole name=“角色名称进行判断”>

案例扩展

在shiro.ini文件中添加角色和角色对应的权限信息
在这里插入图片描述
登录成功之后,判断该用户是否用户“admin”这个角色,同时判断是否拥有user:insert插入数据的权限
校验校色使用:subject.hasRoles()
校验权限使用:subject.isPermitted()

import com.sun.org.omg.CORBA.InitializerSeqHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

public class ShiroLoginText {
    public static void main(String[] args) {
        //1.初始化获取SecurityManager
        IniSecurityManagerFactory factory=new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager instance = factory.getInstance();

        SecurityUtils.setSecurityManager(instance);
        //2.获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        //3.创建token对象,web应用从前端传递用户名和密码
        AuthenticationToken token=new UsernamePasswordToken("zhangsan","zss");
        //4.完成登录
        try {
            subject.login(token);
            System.out.println("登录成功" );
            //验证是否拥有  角色admin
            boolean admin = subject.hasRole("admin");
            System.out.println("是否拥有admin角色 " + admin);
            //拥有该角色,验证是否拥有 User:insert权限
            boolean permitted = subject.isPermitted("user:insert");
            System.out.println("是否拥有该权限 = " + permitted);
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("用户不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

运行结果:
在这里插入图片描述
看到他没有这个角色和权限,检查一下是不是我们的参数写错了:
在这里插入图片描述
果然写错了,我们登录的账户是zhangsan,结果角色给lisi了,肯定就没有这个权限和角色了,在实际开发中,这种情况就可以拦截这个请求了,或者直接return一个空值结束请求。
我们继续把admin角色给张三
在这里插入图片描述
再次运行查看结果:
在这里插入图片描述
这次就拥有这个角色和权限啦。

7.Shrio加密

我们在数据库中保存账户名和密码时,一般都需要对账户的密码进行加密,不可能保存明文密码吧,使用shiro可以很简单的对数据进行加密
例如:

import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
public class shiroMD5 {
    public static void main(String[] args) {
        String password="12345"; //比如这是密码
        //使用MD5加密
        Md5Hash md5Hash=new Md5Hash(password);
        System.out.println("MD5加密 = " + md5Hash.toHex());
        //带盐的MD5加密,就是给你的密码加点料,在你的密码后面拼接字符串,在进行M加密
        Md5Hash md5Hash1=new Md5Hash(password,"jialiao");//很明显,多的这个字符串参数就是加的料
        System.out.println("带盐(加料)的加密= " + md5Hash1.toHex());
        //你以为这就完了?加一次还可能被别人破解,那我多加几次,多迭代几次,多加点料
        Md5Hash md5Hash2=new Md5Hash(password,"jialiao",3);
        System.out.println("迭代三次盐(加了三次料)的加密 = " + md5Hash2.toHex());
        //还可以使用父类加密,Md5Hash它的父类是SimpleHash
        //使用父类需要额外指定加密方式
        SimpleHash simpleHash=new SimpleHash("MD5",password,"jialiao",3);
        System.out.println("使用父类,指定加密方式为MD5,迭代三次盐(加了三次料)的加密 = "+simpleHash.toHex());
    }
}

运行结果:
在这里插入图片描述

八、Spring Boot 整合Shiro
创建Spring Boot项目、导入相关依赖,自定义登录认证
<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-spring-boot-web-starter</artifactId>
 <version>1.9.0</version>
 </dependency>
 
 <!--mybatis-plus-->
 <dependency>
 <groupId>com.baomidou</groupId>
 <artifactId>mybatis-plus-boot-starter</artifactId>
 <version>3.0.5</version>
 </dependency>
 <!--mysql-->
 <dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.46</version>
 </dependency>
 <dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency

在yml文件配置进行配置

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  #  MySQL数据源
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shirodb?characterEncoding=utf-8&useSSL=false
    username: *****
    password: *****
    type: com.zaxxer.hikari.HikariDataSource
#日期格式
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#登录接口
shiro:
  loginUrl: /myController/login

创建登录表

CREATE DATABASE IF NOT EXISTS `shirodb` CHARACTER SET utf8mb4;
USE `shirodb`;
CREATE TABLE `user` (
 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
 `name` VARCHAR(30) DEFAULT NULL COMMENT '用户名',
 `pwd` VARCHAR(50) DEFAULT NULL COMMENT '密码',
 `rid` BIGINT(20) DEFAULT NULL COMMENT '角色编号',
 PRIMARY KEY (`id`)
 ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';

创建数据库表对应的实体类User(或者使用MybatisX逆向生成实体类、mapper、service、serviceimpl)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
    private Integer rid;
}

创建mapper

package com.lqkj.dong.springboot_shiro.mapper;

import com.lqkj.dong.springboot_shiro.pojo.User;

public class UserMapper extends BaseMapper<User>{
   //自定义查询数据库的方法
}

创建service接口及实现类

package com.dong.shirotext.service;

import com.dong.shirotext.pojo.User;

public interface UserService {
    //定义根据用户名从数据库中查询用户信息的方法
   public User getUSerByName(String name);
}

package com.dong.shirotext.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dong.shirotext.mapper.UserMapper;
import com.dong.shirotext.pojo.User;
import com.dong.shirotext.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceimpl implements UserService {
    @Autowired
    private UserMapper userMapper;//注入mapper

    /**
     * 根据用户名,从数据库中查询用户记录
     * @param name
     * @return
     */
    @Override
    public User getUSerByName(String name) {
        User user = userMapper.selectOne(new QueryWrapper<User>().eq("name", name));
        return user;
    }
}

项目架构大概是这个样子,大家随意即可
在这里插入图片描述

当我们使用shiro框架时,我们需要shiro去查询我的数据库,走我们自己的登录验证的逻辑,这时候就需要我们自定义realm,创建类去继承AuthorizingRealm类,重写里面的两个方法(重载),即可完成自定义的授权和登录认证的方法。详见代码:

package com.dong.shirotext.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {
    //用于鉴权
    /**
     * 当前登录用户,我们可以从数据库中查询到该用户的权限,并通过该方法,将我们查询到的
     该用户拥有的角色,保存在shiro框架中,后面我们使用shiro框架去核对用户权限时,所比较的
     就是我们在这里查询、并且保存到的权限
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    /**
     * 这个方法是用于登录认证
     我们要自定义自己的登录逻辑,就需要在该方法中编写自己的登录逻辑
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}

接下来,我们自定义自己的登录逻辑,让它从我们的数据库中查询用户的信息进行比对。

package com.dong.shirotext.realm;

import com.dong.shirotext.pojo.User;
import com.dong.shirotext.service.UserService;
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;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    //用于鉴权

    /**
     * 当前登录用户,我们可以从数据库中查询到该用户的权限,并通过该方法,将我们查询到的
     * 该用户拥有的角色,保存在shiro框架中,后面我们使用shiro框架去核对用户权限时,所比较的
     * 就是我们在这里查询、并且保存到的权限
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 这个方法是用于登录认证
     * 我们要自定义自己的登录逻辑,就需要在该方法中编写自己的登录逻辑
     *
     * @param
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //首先,从token中获取登录时保存的用户信息,注意,该方法是登录时shiro会调用的方法
        //也就是说我们在登录时,会将名称和密码封装成token,所以这里不要疑惑为什么token中可以
        //获取到用户的信息,后续我们编写登录时,你就会恍然大悟
        String name = token.getPrincipal().toString();//得到的就是用户名称
        System.out.println("name = " + name);//可以打印出来看看
        //从数据库中查询用户信息,根据name查询
        User user = userService.getUSerByName(name);
        //查到用户信息之后,将登录时的密码,和现在从数据库中查到的密码进行比较
        //我们将相关的数据封装到AuthenticationInfo中,知道密码的盐,用户名、密码,
        //该对象会帮我们进行校验,并且将校验结果返回给shiro框架
        if (user != null) {
            //创建AuthenticationInfo对象,在构造器中传递相关参数
            AuthenticationInfo info = new SimpleAuthenticationInfo(
                    token.getPrincipal(),
                    user.getPwd(),//用户密码
                    ByteSource.Util.bytes("salt"),//密码使用MD5加密时指定的盐
                    token.getPrincipal().toString()
            );
            return info;
        }
        return null;
    }
}

接下来,我们去数据库中准备两条数据,注意密码要使用MD5加密,盐使用“salt”,并且需要迭代三次的密码
(12345)
在这里插入图片描述
接下来创建Controller登录接口:

package com.dong.shirotext.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/myController")
public class LoginController {
    @GetMapping("/login")//使用get方式,方便参数传递
    public String login( String name, String password){
        //1.获取subject对象
        Subject subject= SecurityUtils.getSubject();
        //通过name,password封装为token对象中
        //现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
        AuthenticationToken token=new UsernamePasswordToken(name,password);
        //然后调用subject.login方法登录
        try {
            //此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
            subject.login(token);
            return "登录成功";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "登录失败!";
        }
    }
}

ok,现在可以试一下我们能否登录成功了,在看一下我目前的目录结构在这里插入图片描述
启动我们的项目(记得在启动上方,添加MapperScan注解,扫描mapper所在的目录)
在这里插入图片描述
启动报错

Please create bean of type 'Realm' or add a shiro.ini in the root classpath (src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).

因为我们目前还没有对shiro进行配置,所以shiro默认去寻找配置文件,发现找不到配置文件,没有配置文件就没有Realm对象,没有Realm对象你让我怎么干活,直接罢工了,所以解下来我们需要对shiro进行配置,既然使用了Spring Boot框架,我们就不需要配置文件了, 直接创建一个配置类即可

package com.dong.shirotext.config;

import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.management.MXBean;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm realm; //注入我们自定义的Realm对象
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        DefaultWebSecurityManager defaultWebSecurityManager=
                new DefaultWebSecurityManager();
        //创建密码加密对象
        HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
        //设置加密对象的属性
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(3);
        //将加密对象  存储到Realm对象中
        realm.setCredentialsMatcher(matcher);
        //将Realm对象 存入 defaultWebSecurityManager对象中
        defaultWebSecurityManager.setRealm(realm);
        //返回
        return defaultWebSecurityManager;
    }
    //配置shiro的拦截范围,我们的登录接口肯定是需要放行的
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition definition
                =new DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        definition.addPathDefinition("/myController/userlogin","anon");
        definition.addPathDefinition("/myController/login","anon");
        //配置需要拦截的请求范围
        definition.addPathDefinition("/**","authc");
        
        return definition;
    }
}

ok,对shiro进行完配置,同时放行了我们的登录接口,这次启动应该不会报错了吧,启动项目
在这里插入图片描述
启动成功,测试登录接口
http://localhost:8080/myController/login?name=zs&password=12345
在这里插入图片描述
可以看到能够登录成功,并且是通过数据库的方式登录。
接下来,我们增加一个前端页面,在前端页面中进行登录操作,在resources文件下创建templates文件,在templates文件下创建login.html和登录成功之后的跳转页面main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Shiro 登录认证</h1>
<br>
<form action="/myController/userlogin">
    <div>用户名:<input type="text" name="name" value=""></div>
    <div>密码:<input type="password" name="password" value=""></div>
    <div><input type="submit" value="登录"></div>
</form>
</body>
</html>

main.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为:<span th:text="${session.user}"></span>
</body>
</html>

增加一个跳转登录页面的controller方法:

 @GetMapping("/login")
    public String toLogin(){
        return "login";
    }

改造登录接口

package com.dong.shirotext.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/myController")
public class LoginController {

    @GetMapping("/userlogin")//使用get方式,方便参数传递
    public String login(String name, String password, HttpSession session){
        //1.获取subject对象
        Subject subject= SecurityUtils.getSubject();
        //通过name,password封装为token对象中
        //现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
        AuthenticationToken token=new UsernamePasswordToken(name,password);
        //然后调用subject.login方法登录
        try {
            //此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
            subject.login(token);
            //将用户信息存到session中
         session.setAttribute("user",token.getPrincipal().toString());
         //跳转页面
            return "main";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "登录失败!";
        }
    }
    @GetMapping("/login")
    public String toLogin(){
        return "login";
    }
}

再次启动项目,访问http://localhost:8080/myController/login,页面如下:
在这里插入图片描述
输入用户名称,密码点击登录,能够登录成功,并且展示存在session中的用户
在这里插入图片描述

九、多个Realm校验策略

到这里我们发现了,其实一种Realm就代表了一种认证规则,而Shiro可以同时创建多个Realm,每一个Realm代表不同的认证规则,比用账号密码、手机号+验证码等等,如果存在多个Realm,Shiro又该怎么进行验证?
Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认证是成功还是失败。
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这4 次交互所需的任何必要的状态将被作为方法参数):

  • 在所有 Realm 被调用之前
  • 在调用 Realm 的 getAuthenticationInfo 方法之前
  • 在调用 Realm 的 getAuthenticationInfo 方法之后
  • 在所有 Realm 被调用之后

认证策略的另外一项工作就是聚合所有 Realm 的结果信息封装至一个AuthenticationInfo 实例中,并将此信息返回,以此作为 Subject 的身份信息。
Shiro中定义了三种认证策略的实现

AuthenticationStrategy class描述
AtLeastOneSuccessfulStrategy只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功
FirstSuccessfulStrategy第一个Realm验证成功,就认为整体是成功的,后面的Realm会被忽略
AllSuccessfulStrategy所有的Realm都验证成功,认证才视为成功

ModularRealmAuthenticator 默认的认证策略是第一种AtLeastOneSuccessfulStrategy

多个Realm代码实现,主要在配置中配置modularRealmAuthenticator,指定认证策略,然后封装Realm集合

//多个Realm
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        //创建认证对象,并指定认证策略
        ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
        //这里指定了第三种认证策略,及所有的Realm都要认证成功
        modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
        //封装Realm集合
        List<Realm> list=new ArrayList<>();
        list.add(realm);//有几个就添加几个
        //将Realm集合存入DefaultWebSecurityManager中返回
        manager.setRealms(list);
        return manager;
    }
}
十、通过Shiro实现记住我

记住我,这个功能相信大家都有使用过,我们直接开始正文,首先,在shiro配置类中进行配置

package com.dong.shirotext.config;
import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AllSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MXBean;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm realm; //注入我们自定义的Realm对象

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager =
                new DefaultWebSecurityManager();
        //创建密码加密对象
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //设置加密对象的属性
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(3);
        //将加密对象  存储到Realm对象中
        realm.setCredentialsMatcher(matcher);
        //将Realm对象 存入 defaultWebSecurityManager对象中
        defaultWebSecurityManager.setRealm(realm);
        //设置remember Me
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        //返回
        return defaultWebSecurityManager;
    }
    //cokie属性设置
    public SimpleCookie remeberMeCookie(){
        SimpleCookie cookie=new SimpleCookie("remeberMe");
        //设置跨域
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        return cookie;
    }
    //创建Shiro的cookie管理对象
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(remeberMeCookie());
        cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
        return cookieRememberMeManager;
    }
    //配置shiro的拦截范围,我们的登录接口肯定是需要放行的
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition definition
                = new DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        definition.addPathDefinition("/myController/userlogin", "anon");
        definition.addPathDefinition("/myController/logon", "anon");
        //配置需要拦截的请求范围
        definition.addPathDefinition("/**", "authc");
        //添加存在用户的过滤器(remeberMe)
        definition.addPathDefinition("/**","user");
        return definition;
    }
}

修改controller

    @GetMapping("/userlogin")//使用get方式,方便参数传递
    public String login(String name, String password,
                        @RequestParam(defaultValue = "false") boolean rememberMe,
                        HttpSession session){
        //1.获取subject对象
        Subject subject= SecurityUtils.getSubject();
        //通过name,password封装为token对象中
        //现在能理解我们自定义的登录验证方法中,为什么能够从token中获取用户信息了吧
        AuthenticationToken token=new UsernamePasswordToken(name,password,rememberMe);
        //然后调用subject.login方法登录
        try {
            //此时就会去执行我们自定义的登录验证的方法,同时将token作为参数传递过去了
            subject.login(token);
            //将用户信息存到session中
         session.setAttribute("user",token.getPrincipal().toString());
         //跳转页面
            return "main";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "登录失败!";
        }
    //登录认证验证 rememberMe
    @GetMapping("userLoginRm")
    public String userLogin(HttpSession session) {
        session.setAttribute("user","rememberMe");
        return "main";
    }
    }

启动项目,直接去访问: http://localhost:8080/myController/userLoginRm 会跳转到登录页面让我们登录,
在这里插入图片描述
选择记住我,然后登录,重启浏览器直接访问http://localhost:8080/myController/userLoginRm ,就不会自动跳转到登录页面。,而是直接访问成功。
在这里插入图片描述

十一、用户认证登录成功之后退出登录

退出登录很简单,我们直接把退出登录的接口配置在Shiro配置类中即可

        //退出登录
        definition.addPathDefinition("/logout","logout");
十二、授权,角色验证

用户登录后,需要验证是否具有指定角色指定权限。Shiro也提供了方便的工具进行判断。
这个工具就是Realm的doGetAuthorizationInfo方法进行判断。触发权限判断的有两种方式:

  • 在页面中通过shiro:****属性判断
  • 在接口服务中通过注解@Requires****进行判断

通过给接口服务方法添加注解可以实现权限校验,可以加在控制器方法上,也可以加在业务方法上,一般加在控制器方法上。常用注解如下:

  • @RequiresAuthentication
    验证用户是否登录,等同于方法subject.isAuthenticated()
  • @RequiresUser
    验证用户是否被记忆:
    登录认证成功subject.isAuthenticated()为true
    登录后被记忆subject.isRemembered()为true
  • @RequiresGuest
    验证是否是一个guest的请求,是否是游客的请求
    此时subject.getPrincipal()为null
  • @RequiresRoles
    验证subject是否有相应角色,有角色访问方法,没有则会抛出异常AuthorizationException。
    例如:@RequiresRoles(“aRoleName”)
    void someMethod();
    只有subject有aRoleName角色才能访问方法someMethod()
  • @RequiresPermissions
    验证subject是否有相应权限,有权限访问方法,没有则会抛出异常AuthorizationException。
    例如:
    @RequiresPermissions (“file:read”,”wite:aFile.txt”)
    void someMethod();
    subject必须同时含有file:read和wite:aFile.txt权限才能访问方法someMethod()

新加接口,该接口添加注解RequiresRoles,表示当前登录的用户需要拥有admin角色才能访问该方法。

    @GetMapping("/userLoginRoles")
    @ResponseBody
    @RequiresRoles("admin")
    public String Role(){
        return "恭喜你通过认证!";
    }

启动项目进行测试,直接报错,报错信息就是当前的用户不具有admin这个角色。
在这里插入图片描述
赋予它admin的角色

   @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //我们直接当前的用户赋予它admin的角色,正常来说角色是需要根据用户名称去数据库中查询的
//        String name = principalCollection.getPrimaryPrincipal().toString();
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        authorizationInfo.addRole("admin");
        return authorizationInfo;
    }

这次就不会再报错
在这里插入图片描述
无论是角色还是权限,都是在我们自定义的Realm中的 doGetAuthorizationInfo(PrincipalCollection principalCollection) 方法中进行验证,如果我们的用户存在角色和权限,我们需要再该方法中,将角色和权限从数据库中查询出来,保存在AuthenticationInfo对象中返回。

十三、前端页面授权验证Thymeleaf

添加依赖

<!--配置 Thymeleaf 与 Shrio 的整合依赖-->
<dependency>
 <groupId>com.github.theborakompanioni</groupId>
 <artifactId>thymeleaf-extras-shiro</artifactId>
 <version>2.0.0</version>
</dependency>

配置类中添加新配置

    //解析thymeleaf中的shiro:相关属性
    @Bean
    public ShiroDialect shiroDialect(){
        return  new ShiroDialect();
    }

Thymeleaf 中常用的 shiro:属性

--guest 标签
<shiro:guest>
</shiro:guest>
用户没有身份验证时显示相应信息,即游客访问信息。
--user 标签
<shiro:user>
</shiro:user>
用户已经身份验证/记住我登录后显示相应的信息。
--authenticated 标签
<shiro:authenticated>
</shiro:authenticated>
用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。
--notAuthenticated 标签
<shiro:notAuthenticated>
</shiro:notAuthenticated>
用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的
也属于未进行身份验证。
--principal 标签
<shiro: principal/>
<shiro:principal property="username"/>
相当于((User)Subject.getPrincipals()).getUsername()。
-lacksPermission 标签
<shiro:lacksPermission name="org:create">
</shiro:lacksPermission>
如果当前 Subject 没有权限将显示 body 体内容。
--hasRole 标签
<shiro:hasRole name="admin">
</shiro:hasRole>
如果当前 Subject 有角色将显示 body 体内容。
--hasAnyRoles 标签
<shiro:hasAnyRoles name="admin,user">
</shiro:hasAnyRoles>
如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。
--lacksRole 标签
<shiro:lacksRole name="abc">
</shiro:lacksRole>
如果当前 Subject 没有角色将显示 body 体内容。
--hasPermission 标签
<shiro:hasPermission name="user:create">
</shiro:hasPermission>
如果当前 Subject 有权限将显示 body 体内容
十四、EhCache缓存的搭建使用

导入依赖

<!--Shiro 整合 EhCache-->
<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-ehcache</artifactId>
 <version>1.4.2</version>
</dependency>
<dependency>
 <groupId>commons-io</groupId>
 <artifactId>commons-io</artifactId>
 <version>2.6</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
 <!--磁盘的缓存位置-->
 <diskStore path="java.io.tmpdir/ehcache"/>
 <!--默认缓存-->
 <defaultCache
 maxEntriesLocalHeap="10000"
 eternal="false"
 timeToIdleSeconds="120"
 timeToLiveSeconds="120"
 maxEntriesLocalDisk="10000000"
 diskExpiryThreadIntervalSeconds="120"
 memoryStoreEvictionPolicy="LRU">
 <persistence strategy="localTempSwap"/>
 </defaultCache>
 <!--helloworld 缓存-->
 <cache name="HelloWorldCache"
 maxElementsInMemory="1000"
 eternal="false"
 timeToIdleSeconds="5"
 timeToLiveSeconds="5"
 overflowToDisk="false"
 memoryStoreEvictionPolicy="LRU"/>
 <!--
 defaultCache:默认缓存策略,当 ehcache 找不到定义的缓存时,则使用这个
缓存策略。只能定义一个。
 -->
 <!--
 name:缓存名称。
 maxElementsInMemory:缓存最大数目
 maxElementsOnDisk:硬盘最大缓存个数。
 eternal:对象是否永久有效,一但设置了,timeout 将不起作用。
 overflowToDisk:是否保存到磁盘,当系统宕机时
 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当
eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间
无穷大。
 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间
介于创建时间和失效时间之间。仅当 eternal=false 对象不是永久有效时使用,默认
是 0.,也就是对象存活时间无穷大。
 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store 
persists between restarts of the Virtual Machine. The default value 
is false.
 diskSpoolBufferSizeMB:这个参数设置 DiskStore(磁盘缓存)的缓存区大
小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。
 diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是
120 秒。
 memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,
Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以
设置为 FIFO(先进先出)或是 LFU(较少使用)。
 clearOnFlush:内存数量最大时是否清除。
 memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策
略)、FIFO(先进先出)、LFU(最少访问次数)。
 FIFO,first in first out,这个是大家最熟的,先进先出。
 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是
讲一直以来最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将
会被清出缓存。
 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当
缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳
离当前时间最远的元素将被清出缓存。
 -->
</ehcache>

在在 resources 下添加配置文件 ehcache/ehcache-shiro.xml
在这里插入图片描述
配置文件中进行如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
 <!--磁盘的缓存位置-->
 <diskStore path="java.io.tmpdir"/>
 <!--默认缓存-->
 <defaultCache
 maxEntriesLocalHeap="1000"
 eternal="false"
 timeToIdleSeconds="3600"
 timeToLiveSeconds="3600"
 overflowToDisk="false">
 </defaultCache>
 <!--登录认证信息缓存:缓存用户角色权限-->
<cache name="loginRolePsCache"
 maxEntriesLocalHeap="2000"
 eternal="false"
 timeToIdleSeconds="600"
 timeToLiveSeconds="0"
 overflowToDisk="false"
 statistics="true"/>
</ehcache>

修改shiro配置类

package com.dong.shirotext.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.dong.shirotext.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AllSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.management.MXBean;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyRealm realm; //注入我们自定义的Realm对象

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager =
                new DefaultWebSecurityManager();
        //创建密码加密对象
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //设置加密对象的属性
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(3);
        //将加密对象  存储到Realm对象中
        realm.setCredentialsMatcher(matcher);
        //将Realm对象 存入 defaultWebSecurityManager对象中
        defaultWebSecurityManager.setRealm(realm);
        //设置remember Me
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        //设置缓存管理器
        defaultWebSecurityManager.setCacheManager(getCacheManager());
        //返回
        return defaultWebSecurityManager;
    }
//获取缓存管理器
    private CacheManager getCacheManager() {
        EhCacheManager ehCacheManager=new EhCacheManager();
        //读取配置文件
        InputStream is=null;
        try{
            is= ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
        }catch (IOException e){
            e.printStackTrace();
        }
        //获取缓存管理器
        net.sf.ehcache.CacheManager cacheManager=new net.sf.ehcache.CacheManager(is);
        ehCacheManager.setCacheManager(cacheManager);
        return ehCacheManager;
    }

    //cokie属性设置
    public SimpleCookie remeberMeCookie(){
        SimpleCookie cookie=new SimpleCookie("rememberMe");
        //设置跨域
        cookie.setPath("/");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(30*24*60*60);
        return cookie;
    }
    //创建Shito的cookie管理对象
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(remeberMeCookie());
        cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
        return cookieRememberMeManager;
    }
    //配置shiro的拦截范围,我们的登录接口肯定是需要放行的
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition definition
                = new DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        definition.addPathDefinition("/myController/userlogin", "anon");
        definition.addPathDefinition("/myController/logon", "anon");
        //配置需要拦截的请求范围
        definition.addPathDefinition("/**", "authc");
        //添加存在用户的过滤器(remeberMe)
        definition.addPathDefinition("/**","user");
        //退出登录
        definition.addPathDefinition("/logout","logout");
        return definition;
    }
    //解析thymeleaf中的shiro:相关属性
    @Bean
    public ShiroDialect shiroDialect(){
        return  new ShiroDialect();
    }
}

十五、会话管理

会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话。默认情况下,Shiro会检测当前环境中现有的会话机制(比如Servlet容器)进行适配,如果没有(比如独立应用程序或者非Web环境),它将会使用内置的企业会话管理器来提供相应的会话管理服务,其中还涉及一个名为SessionDAO的对象。SessionDAO负责Session的持久化操作(CRUD),允许Session数据写入到后端持久化数据库。
SessionManager由SecurityManager管理。Shiro提供了三种实现:

  • DefaultSessionManager:用于JavaSE环境
  • ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话
  • DefaultWebSessionManager:用于web环境,自己维护会话(不使用Servlet容器的会话管理)

获取Session方式

(1)实现
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute(“key”,”value”)
(2)说明
Controller 中的 request,在 shiro 过滤器中的 doFilerInternal 方法,被包装成ShiroHttpServletRequest。
SecurityManager 和 SessionManager 会话管理器决定 session 来源于 ServletRequest还是由 Shiro 管理的会话。
无论是通过 request.getSession 或 subject.getSession 获取到 session,操作session,两者都是等价的。
总结

使用Shiro,导入Shiro依赖,自定义自己的登录逻辑和鉴权,继承AuthorizingRealm类,重写其中两个方法,一个用于鉴权,一个用于登录认证。
然后是shiro配置类的编写,自定义的操作需要再配置类中进行配置,DefaultWebSecurityManager。
其次,在Spring Boot中,可以使用注解@RequiresRole(是否拥有角色)、@RequiresPermissions(是否拥有权限)、@RequiresGuest(是否游客)、@RequiresUser(是否被记忆)、@RequiresAuthentication(是否登录)。
最后是缓存EhCache和Session的使用。demo案例在代码包中,大家可以自己下载来看看。

  • 9
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@Yjd007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值