# shiro

# 1. 入门概述

# 1.1. 是什么

Apache Shiro 是 Java 安全(权限)框架。

功能:

  • 认证
  • 授权
  • 加密
  • 会话管理
  • 与 Web 集成
  • 缓存

# 1.2. 为什么要用 Shiro

  • 易于使用
  • 全面
  • 灵活
  • 强力支持 Web
  • 兼容性强

# 1.3. 与 Spring Security 的对比

  • Spring Security 基于 Spring,更方便、强大,社区资源也更好
  • Shiro 不依赖 Spring,在集群中 会话 独立于容器

# 1.4. 基本功能

image-20250827210931859

image-20250827212610684

image-20250827212658646

# 2. 基本使用

# 2.1. 环境准备

  1. 创建普通 Java 工程(选择 Maven 构建)

  2. 添加依赖

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

# 2.2. ini 文件

Shiro 获取权限相关信息可以通过数据库,也可以通过 ini 配置文件

位置:

${root}/
  src/main/resources
    shiro.ini

shiro.ini:

[users]
zhangsan=123
lisi=456

# 2.3. 登录认证

# 2.3.1. 登录认证概念

  1. 身份验证

    • 一般需要提供标志信息来表明登录者的身份
    • 比如 email 、 用户名/密码
  2. principals / credentials

    • principals: /ˈprɪnsəpəlz/ , 身份
    • credentials: /krəˈdenʃlz/, 证明
    • 在 Shiro 中,用户需要提供 身份/证明 来验证用户身份
  3. principals

    • 身份,即主体的标识属性
    • 比如: 用户名、邮箱、手机号(唯一即可)
    • 但只有一个 primary principals
  4. credentials

    • 证明 / 凭证
    • 只有主体知道的安全值
    • 比如: 密码 、数字证书
  5. 最常见的 principals/credentials 组合就是 用户名/密码

# 2.3.2. 登录认证基本流程

  1. 收集用户 身份/凭证,比如 用户名/密码
  2. 调用 Subject.login() 登录
    • 登录失败则抛异常 AuthenticationException
  3. 创建自定义 Realm 类
    • 继承 AuthenticatingRealm 类
    • 实现 doGetAuthenticationInfo() 方法

image-20250827220604792

# 2.3.3. 登录认证实例

// 1. 初始化: 创建 securityManager
IniSecurityManagerFactory securityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = securityManagerFactory.getInstance();
// securityManager 放入 SecurityUtils
SecurityUtils.setSecurityManager(securityManager);

// 2. 获取 Subject 对象
Subject subject = SecurityUtils.getSubject();

// 3. 创建 token 对象
AuthenticationToken token = new UsernamePasswordToken("zhangsan", "123");

try {
    // 4. 登录
    subject.login(token);

    System.out.println("登录成功");
} catch (UnknownAccountException e) {
    System.out.println("登录失败: 用户名不存在");
} catch (IncorrectCredentialsException e) {
    System.out.println("登录失败: 密码错误");
} catch (AuthenticationException e) {
    System.out.println("登录失败: 未知错误");
}

# 2.4. 角色 和 授权

# 2.4.1. 授权概念

  1. 授权

    • 访问控制,即在应用中控制谁能访问的哪些资源
    • 关键对象:主体(subject)、资源(resource)、权限(Permission)、角色(role)
  2. 主体(subject)

    • 访问应用的用户
    • 在 shiro 中,Subject 代表该用户
    • 用户只有授权后才允许访问相应资源
  3. 资源(resource)

    • 用户可以访问的 URL
  4. 权限(permission)

    • 在应用中用户能不能访问某个资源
  5. 角色(role)

    • 权限的集合
    • 一般会赋予用户角色,不同的角色拥有一组不同的权限

# 2.4.2. 授权方式

编程:

if (subject.hasRole("admin")) {
    // 有权限
} else {
    // 无权限
}

注解:

// 在方法上放置相应注解,没有权限则抛出相应异常
@RequiresRoles("admin")
@PostMapping("/user/getById")
public JsonResult getById(String id) {
    // 有权限
}

# 2.4.3. 授权流程

  1. 先调用 subject.hasRole() / subject.isPermitted()

    • 委托给 SecurityManager
    • SecurityManager 委托给 Authorizer
  2. Authorizer 是真正的授权者

    • 比如调用 subject.isPermitted("user:insert")
    • PermissionResolver 把 "user:insert" 转为相应 Permission 实例
  3. 在授权前,会调用相应的 Realm 获取 subject 的 角色和权限

  4. Authorizer 会判断 Realm 的 角色/权限 是否与传入的匹配

# 2.4.4. 授权实例

shiro.ini :

[users]
zhangsan=123,role1,role2
lisi=456

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

代码:

try {
    // 4. 登录
    subject.login(token);
    System.out.println("登录成功");
} catch (UnknownAccountException e) {
    System.out.println("登录失败: 用户名不存在");
} catch (IncorrectCredentialsException e) {
    System.out.println("登录失败: 密码错误");
} catch (AuthenticationException e) {
    System.out.println("登录失败: 未知错误");
}

// 5. 判断角色: 用户 是否 有指定的角色
boolean hasRole1 = subject.hasRole("role1");
System.out.println("hasRole1 = " + hasRole1);

// 6. 判断权限: 用户 是否 有指定的权限
boolean isPermittedOfUserInsert = subject.isPermitted("user:insert");
System.out.println("isPermittedOfUserInsert = " + isPermittedOfUserInsert);

// 检查权限,没有则抛异常(UnauthorizedException)
subject.checkPermission("user:insert2");

# 2.5. MD5 加密

使用 Shiro 的加密工具类,实现方便的加密

String password = "123456";

// md5 加密
Md5Hash md5Hash1 = new Md5Hash(password);
System.out.println("md5Hash1 = \t" + md5Hash1.toHex());

// md5 带盐加密
Md5Hash md5Hash2 = new Md5Hash(password, "salt-test");
System.out.println("md5Hash2 = \t" + md5Hash2.toHex());

// md5 带盐 迭代 加密
Md5Hash md5Hash3 = new Md5Hash(password, "salt-test", 3);
System.out.println("md5Hash3 = \t" + md5Hash3.toHex());

// (使用 Md5Hash 的父类) md5 带盐 迭代 加密
SimpleHash md5Hash4 = new SimpleHash("MD5", password, "salt-test", 3);
System.out.println("md5Hash4 = \t" + md5Hash4.toHex());

# 2.6. 自定义 Ream

# 3. 与 Spring Boot 整合

# 3.1. 框架整合

目录:

shiro_02_spring/src/
    main/java
        org.example
            mapper/
            ShiroApplication.java
    resources/
        application.yml

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>shiro_02_spring</artifactId>
    <version>1.0-SNAPSHOT</version>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>

    <!--JDK 的版本-->
    <properties>
        <java.version>8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml:

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro_db?serverTimezone=UTC
    username: root
    password: 123456
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:/mapper/**/*.xml

shiro:
  loginUrl: /login

ShiroApplication.java:

package org.example;

@SpringBootApplication
@MapperScan("org.example.mapper")
public class ShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShiroApplication.class, args);
    }
}

# 3.2. 登录认证实现

# 3.2.1. 数据库

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint NOT NULL,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `role_id` bigint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

INSERT INTO `sys_user` VALUES (1, 'ZhangSan', '3dda874af7bb9cb770722e15ea03c1d4', NULL);
INSERT INTO `sys_user` VALUES (2, 'LiSi', '83b5be425a1adcfba658d228716593e3', NULL);

# 3.2.2. 实体

package org.example.entity;

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user")
public class User {
    private Long id;
    private String username;
    private String password;
    private Long roleId;
}

# 3.2.3. Mapper

package org.example.mapper;

@Repository
public interface UserMapper extends BaseMapper<User> {
}

# 3.2.4. Service

package org.example.service;

public interface UserService {
    User getUserInfoByUsername(String username);
}
package org.example.service.impl;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserInfoByUsername(String username) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(username != null, User::getUsername,  username);

        User user = userMapper.selectOne(queryWrapper);

        return user;
    }
}

# 3.2.5. 自定义 Realm

package org.example.realm;

@Component
public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

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

    // 登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 身份(用户名)
        Object principal = authenticationToken.getPrincipal();
        String username = principal.toString();

        // 获取数据库中的密码
        User user = userService.getUserInfoByUsername(username);

        if (user == null) {
            return null;
        }

        String password = user.getPassword();

        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                principal,
                password,
                ByteSource.Util.bytes("salt-test"), // 盐值
                username
        );

        return authenticationInfo;
    }
}

# 3.2.6. 配置 Shiro

package org.example.config;

@Configuration
public class ShiroConfig {
    @Autowired
    private MyShiroRealm myShiroRealm;

    // 配置 SecurityManager
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        // 加密对象,设置密码加密方式
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // md5 算法
        matcher.setHashAlgorithmName("md5");
        // 迭代 3 次
        matcher.setHashIterations(3);

        myShiroRealm.setCredentialsMatcher(matcher);

        defaultWebSecurityManager.setRealm(myShiroRealm);

        return defaultWebSecurityManager;
    }

    // 配置 Shiro 内置过滤器 拦截的范围
    @Bean
    public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();

        // 匿名
        definition.addPathDefinition("/sysUser/login", "anon");

        // 认证
        definition.addPathDefinition("/**", "authc");

        return definition;
    }
}

# 3.2.7. controller

package org.example.controller;

@RestController
public class UserController {
    @GetMapping("/sysUser/login")
    public String login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();

        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            subject.login(token);
            return "Login success";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "Login fail";
        }
    }
}

# 3.2.8. 测试

GET http://localhost:8080/sysUser/login?username=ZhangSan&password=123
GET http://localhost:8080/sysUser/login?username=LiSi&password=456

# 3.3. 多个 realm

# 3.4. remember me

# 3.5. 登出

// 配置 Shiro 内置过滤器 拦截的范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();

    // 匿名
    definition.addPathDefinition("/sysUser/login", "anon");

    // 登出
    definition.addPathDefinition("/logout", "logout");

    // 认证
    definition.addPathDefinition("/**", "authc");

    return definition;
}

# 3.6. 授权、角色认证

# 3.6.1. 授权

用户登录后,需要验证其 角色、权限

涉及方法: AuthorizingRealm.doGetAuthorizationInfo()

触发判断的方式: 调用的方法上 使用了 @RequiresXxx 注解

# 3.6.2. 注解

一般在 控制器方法 上使用注解

  1. @RequiresAuthentication

    • 是否登录
    • 等同 subject.isAuthenticated()
  2. @RequiresUser

    • 是否被记忆
    • 等同 subject.isRemembered()
  3. @RequiresGuest

    • 是否是游客
    • subject.getPrincipal() 为 null
  4. @RequiresRoles({"admin"})

    • 是否是 admin 角色
  5. @RequiresPermissions({"read:file", "write:file"})

    • 必须同时有 "read:file", "write:file" 权限

# 3.6.3. 角色验证

MyShiroRealm:

package org.example.realm;

@Component
public class MyShiroRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1. 获取用户名
        String username = principalCollection.getPrimaryPrincipal().toString();

        System.out.println("doGetAuthorizationInfo. username = " + username);

        // 2. 根据用户名查询角色
        List<String> roleList = Arrays.asList("admin", "employee" /*, "boss"*/);

        // 3. 将 角色 添加进 AuthorizationInfo 并返回
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(roleList);

        return simpleAuthorizationInfo;
    }

    // 登录
    // ...
}

UserController:

@GetMapping("/sysUser/callMethodRequiresAdminRole")
@RequiresRoles("admin")
@ResponseBody
public String callMethodRequiresAdminRole() {
    System.out.println("访问需要 admin 角色的方法");
    return "OK";
}

// 无 boss 角色,则抛出 AuthorizationException 异常
@GetMapping("/sysUser/callMethodRequiresBossRole")
@RequiresRoles("boss")
@ResponseBody
public String callMethodRequiresBossRole() {
    System.out.println("访问需要 boss 角色的方法");
    return "OK";
}

# 3.6.4. 权限验证

MyShiroRealm:

package org.example.realm;

@Component
public class MyShiroRealm extends AuthorizingRealm {

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1. 获取用户名
        String username = principalCollection.getPrimaryPrincipal().toString();

        System.out.println("doGetAuthorizationInfo. username = " + username);

        // 2.1 根据用户名查询角色
        List<String> roleList = Arrays.asList("admin", "employee" /*, "boss"*/);

        // 2.2 根据角色查询权限
        List<String> permissionList = Arrays.asList("user:add", "user:edit"/*, "user:delete"*/);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 3.1 将 角色 添加进 AuthorizationInfo
        simpleAuthorizationInfo.addRoles(roleList);

        // 3.2 将 角色 添加进 AuthorizationInfo
        simpleAuthorizationInfo.addStringPermissions(permissionList);

        return simpleAuthorizationInfo;
    }

    // 登录
    // ...
}

UserController:

@GetMapping("/sysUser/callMethodRequiresAddPermission")
@RequiresPermissions("user:add")
@ResponseBody
public String callMethodRequiresAddPermission() {
    System.out.println("访问需要 user:add 权限的方法");
    return "OK";
}

// 无 user:delete 权限,则抛出 AuthorizationException 异常
@GetMapping("/sysUser/callMethodRequiresDeletePermission")
@RequiresPermissions("user:delete")
@ResponseBody
public String callMethodRequiresDeletePermission() {
    System.out.println("访问需要 user:delete 权限的方法");
    return "OK";
}

# 3.6.5. 全局异常处理

@ControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(AuthorizationException.class)
    public String handleAuthorizationException() {
        return "无权限";
    }
}

# 3.7. 缓存

说明:

  • 每次调用需要 授权 的方法,都会执行一次 AuthorizingRealm.doGetAuthorizationInfo()
  • 使用缓存 defaultWebSecurityManager.setCacheManager() 可以避免频繁获取 授权

# 3.8. 会话管理

说明:

  • 在 shiro 过滤器中对 request 做了封装(ShiroHttpServletRequest)
  • 通过 subject.getSession() / request.getSession() 获取到的 session, 两者是等价的

示例:

@GetMapping("/sysUser/login")
public String login(String username, String password, HttpSession session, HttpServletRequest request) {
    Subject subject = SecurityUtils.getSubject();

    Session sessionFromShiro = subject.getSession();
    HttpSession sessionFromRequest = request.getSession();

    System.out.println("request = " + request);
    //=> org.apache.shiro.web.servlet.ShiroHttpServletRequest

    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    User user = userService.getUserInfoByUsername(username);

    session.setAttribute("user", user);

    System.out.println("sessionFromShiro.getAttribute(user) = " + sessionFromShiro.getAttribute("user"));
    System.out.println("sessionFromRequest.getAttribute(user) = " + sessionFromRequest.getAttribute("user"));

    try {
        subject.login(token);
        return "redirect:/main";
    } catch (AuthenticationException e) {
        e.printStackTrace();
        return "login";
    }
}
本章目录