Shiro

第一章·入门概述

1.1、是什么

Apache Shiro | Simple. Java. Security.官网

111

Shiro是一个强大的简单易用的Java安全框架,主要用来更便捷的认证,授权,加密,会话管理。Shiro首要的和最重要的目标就是容易使用并且容易理解。

1.2、为什么要使用Shiro

shiro的特点:

  1. 身份认证:支持多种方式的身份认证,如LDAP、数据库、INI文件等。

  2. 授权:支持基于角色的访问控制(RBAC)和基于权限的访问控制(PBAC),可以进行精细的权限控制。

  3. 会话管理:无论是在单机环境还是分布式环境,Shiro都提供了会话管理的功能。

  4. 加密:Shiro提供了一套简单易用的加密/解密API,支持多种加密算法。

  5. 缓存支持:Shiro内置了对缓存的支持,可以很容易地集成各种缓存框架。

  6. 与Web集成:Shiro可以很容易地与Web环境集成,提供了一套用于Web安全的过滤器。

  7. 易于使用:Shiro的API设计简洁明了,易于理解和使用。

  8. 灵活性:Shiro的各个组件都是解耦的,可以根据需要进行自定义和扩展。

1.3、Shiro与SpringSecurity对比

1、Spring·Security·基于Spring·开发,项目若使用·Spring·作为基础,配合·SprSecurity·做权限更加方便,而·Shiro·需要和·Spring·进行整合开发;

2、Spring-Security·功能比·Shiro·更加丰富些,例如安全维护方面;

3、Spring Security·社区资源相对比·Shiro·更加丰富;

4、Shiro·的配置和使用比较简单,Spring- Security上手复杂些;

5、Shiro·依赖性低,不需要任何框架和容器,可以独立运行.Spring-Security-依赖Spring容器;

6、shiro·不仅仅可以使用在web中,它可以工作在任何应用环境中。在集群会话时Shiro最重要的一个好处或许就是它的会话是独立于容器的。

1.4、基本功能

image-20230724095554910

  • Authentication 认证验证

  • Authorization 权限授权

  • SessionManagement 会话管理

  • Cryptography 数据加密

  • Caching 缓存机制

  • Concurrency 并发性

  • testing 单元测试

  • RememberMe 记住我

2、基本框架原理

1、从外部来看Shiro,即从应用程序角度来观察Shiro怎么完成工作的

image-20230724100741093

  • Subject:翻译为主角,当前参与应用安全部分的主角。可以是用户,可以试第三方服务,可以是cron 任务,或者任何东西。主要指一个正在与当前软件交互的东西。 所有Subject都需要SecurityManager,当你与Subject进行交互,这些交互行为实际上被转换为与SecurityManager的交互

  • SecurityManager:安全管理员,Shiro架构的核心,它就像Shiro内部所有原件的保护伞。然而一旦配置了SecurityManager,SecurityManager就用到的比较少,开发者大部分时间都花在Subject上面。 请记得,当你与Subject进行交互的时候,实际上是SecurityManager在背后帮你举起Subject来做一些安全操作。

  • Realms:Realms作为Shiro和你的应用的连接桥,当需要与安全数据交互的时候,像用户账户,或者访问控制,Shiro就从一个或多个Realms中查找。 Shiro提供了一些可以直接使用的Realms,如果默认的Realms不能满足你的需求,你也可以定制自己的Realms

2、从内部结构看工作原理

image-20230724101334645

  • Authenticator:负责·Subject·认证,是一个扩展点,可以自定义实现;可以使用认证·策略(Authentication-Strategy),即什么情况下算用户认证通过了;

  • Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控·制着用户能访问应用中的哪些功能;

  • Realm:可以有一个或多个·Realm,可以认为是安全实体数据源,即用于获取安全实体·的;可以是JDBC·实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要·实现自己的· Realm;·

  • SessionManager:管理·Session生命周期的组件;而·Shiro·并不仅仅可以用在·Web环境,也可以用在如普通的·JavaSE·环境;

  • CacheManager:缓存控制器,T来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;

  • Cryptography:密码模块,Shiro·提高了一些常见的加密组件用于如密码加密/解密。

第二章·基本使用

2.1、基本环境搭建

创建一个普通的maven项目,Shiro获取权限相关信息可以通过数据库获取,也可以通过ini配置文件获得

这里使用配置文件ini获取

<!--导入Shiro依赖-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.10.0</version>
</dependency>
<!--导入通用的日志包-->
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
[users]
zhangsan=z3
kishi=l4

2.2、登录认证

概念:说白了就是登录的用户名和密码

(1)身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供·emai1,用户名/密码来证明。

(2)在shiro中,用户需要提供principals(身份)和credentials (证明)给shiro,从而应用能验证用户身份:

(3) principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/邮箱/手机号。

(4) credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。

(5)最常见的principals和credentials组合就是用户名/密码;

登录认证的基本流程

(1)收集用户身份/凭证,即如用户名/密码;

(2)调用·Subject.login·进行登录,如果失败将得到相应·的·AuthenticationException异常,根据异常提示用户·错误信息;否则登录成功;

(3)创建自定义的·Realm-类,继承· org.apache. shiro.realm.AuthenticatingRealm类,实现·doGetAuthenticationInfo()·方法。

image-20230724110356123

实例

创建测试类ShiroRun

package com.zhang;
​
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.env.BasicIniEnvironment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
​
public class ShiroRun {
    public static void main(String[] args) {
        //1,初始化获取SecurityManager
        BasicIniEnvironment factory =new BasicIniEnvironment("classpath:shiro.ini");
        SecurityManager securityManager = factory.getSecurityManager();//得到认证管理对象
        SecurityUtils.setSecurityManager(securityManager);//将认证管理器对象放到公具中,通过工具获取到subject对象
        //2、获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        //3、模拟前端获得表单,创建token对象,web应用用户名和密码从页面传递
        AuthenticationToken token = new UsernamePasswordToken("zhangsan", "123456");
        //4、完成登录
        try {
            subject.login(token);
            System.out.println("登陆成功");
        }catch (UnknownAccountException e){
            e.printStackTrace();
            System.out.println("用户名不存在");
        }catch (IncorrectCredentialsException e){
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

运行起来

image-20230224113339322

修改成错误的用户名:

image-20230724113444593

修改成错误的密码:

bgc

2.3、角色、授权

概念:

(1)授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作·等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限·(Permission)、角色(Role) 。

(2)主体(Subject):访问应用的用户,在· Shiro-中使用·Subject-代表该用户。用户只有授权·后才允许访问相应的资源。

(3)资源(Resource):在应用中用户可以访问的·URL,比如访问·JSP·页面、查看/编辑某些·数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。

(4)权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户·有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用·户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控·制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允·许。

(5) Shiro·支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,·即实例级别的)。

(6)角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有·一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等·都是角色,不同的角色拥有一组不同的权限。

授权方式

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

  • 注解式:通过@RequiresRoles()

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

授权流程

(1)首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着会委托给·Authorizer;

(2) Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;

(3)在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;

(4) Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole·会返回true,否则返回false表示授权失败;

we

实例测试

在shiro.ini文件中给zhangsan加上角色r1,r2

[users]
zhangsan=123456,r1,r2
kishi=123456

在测试类中添加

//是否拥有此角色
boolean role = subject.hasRole("r1");
System.out.println("是否拥有此角色:"+role);

image-20230724144321084

给角色添加权限,在shiro.ini文件增加权限集合

[roles]
r1=user:insert,user:select

测试

//是否有此权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有此权限:"+permitted);

image-20230724144606433

2.4、shiro加密

实际系统开发中,一些敏感信息需要进行加密,比如说用户的密码。Shiro 内嵌很多常用的加密算法,比如MD5加密。Shiro可以很简单的使用信息加密。<

实例:

新建ShiroMD5测试类

public class ShiroMD5 {
​
    public static void main(String[] args) {
        //明文密码
        String pwd = "123456";
        //就行加密
        Md5Hash md5Hash = new Md5Hash(pwd);
        System.out.println("md5加密后="+md5Hash);
    }
}

image-20230724145312382

带盐的md5加密,盐就是在密码明文后拼接新字符串,然后再进行加密

//带盐的md5加密,盐就是在密码明文后拼接新字符串,然后再进行加密
Md5Hash salt = new Md5Hash(pwd, "salt");
System.out.println("带盐的md5加密 = " + salt.toHex());

image-20230724150546888

为了保证安全。避免被破解还可以多次迭代加密,保证数据安全

//为了保证安全。避免被破解还可以多次迭代加密,保证数据安全
Md5Hash diedai = new Md5Hash(pwd, "salt", 3);
System.out.println("迭代3次带盐的md5加密 = " + diedai.toHex());

image-20230724150804501

使用父类进行加密

//使用父类进行加密
SimpleHash simpleHash = new SimpleHash("MD5", pwd, "salt", 3);
System.out.println("父类带盐的三次加密 = " + simpleHash.toHex())

image-20230724151037180

完整代码:

package com.zhang;

import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;

public class ShiroMD5 {

    public static void main(String[] args) {
        //明文密码
        String pwd = "123456";
        //md5加密,toHex转16进制
        Md5Hash md5Hash = new Md5Hash(pwd);
        System.out.println("md5加密后="+md5Hash.toHex());
        //带盐的md5加密,盐就是在密码明文后拼接新字符串,然后再进行加密
        Md5Hash salt = new Md5Hash(pwd, "salt");
        System.out.println("带盐的md5加密 = " + salt.toHex());
        //为了保证安全。避免被破解还可以多次迭代加密,保证数据安全
        Md5Hash diedai = new Md5Hash(pwd, "salt", 3);
        System.out.println("迭代3次带盐的md5加密 = " + diedai.toHex());
        //使用父类进行加密
        SimpleHash simpleHash = new SimpleHash("MD5", pwd, "salt", 3);
        System.out.println("父类带盐的三次加密 = " + simpleHash.toHex());
    }
}

2.5、shiro自定义登录认证

Shiro 默认的登录认证是不带加密的,如果想要实现加密认证需要自定义登录认证,自定义Realm。

创建自定义的·Realm·类,继承·org.apache.shiro.realm.AuthenticatingRealm类,实现

·doGetAuthenticationInfo()·方法

新建MyRealm自定义Realm类继承AuthenticatingRealm

package com.zhang;

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.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

public class MyRealm extends AuthenticatingRealm {
    /*自定义登录认证方法,shiro的Login方法底层会调用该类的认证方法进行认证
    需要配置自定义的reaLm生效,在ini文件中配置,在Springboot中配置
    该方法只是获取进行对比的信息,认证逻辑还是按Kishiro底层认证逻辑完成
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1获取身份信息
        String principal = authenticationToken.getPrincipal().toString();
        //2获取凭证信息
        String pwd = new String((char[]) authenticationToken.getCredentials());
        System.out.println("认证用户信息: "+principal +"-----"+ pwd);
        //3获取数据库中存储的用户信息
        if (principal.equals("zhangsan")){
            //数据库中存储的加盐3次迭代加密的密码
            String pwdInfo="d1b129656359e35e95ebd56a63d7b9e0";
            //4创建封装校验逻辑对象,封装数据返回
            AuthenticationInfo info =new SimpleAuthenticationInfo(
                    authenticationToken.getPrincipal(),
                    pwdInfo,
                    ByteSource.Util.bytes("salt"),
                    authenticationToken.getPrincipal().toString()
            );
            return info;
        }
        return null;
    }
}

现在还需要去配置使其生效,配置在shiro.ini文件中

[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.zhang.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm

[users]
zhangsan=d1b129656359e35e95ebd56a63d7b9e0,r1,r2
kishi=123456

现在运行ShiroRun测试类看结果,可以登录成功,经过加密的密码能登录成功,但这里由于没有写自定义授权,所以角色和权限都是false

image-20230724162055291

第三章·Shiro整合Springboot

3.1、创建SpringBoot项目

创建名为SpringBoot-Shiro的项目,添加这些依赖,如何再添加Shiro的依赖和Mybatis的依赖

image-20230724163619530

<!--Shiro-->
  <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.10.0</version>
   </dependency>
<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
#整合mybatis
mybatis:
  type-aliases-package: com.zm.pojo
  mapper-locations: classpath:mybatis/*.xml
#配置Shiro,当shiro拦截到请求时就给它转到/login登录
shiro:
  loginUrl: /login

创建实体类:

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

创建UserMapper接口,先写一个根据姓名查询用户

@Mapper
public interface UserMapper {
    //通过用户名获取用户
    User getUserByName(String name);
}

userMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zm.mapper.UserMapper">
    <select id="getUserByName" resultType="User">
        select * from mybatis.user where name=#{name};
    </select>
</mapper>

service层

@Service
public class UserService {
    @Autowired
    UserMapper userMapper;
    public User getUserByName(String name){
        return userMapper.getUserByName(name);
    }
}

3.2、自定义realm,登录认证实现接口服务

新建目录realm ,新建MyRealm类继承AuthorizingRealm

@Component
public class MyRealm extends AuthorizingRealm {
    @Autowired
    UserService userService;
    //自定义授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    //自定义认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1获取用户身份信息
        String name = authenticationToken.getPrincipal().toString();
        //2调用业务层获取用户信息在数据库里的
        User user = userService.getUserByName(name);
        //3非空判断,将数据封装返回
        if (user != null){
         AuthenticationInfo info =new SimpleAuthenticationInfo(
                 authenticationToken.getPrincipal(),
                 user.getPwd(),
                 ByteSource.Util.bytes("salt"),
                 authenticationToken.getPrincipal().toString()
         );
         return info;
        }
        return null;
    }
}

编写ShiroConfig配置类

新建config,然后新建ShiroConfig配置类

@Configuration
public class ShiroConfig {
    @Autowired
    MyRealm myRealm;
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        //1 创建 defaultWebSecurityManager 对象
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //2 创建加密对象,并设置相关属性
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //2.1 采用 md5 加密
        matcher.setHashAlgorithmName("MD5");
        //2.2 迭代加密次数
        matcher.setHashIterations(3);
        //3 将加密对象存储到 myRealm 中
        myRealm.setCredentialsMatcher(matcher);
        //4 将 myRealm 存入 defaultWebSecurityManager 对象
        defaultWebSecurityManager.setRealm(myRealm);
        //5、返回
        return defaultWebSecurityManager;
    }
    //配置 Shiro 内置过滤器拦截范围
    @Bean
    public DefaultShiroFilterChainDefinition
    shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition definition = new
                DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        //anon是匿名拦截器:不需要登陆就可以访问的资源
        //authc是登录拦截器:需要登录才能访问
        definition.addPathDefinition("/userLogin","anon");
        definition.addPathDefinition("/login","anon");
        //设置需要进行登录认证的拦截范围
        definition.addPathDefinition("/**","authc");
        return definition;
    }

}

创建controller,ShiroController

@Controller
public class ShiroController {
    @Autowired
    UserMapper userMapper;
    
    @GetMapping("/userLogin")
    @ResponseBody
    public String UserLogin(String name, String pwd){
        System.out.println("账号:"+name+"---密码"+pwd);
        System.out.println(userMapper.getUserByName(name));
        //1 获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        //2 封装请求数据到token对象中
        UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
        //3 调用login方法就行登录验证
        try{
            subject.login(token);
            return "登陆成功!";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "登录失败!";
        }
    }
}

输入url:localhost:8080/userLogin?name=张三&pwd=123456,登录成功

image-20230724201016882

查看后台输出的张三信息

image-20230724200948838

3.3、添加login页面

在templates下添加login.html和main.html

login.html

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

添加登录的controller,改造认证方法

@Controller
public class ShiroController {
    //登录
    @RequestMapping("/login")
    public String login(){
        return "login";
    }
    @GetMapping("/userLogin")
    public String UserLogin(String name, String pwd, HttpSession session){
        System.out.println("name = " + name+"pwd--->"+pwd);
        //1 获取Subject对象
        Subject subject = SecurityUtils.getSubject();
        //2 封装请求数据到token对象中
        UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
        //3 调用login方法就行登录验证
        try{
            subject.login(token);
            session.setAttribute("user",token.getPrincipal().toString());
            return "main";
        }catch (AuthenticationException e){
            e.printStackTrace();
            return "登录失败!";
        }
    }
}

重启访问(http://localhost:8080/login) 输入张三和密码123456

image-20230725094915001

image-20230725094804177

3.4、多个realm的认证策略设置

原理:

当应用程序配置多个 Realm 时,例如:用户名密码校验、手机号验证码校验等等。 Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认 证是成功还是失败。

AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这 4 次交互所需的任何必要的状态将被作为方法参数):

(1)在所有 Realm 被调用之前

(2)在调用 Realm 的 getAuthenticationInfo 方法之前

(3)在调用 Realm 的 getAuthenticationInfo 方法之后

(4)在所有 Realm 被调用之后

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

AuthenticationStrategy

描述

AtLeastOneSuccessfulStrategy

只要有一个(或更多)的 Realm 验证成功,那么认证将视为成功

FirstSuccessfulStrategy

第一个 Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略

AllSuccessfulStrategy

所有 Realm 成功,认证才视为成功

ModularRealmAuthenticator 内置的认证策略默认实现是 AtLeastOneSuccessfulStrategy 方式。可以通过配置修改策略。

实例,配置SecurityManager

//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
 //1 创建 defaultWebSecurityManager 对象
 DefaultWebSecurityManager defaultWebSecurityManager = new 
DefaultWebSecurityManager();
 //2 创建认证对象,并设置认证策略
 ModularRealmAuthenticator modularRealmAuthenticator = new 
ModularRealmAuthenticator();
 modularRealmAuthenticator.setAuthenticationStrategy(new 
AllSuccessfulStrategy());
 
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator)
;
 //3 封装 myRealm 集合
 List<Realm> list = new ArrayList<>();
 list.add(myRealm);
 list.add(myRealm2);
 
 //4 将 myRealm 存入 defaultWebSecurityManager 对象
 defaultWebSecurityManager.setRealms(list);
 //5 返回
 return defaultWebSecurityManager;
}

3.5、 remember me 功能

remember me顾名思义就是记住我,这次登录的账号数据关闭浏览器再打开还是原样,不用登陆的就能访问。

基本流程

(1)首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会 把 RememberMe 的 Cookie 写到客户端并保存下来;

(2) 关闭浏览器再重新打开;会发现浏览器还是记住你的;

(3) 访问一般的网页服务器端,仍然知道你是谁,且能正常访问;

(4) 但是,如果我们访问电商平台时,如果要查看我的订单或进行支付时,此时还 是需要再进行身份认证的,以确保当前用户还是你。

实例,修改配置类

ShiroConfig的defaultWebSecurityManager方法添加内容

//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealm(myRealm);

新加方法cookie 属性设置和创建 Shiro 的 cookie 管理对象

//cookie 属性设置
public SimpleCookie rememberMeCookie(){
    SimpleCookie cookie = new SimpleCookie("rememberMe");
    //设置跨域
    //cookie.setDomain(domain);
    cookie.setPath("/");
    cookie.setHttpOnly(true);
    cookie.setMaxAge(30*24*60*60);
    return cookie;
}
//创建 Shiro 的 cookie 管理对象
public CookieRememberMeManager rememberMeManager(){
    CookieRememberMeManager cookieRememberMeManager = new
            CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());

    cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
    return cookieRememberMeManager;
}

然后设置拦截器,再添加一个用户的

//添加存在用户的过滤器(rememberMe)
definition.addPathDefinition("/**","user");

修改controller

@GetMapping("/userLogin")
public String UserLogin(String name, String pwd,
                        @RequestParam(defaultValue = "false") boolean rememberMe,
                        HttpSession session){
    System.out.println("name = " + name+"pwd--->"+pwd);
    //1 获取Subject对象
    Subject subject = SecurityUtils.getSubject();
    //2 封装请求数据到token对象中
    UsernamePasswordToken token = new UsernamePasswordToken(name, pwd,rememberMe);
    //3 调用login方法就行登录验证
    try{
        subject.login(token);
        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";
}

在登录的页面新加上记住我单选框

<div>记住用户:<input type="checkbox" name="rememberMe" value="true"></div>

重启访问:http://localhost:8080/userLoginRm 会被shiro的拦截器拦截到我们配置文件中配的地址/login

image-20230725151338506

image-20230725151440388

关闭浏览器再访问http://localhost:8080/userLoginRm

jh

3.6、用户登陆后登出

有登录就有登出,,Shiro的过滤器就可以实现。

修改登陆后的main.html,加上内容

<a href="/logout">登出</a>

修改Shiro配置类。添加logout过滤器,这里有个坑,登出不要放在代码最后面,不然找不到404

 //配置 Shiro 内置过滤器拦截范围
    @Bean
    public DefaultShiroFilterChainDefinition
    shiroFilterChainDefinition(){
        DefaultShiroFilterChainDefinition definition = new
                DefaultShiroFilterChainDefinition();
        //设置不认证可以访问的资源
        //anon是匿名拦截器:不需要登陆就可以访问的资源
        //authc是登录拦截器:需要登录才能访问
        definition.addPathDefinition("/userLogin","anon");
        definition.addPathDefinition("/login","anon");
        //配置登出过滤器
        definition.addPathDefinition("/logout","logout");
        //设置需要进行登录认证的拦截范围
        definition.addPathDefinition("/**","authc");
        //添加存在用户的过滤器(rememberMe)
        definition.addPathDefinition("/**","user");
        return definition;
    }

测试:登录后进入main页面,点击登出,退出登录,直接跳到登录页面

image-20230725152244912

image-20230725153338888

3.7、授权角色认证

授权

用户登录后,需要验证是否具有指定角色指定权限。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()

授权验证---没有角色无法访问

添加controller方法,添加角色注解

//登录认证验证角色@RequiresRoles("admin")验证有无admin角色
@RequiresRoles("admin")
@GetMapping("/roles")
@ResponseBody
public String userLoginRoles() {
 System.out.println("登录认证验证角色");
 return "验证角色成功";
}

main.html增加内容

<a href="/myController/userLoginRoles">测试授权</a>

修改MyRealm方法

//自定义授权方法:获取当前登录用户权限信息,返回给 Shiro 用来进行授权对比
@Override
protected AuthorizationInfo 
doGetAuthorizationInfo(PrincipalCollection principalCollection) {
 System.out.println("进入自定义授权方法");
 return null;
}

测试,点击测试授权

image-20230725155921080

image-20230725160252465

image-20230725160223523

报错没有admin角色

3.8、授权验证-获取角色进行验证

修改MyRealm方法

//自定义授权方法:获取当前登录用户权限信息,返回给 Shiro 用来进行授权对比
@Override
protected AuthorizationInfo 
doGetAuthorizationInfo(PrincipalCollection principalCollection) {
 System.out.println("进入自定义授权方法");
 //1 创建对象,存储当前登录的用户的权限和角色
 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
 //2 存储角色
 info.addRole("admin");
 //返回
 return info;
}

重启测试,验证成功

image-20230725160949460

真正的角色当然在数据库里面,所以角色是要从数据库拿到的

数据库里有两张表

roles代表角色

image-20230725180258029

role_user代表角色和用户之间的关系,多对多的关系

image-20230725180407880

到UserMapper写getRoles接口

//获取用户的角色
List<String> getRoles(@Param("principal") String principal);

然后去userMapper.xml写sql

<select id="getRoles" resultType="String">
    SELECT r.name
    FROM mybatis.role r
             JOIN mybatis.role_user ur ON r.id= ur.rid
             JOIN mybatis.user u ON ur.uid=u.id
    WHERE u.name= #{principal}
</select>

到UserService实现getRoles方法

public List<String> getRoles(String principal){
    return userMapper.getRoles(principal);
}

MyRealm类修改

//自定义授权,获取当前登录用户权限信息,返回给 Shiro 用来进行授权对比
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("进入自定义授权方法");
    //1 获取当前用户身份信息
    String principal = principalCollection.getPrimaryPrincipal().toString();
    //2 调用接口方法获取用户的角色信息
    List<String> roles = userService.getRoles(principal);
    System.out.println("当前用户角色信息:"+roles);
    //3 创建对象,存储当前登录的用户的权限和角色
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //4 存储角色
    info.addRoles(roles);
    //5 返回
    return info;
}

测试登录后点测试授权

image-20230725183704708

看一下后台的输出,没有问题。

image-20230725183749495

3.9、授权验证-获取权限进行验证

再有权限表和角色权限映射表

CREATE TABLE `permissions` (
 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
 `name` VARCHAR(30) DEFAULT NULL COMMENT '权限名',
 `info` VARCHAR(30) DEFAULT NULL COMMENT '权限信息',
 `desc` VARCHAR(50) DEFAULT NULL COMMENT '描述',
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='权限表';

image-20230725184319435

CREATE TABLE `role_ps` (
 `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
 `rid` BIGINT(20) DEFAULT NULL COMMENT '角色 id',
 `pid` BIGINT(20) DEFAULT NULL COMMENT '权限 id',
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='角色权限映射
表';

image-20230725184455061

到UserMapper写getUserPermission接口

//获取用户权限
List<String> getUserPermission(List<String> permission);

到userMapper.xml写sql

<select id="getUserPermission" resultType="String">
    select per.info
    from mybatis.permissions per
        join mybatis.role_ps rp on per.id = rp.pid
        join mybatis.role r on rp.rid=r.id
        where r.name in 
    <foreach item="item" index="index" collection="permission" open="(" separator="," close=")">
                      #{item}
                  </foreach>
</select>

到UserService实现getUserPermission

public List<String> getUserPermission(@Param("roles") List<String> permission){
    System.out.println("permission = " + permission);
    return userMapper.getUserPermission(permission);
}

修改MyRealm类

//自定义授权,获取当前登录用户权限信息,返回给 Shiro 用来进行授权对比
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("进入自定义授权方法");
    //1 获取当前用户身份信息
    String principal = principalCollection.getPrimaryPrincipal().toString();

    //2 调用接口方法获取用户的角色信息
    List<String> roles = userService.getRoles(principal);
    System.out.println("当前用户角色信息:"+roles);
    //2.1调用接口方法获取到权限信息
    List<String> permissions = userService.getUserPermission(roles);
    System.out.println("当前用户权限信息:"+permissions);
    //3 创建对象,存储当前登录的用户的权限和角色
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //4 存储角色
    info.addRoles(roles);
    //存储权限信息
    info.addStringPermissions(permissions);
    //5 返回
    return info;
}

增加UserPermission的controller

//验证用户的权限信息
@RequiresPermissions("user:delete,user:add,user:update")
@RequestMapping("/permissions")
@ResponseBody
public String Permission(){
    System.out.println("验证用户权限");
    return "用户权限认证成功";
}

main.html再添加验证权限的链接

<br>
<a href="/permissions">测试授权 权限</a>

重启测试,点击测试授权

image-20230725195848653

image-20230725200808484

查看后台,没有问题

image-20230725200825687

3.10、异常处理

创建认证异常处理类,使用@ControllerAdvice 加@ExceptionHandler 实现特殊异 常处理。

在controller下新建异常处理类PermissionsException

@ControllerAdvice
public class PermissionsException {
 @ResponseBody
 @ExceptionHandler(UnauthorizedException.class)
 public String unauthorizedException(Exception ex){
 return "无权限";
 }
 @ResponseBody
 @ExceptionHandler(AuthorizationException.class)
 public String authorizationException(Exception ex){
 return "权限认证失败";
 }
}

启动测试,这次登录李四,分别点击测试授权 角色和权限,就会在前端页面提示无权限。

image-20230726094310984

image-20230726100225057

image-20230726100236964

3.11、前端页面授权验证

不同权限拥有者所能看到页面要有不同的呈现,这里就需要使用thymeleaf模板引擎和Shiro配合了

<!--配置 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 体内容

mian.html修改

<a shiro:hasRole="admin" href="/roles">测试授权角色</a>
<br>
<a shiro:hasPermission="user:delete" href="/permissions">测试授权 权限</a>

测试,张三的有权限所以可以显示全部

image-20230726102238165

李四没有权限所以不能显示

jgf

3.12、缓存工具EhCache

缓存工具EhCache

EhCache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级 容器。可以和大部分Java项目无缝整合,例如:Hibernate中的缓存就是基于EhCache实现 的。 EhCache支持内存和磁盘存储,默认存储在内存中,如内存不够时把缓存数据同步到磁 盘中。EhCache支持基于Filter的Cache实现,也支持Gzip压缩算法.

  • EhCache直接在JVM虚拟机中缓存,速度快,效率高;

  • EhCache缺点是缓存共享麻烦,集群分布式应用使用不方便

EhCache搭建使用

在源项目基础上再创建一个模块ehcacheTest

添加上依赖

<dependencies>
 <dependency>
 <groupId>net.sf.ehcache</groupId>
 <artifactId>ehcache</artifactId>
 <version>2.6.11</version>
 <type>pom</type>
 </dependency>
</dependencies>

添加配置文件ehcache.xml

<?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>

创建测试类,操作缓存

public class TestEH {
 public static void main(String[] args) {
 //获取编译目录下的资源的流对象
 InputStream input = 
TestEH.class.getClassLoader().getResourceAsStream("ehcache.xml");
 //获取 EhCache 的缓存管理对象
 CacheManager cacheManager = new CacheManager(input);
 //获取缓存对象
 Cache cache = cacheManager.getCache("HelloWorldCache");
 //创建缓存数据
 Element element = new Element("name","我是缓存");
 //存入缓存
 cache.put(element);
 //从缓存中取出
 Element element1 = cache.get("name");
 System.out.println(element1.getObjectValue());
 }
}

启动测试

fadfs

3.13、Shiro整合Ehcache

Shiro官方提供了shiro-ehcache,实现了整合EhCache作为Shiro的缓存工具。可以缓 存认证执行的Realm方法,减少对数据库的访问,提高认证效率

添加依赖

<!--Shiro 整合 EhCache-->
<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-ehcache</artifactId>
 <version>1.4.2</version>
</dependency>

在 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>

修改配置类 ShiroConfig增加缓存配置

 @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(){
        //1 创建 defaultWebSecurityManager 对象
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //2 创建加密对象,并设置相关属性
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        //2.1 采用 md5 加密
        matcher.setHashAlgorithmName("MD5");
        //2.2 迭代加密次数
        matcher.setHashIterations(3);
        //3 将加密对象存储到 myRealm 中
        myRealm.setCredentialsMatcher(matcher);
        //4 将 myRealm 存入 defaultWebSecurityManager 对象
        defaultWebSecurityManager.setRealm(myRealm);
        //4.1 设置缓存管理器
        defaultWebSecurityManager.setCacheManager(getEhCacheManager());
        //5、设置记住我remember me
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        //6、返回
        return defaultWebSecurityManager;
    }

//创建缓存管理器
public EhCacheManager getEhCacheManager(){
    EhCacheManager ehCacheManager = new EhCacheManager();
    InputStream is = null;
    try {
        is= ResourceUtils.getInputStreamForPath("classpath:ehcache/*.xml");
    } catch (IOException e) {
        e.printStackTrace();
    }
    CacheManager cacheManager = new CacheManager(is);
    ehCacheManager.setCacheManager(cacheManager);
    return ehCacheManager;
}

启动测试,登录张三,发现后台只是输出了controller输出的结果,并没有查询数据库的信息。

image-20230726153720418

3.14、会话管理

SessionManager由SecurityManager管理。Shiro提供了三种实现

  • DefaultSessionManager:用于JavaSE环境

  • ServletContainerSessionManager:用于web环境,直接使用Servlet容器的会话

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

Session session = SecurityUtils.getSubject().getSession();
session.setAttribute(“key”,”value”)

Controller 中的 request,在 shiro 过滤器中的 doFilerInternal 方法,被包装成 ShiroHttpServletRequest。 SecurityManager 和 SessionManager 会话管理器决定 session 来源于 ServletRequest 还是由 Shiro 管理的会话。 无论是通过 request.getSession 或 subject.getSession 获取到 session,操作 session,两者都是等价的。