Spring Boot整合Shiro实现权限认证

本文详细讲解如何在Spring Boot项目中整合Apache Shiro安全框架,实现完整的认证授权功能,包含环境搭建、核心概念解析、完整代码实现及常见问题解决方案。


一、环境准备

1.1 开发环境要求

组件 版本要求 说明
JDK 1.8+ 推荐1.8版本,兼容性最佳
Spring Boot 2.7.x 稳定版本,兼容性好
MyBatis-Plus 3.5.x 增强版MyBatis,简化开发
Apache Shiro 1.10.x 最新稳定版
MySQL 5.7+ / 8.0+ 数据库存储
Maven 3.6+ 项目构建工具

1.2 Maven依赖配置

pom.xml 中添加以下核心依赖:

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>com.example</groupId>
    <artifactId>springboot-shiro-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    
    <name>Spring Boot Shiro Demo</name>
    <description>Spring Boot整合Shiro实现权限认证示例</description>
    
    <!-- Spring Boot父工程依赖管理 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
        <shiro.version>1.10.1</shiro.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web Starter - 提供Web开发基础功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Spring Boot Thymeleaf - 模板引擎,用于前端页面渲染 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- Shiro Spring Boot Starter - Shiro核心整合包 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        
        <!-- Shiro整合Thymeleaf - 在Thymeleaf模板中使用Shiro标签 -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.1.0</version>
        </dependency>
        
        <!-- MyBatis-Plus - 增强版MyBatis,简化数据库操作 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- MyBatis-Plus代码生成器 - 自动生成代码(可选) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- MySQL驱动 - 连接MySQL数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        
        <!-- Druid数据源 - 阿里巴巴数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.18</version>
        </dependency>
        
        <!-- Lombok - 简化实体类代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Spring Boot Test - 单元测试支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <!-- Hutool工具包 - Java工具类库 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Spring Boot Maven插件 - 打包可执行jar -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1.3 application.yml配置文件

yaml 复制代码
# 服务器端口配置
server:
  port: 8080
  servlet:
    context-path: /shiro-demo

# Spring相关配置
spring:
  # 数据源配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    druid:
      # 初始连接数
      initial-size: 5
      # 最小空闲连接数
      min-idle: 5
      # 最大活跃连接数
      max-active: 20
      # 获取连接超时时间(毫秒)
      max-wait: 60000
      # 配置间隔多久进行一次检测,检测需要关闭的空闲连接(毫秒)
      time-between-eviction-runs-millis: 60000
      # 配置连接在池中最小生存的时间(毫秒)
      min-evictable-idle-time-millis: 300000
      # 验证连接有效性的SQL
      validation-query: SELECT 1
      # 申请连接时执行validationQuery检测连接是否有效
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开PSCache,并指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置监控统计拦截的filters
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # Druid监控页面配置
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        reset-enable: false
        login-username: admin
        login-password: admin
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"

  # Thymeleaf模板引擎配置
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    cache: false
    mode: HTML

# MyBatis-Plus配置
mybatis-plus:
  # Mapper XML文件位置
  mapper-locations: classpath:mapper/*.xml
  # 实体类扫描包路径
  type-aliases-package: com.example.shiro.entity
  # 全局配置
  global-config:
    # 数据库相关配置
    db-config:
      # 主键类型:AUTO自增
      id-type: auto
      # 逻辑删除字段
      logic-delete-field: deleted
      # 逻辑删除值(已删除)
      logic-delete-value: 1
      # 逻辑未删除值(未删除)
      logic-not-delete-value: 0
  # 配置项
  configuration:
    # 驼峰命名转换
    map-underscore-to-camel-case: true
    # 日志实现
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# Shiro配置
shiro:
  # 登录URL
  loginUrl: /login
  # 登录成功后跳转URL
  successUrl: /index
  # 未授权跳转URL
  unauthorizedUrl: /unauthorized
  # 允许匿名访问的URL
  anon:
    - /login
    - /css/**
    - /js/**
    - /images/**
    - /druid/**
  # 会话管理
  session:
    # 会话超时时间(毫秒),30分钟
    timeout: 1800000
    # 定时清理失效会话
    scheduler-interval: 1800000

# 日志配置
logging:
  level:
    com.example.shiro: debug
    org.apache.shiro: info
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

二、核心概念解析

2.1 Shiro架构概述

Shiro是一个功能强大且易于使用的Java安全框架,提供认证、授权、加密和会话管理功能。

2.2 核心组件说明

组件 说明 作用
Subject 主体 表示当前"用户",可以是人、第三方服务、定时任务等,Shiro抽象出的核心概念
SecurityManager 安全管理器 Shiro的核心,协调各个组件工作,类似SpringMVC的DispatcherServlet
Realm 领域 连接Shiro与安全数据源的桥梁,负责获取用户数据(认证信息、权限信息)
Authenticator 认证器 负责处理用户认证逻辑
Authorizer 授权器 负责处理用户授权逻辑
SessionManager 会话管理器 管理用户会话的生命周期
CacheManager 缓存管理器 缓存用户、权限等信息,提高性能

2.3 Shiro认证流程

复制代码
┌─────────┐    1.提交用户名密码    ┌─────────────┐    2.创建Token    ┌─────────┐
│  用户   │ ───────────────────> │   Subject   │ ──────────────> │  Token  │
└─────────┘                      └─────────────┘                 └─────────┘
                                                                      │
                                                                      ▼
┌─────────────┐    6.返回认证结果    ┌─────────────┐    3.调用认证    ┌─────────────┐
│  返回结果   │ <────────────────── │Authenticator│ <───────────── │Security     │
└─────────────┘                    └─────────────┘                 │ Manager     │
   ▲      │                                                       └─────────────┘
   │      5.验证用户信息                                                │
   │      └──────────────────────────────┐                             │
   │                                     ▼                             │
   │                         ┌─────────────┐    4.查询用户数据        │
   │                         │    Realm    │ <─────────────────────┘
   │                         └─────────────┘
   │
   └── 7.将认证结果存入Session

2.4 认证与授权的区别

类型 英文 说明 示例
认证 Authentication 验证用户身份是否合法 登录系统,验证用户名密码
授权 Authorization 验证用户是否有权限执行操作 删除用户、查看报表等操作权限

三、完整实现步骤

3.1 数据库表设计

sql 复制代码
-- 创建数据库
CREATE DATABASE IF NOT EXISTS shiro_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

USE shiro_demo;

-- 用户表
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
    id BIGINT AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    password VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
    real_name VARCHAR(50) COMMENT '真实姓名',
    email VARCHAR(100) COMMENT '邮箱',
    phone VARCHAR(20) COMMENT '手机号',
    status TINYINT DEFAULT 1 COMMENT '状态(1:正常 0:禁用)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted TINYINT DEFAULT 0 COMMENT '删除标记(0:未删除 1:已删除)',
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 角色表
DROP TABLE IF EXISTS sys_role;
CREATE TABLE sys_role (
    id BIGINT AUTO_INCREMENT COMMENT '角色ID',
    role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
    role_code VARCHAR(50) NOT NULL COMMENT '角色编码',
    description VARCHAR(200) COMMENT '角色描述',
    status TINYINT DEFAULT 1 COMMENT '状态(1:正常 0:禁用)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted TINYINT DEFAULT 0 COMMENT '删除标记(0:未删除 1:已删除)',
    PRIMARY KEY (id),
    UNIQUE KEY uk_role_code (role_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- 权限表
DROP TABLE IF EXISTS sys_permission;
CREATE TABLE sys_permission (
    id BIGINT AUTO_INCREMENT COMMENT '权限ID',
    permission_name VARCHAR(50) NOT NULL COMMENT '权限名称',
    permission_code VARCHAR(100) NOT NULL COMMENT '权限编码',
    permission_type TINYINT DEFAULT 1 COMMENT '权限类型(1:菜单 2:按钮)',
    parent_id BIGINT DEFAULT 0 COMMENT '父权限ID',
    url VARCHAR(200) COMMENT '权限URL',
    description VARCHAR(200) COMMENT '权限描述',
    status TINYINT DEFAULT 1 COMMENT '状态(1:正常 0:禁用)',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted TINYINT DEFAULT 0 COMMENT '删除标记(0:未删除 1:已删除)',
    PRIMARY KEY (id),
    UNIQUE KEY uk_permission_code (permission_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

-- 用户角色关联表
DROP TABLE IF EXISTS sys_user_role;
CREATE TABLE sys_user_role (
    id BIGINT AUTO_INCREMENT COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    role_id BIGINT NOT NULL COMMENT '角色ID',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_user_role (user_id, role_id),
    KEY idx_user_id (user_id),
    KEY idx_role_id (role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 角色权限关联表
DROP TABLE IF EXISTS sys_role_permission;
CREATE TABLE sys_role_permission (
    id BIGINT AUTO_INCREMENT COMMENT '主键ID',
    role_id BIGINT NOT NULL COMMENT '角色ID',
    permission_id BIGINT NOT NULL COMMENT '权限ID',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_role_permission (role_id, permission_id),
    KEY idx_role_id (role_id),
    KEY idx_permission_id (permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';

-- 初始化数据:管理员用户
INSERT INTO sys_user (username, password, real_name, email, phone, status) 
VALUES ('admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '系统管理员', 'admin@example.com', '13800138000', 1);
-- 密码为: admin123

-- 初始化数据:普通用户
INSERT INTO sys_user (username, password, real_name, email, phone, status) 
VALUES ('user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '普通用户', 'user@example.com', '13800138001', 1);
-- 密码为: admin123

-- 初始化数据:角色
INSERT INTO sys_role (role_name, role_code, description) VALUES 
('超级管理员', 'ROLE_ADMIN', '拥有系统所有权限'),
('普通用户', 'ROLE_USER', '基础用户角色');

-- 初始化数据:权限
INSERT INTO sys_permission (permission_name, permission_code, permission_type, parent_id, url, description) VALUES 
('用户管理', 'user:manage', 1, 0, '/user', '用户管理模块'),
('用户查询', 'user:query', 2, 1, '/user/list', '查询用户列表'),
('用户新增', 'user:add', 2, 1, '/user/add', '新增用户'),
('用户修改', 'user:edit', 2, 1, '/user/edit', '修改用户'),
('用户删除', 'user:delete', 2, 1, '/user/delete', '删除用户'),
('角色管理', 'role:manage', 1, 0, '/role', '角色管理模块'),
('角色查询', 'role:query', 2, 6, '/role/list', '查询角色列表'),
('角色新增', 'role:add', 2, 6, '/role/add', '新增角色'),
('系统设置', 'system:setting', 1, 0, '/system', '系统设置模块');

-- 初始化数据:用户角色关联
INSERT INTO sys_user_role (user_id, role_id) VALUES 
(1, 1),  -- admin用户拥有超级管理员角色
(2, 2);  -- user用户拥有普通用户角色

-- 初始化数据:角色权限关联
INSERT INTO sys_role_permission (role_id, permission_id) VALUES 
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5),  -- 管理员拥有用户管理所有权限
(1, 6), (1, 7), (1, 8),                    -- 管理员拥有角色管理权限
(1, 9),                                    -- 管理员拥有系统设置权限
(2, 2);                                    -- 普通用户只有用户查询权限

四、代码示例

4.1 实体类

SysUser.java - 用户实体类

java 复制代码
package com.example.shiro.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 用户实体类
 * 对应数据库表:sys_user
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_user")  // 指定对应的数据库表名
public class SysUser implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 用户ID
     * 主键,自增
     */
    @TableId(value = "id", type = IdType.AUTO)  // 主键策略:自增
    private Long id;
    
    /**
     * 用户名
     * 登录时使用的唯一标识
     */
    @TableField("username")  // 对应数据库字段名
    private String username;
    
    /**
     * 密码
     * 使用BCrypt加密存储
     */
    private String password;
    
    /**
     * 真实姓名
     */
    @TableField("real_name")
    private String realName;
    
    /**
     * 邮箱
     */
    private String email;
    
    /**
     * 手机号
     */
    private String phone;
    
    /**
     * 状态
     * 1:正常, 0:禁用
     */
    private Integer status;
    
    /**
     * 创建时间
     * 插入时自动填充
     */
    @TableField(fill = FieldFill.INSERT)  // 插入时自动填充该字段
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     * 插入和更新时自动填充
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)  // 插入和更新时自动填充
    private LocalDateTime updateTime;
    
    /**
     * 删除标记
     * 0:未删除, 1:已删除
     * 逻辑删除字段
     */
    @TableLogic  // 标记为逻辑删除字段
    private Integer deleted;
}

SysRole.java - 角色实体类

java 复制代码
package com.example.shiro.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 角色实体类
 * 对应数据库表:sys_role
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_role")
public class SysRole implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 角色ID
     * 主键,自增
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    /**
     * 角色名称
     * 例如:超级管理员、普通用户
     */
    @TableField("role_name")
    private String roleName;
    
    /**
     * 角色编码
     * 唯一标识,例如:ROLE_ADMIN、ROLE_USER
     */
    @TableField("role_code")
    private String roleCode;
    
    /**
     * 角色描述
     */
    private String description;
    
    /**
     * 状态
     * 1:正常, 0:禁用
     */
    private Integer status;
    
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    /**
     * 删除标记
     */
    @TableLogic
    private Integer deleted;
}

SysPermission.java - 权限实体类

java 复制代码
package com.example.shiro.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 权限实体类
 * 对应数据库表:sys_permission
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_permission")
public class SysPermission implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 权限ID
     * 主键,自增
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    /**
     * 权限名称
     * 例如:用户管理、角色管理
     */
    @TableField("permission_name")
    private String permissionName;
    
    /**
     * 权限编码
     * 用于Shiro权限校验,例如:user:add、user:delete
     */
    @TableField("permission_code")
    private String permissionCode;
    
    /**
     * 权限类型
     * 1:菜单, 2:按钮
     */
    @TableField("permission_type")
    private Integer permissionType;
    
    /**
     * 父权限ID
     * 0表示顶级权限
     */
    @TableField("parent_id")
    private Long parentId;
    
    /**
     * 权限对应的URL
     * 用于前端路由或接口拦截
     */
    private String url;
    
    /**
     * 权限描述
     */
    private String description;
    
    /**
     * 状态
     * 1:正常, 0:禁用
     */
    private Integer status;
    
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    /**
     * 删除标记
     */
    @TableLogic
    private Integer deleted;
}

SysUserRole.java - 用户角色关联实体类

java 复制代码
package com.example.shiro.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 用户角色关联实体类
 * 对应数据库表:sys_user_role
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@TableName("sys_user_role")
public class SysUserRole implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    /**
     * 用户ID
     */
    @TableField("user_id")
    private Long userId;
    
    /**
     * 角色ID
     */
    @TableField("role_id")
    private Long roleId;
    
    /**
     * 创建时间
     */
    @TableField(value = "create_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
    private LocalDateTime createTime;
}

SysRolePermission.java - 角色权限关联实体类

java 复制代码
package com.example.shiro.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 角色权限关联实体类
 * 对应数据库表:sys_role_permission
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@TableName("sys_role_permission")
public class SysRolePermission implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 主键ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    /**
     * 角色ID
     */
    @TableField("role_id")
    private Long roleId;
    
    /**
     * 权限ID
     */
    @TableField("permission_id")
    private Long permissionId;
    
    /**
     * 创建时间
     */
    @TableField(value = "create_time", fill = com.baomidou.mybatisplus.annotation.FieldFill.INSERT)
    private LocalDateTime createTime;
}

4.2 Mapper接口

SysUserMapper.java

java 复制代码
package com.example.shiro.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shiro.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 用户Mapper接口
 * 继承MyBatis-Plus的BaseMapper,获得基础CRUD方法
 * 
 * @author example
 * @since 2024-01-01
 */
@Mapper  // 标记为MyBatis的Mapper接口
public interface SysUserMapper extends BaseMapper<SysUser> {
    
    /**
     * 根据用户名查询用户信息
     * 
     * @param username 用户名
     * @return 用户实体对象,如果不存在返回null
     */
    @Select("SELECT * FROM sys_user WHERE username = #{username} AND deleted = 0")
    SysUser selectByUsername(@Param("username") String username);
    
    /**
     * 根据用户ID查询用户角色列表
     * 
     * @param userId 用户ID
     * @return 角色编码列表,例如:["ROLE_ADMIN", "ROLE_USER"]
     */
    @Select("SELECT sr.role_code " +
            "FROM sys_user_role sur " +
            "LEFT JOIN sys_role sr ON sur.role_id = sr.id " +
            "WHERE sur.user_id = #{userId} AND sr.deleted = 0 AND sr.status = 1")
    List<String> selectRolesByUserId(@Param("userId") Long userId);
    
    /**
     * 根据用户ID查询用户权限列表
     * 通过用户角色关联表和角色权限关联表查询所有权限
     * 
     * @param userId 用户ID
     * @return 权限编码列表,例如:["user:add", "user:delete", "role:query"]
     */
    @Select("SELECT DISTINCT sp.permission_code " +
            "FROM sys_user_role sur " +
            "LEFT JOIN sys_role_permission srp ON sur.role_id = srp.role_id " +
            "LEFT JOIN sys_permission sp ON srp.permission_id = sp.id " +
            "WHERE sur.user_id = #{userId} " +
            "AND sp.deleted = 0 AND sp.status = 1")
    List<String> selectPermissionsByUserId(@Param("userId") Long userId);
}

4.3 Service层

SysUserService.java

java 复制代码
package com.example.shiro.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.shiro.entity.SysUser;

import java.util.List;

/**
 * 用户Service接口
 * 定义用户相关的业务方法
 * 
 * @author example
 * @since 2024-01-01
 */
public interface SysUserService extends IService<SysUser> {
    
    /**
     * 根据用户名查询用户信息
     * 
     * @param username 用户名
     * @return 用户实体对象,如果不存在返回null
     */
    SysUser getUserByUsername(String username);
    
    /**
     * 根据用户ID查询用户角色列表
     * 
     * @param userId 用户ID
     * @return 角色编码列表
     */
    List<String> getRolesByUserId(Long userId);
    
    /**
     * 根据用户ID查询用户权限列表
     * 
     * @param userId 用户ID
     * @return 权限编码列表
     */
    List<String> getPermissionsByUserId(Long userId);
}

SysUserServiceImpl.java

java 复制代码
package com.example.shiro.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.shiro.entity.SysUser;
import com.example.shiro.mapper.SysUserMapper;
import com.example.shiro.service.SysUserService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 用户Service实现类
 * 实现用户相关的业务逻辑
 * 
 * @author example
 * @since 2024-01-01
 */
@Service  // 标记为Spring的Service组件
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> 
        implements SysUserService {
    
    /**
     * 根据用户名查询用户信息
     * 
     * @param username 用户名
     * @return 用户实体对象,如果不存在返回null
     */
    @Override
    public SysUser getUserByUsername(String username) {
        // 调用Mapper层方法查询用户
        return baseMapper.selectByUsername(username);
    }
    
    /**
     * 根据用户ID查询用户角色列表
     * 
     * @param userId 用户ID
     * @return 角色编码列表
     */
    @Override
    public List<String> getRolesByUserId(Long userId) {
        // 调用Mapper层方法查询角色
        return baseMapper.selectRolesByUserId(userId);
    }
    
    /**
     * 根据用户ID查询用户权限列表
     * 
     * @param userId 用户ID
     * @return 权限编码列表
     */
    @Override
    public List<String> getPermissionsByUserId(Long userId) {
        // 调用Mapper层方法查询权限
        return baseMapper.selectPermissionsByUserId(userId);
    }
}

4.4 自定义Realm实现

CustomRealm.java - 核心认证授权逻辑

java 复制代码
package com.example.shiro.realm;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.shiro.entity.SysUser;
import com.example.shiro.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

/**
 * 自定义Realm类
 * 负责用户的认证和授权逻辑
 * 继承AuthorizingRealm,实现认证和授权两个方法
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j  // Lombok日志注解
public class CustomRealm extends AuthorizingRealm {
    
    @Autowired  // 自动注入UserService
    private SysUserService sysUserService;
    
    /**
     * 授权方法
     * 当用户访问需要权限的资源时,Shiro会自动调用此方法
     * 用于获取用户的角色和权限信息
     * 
     * @param principals PrincipalCollection,包含用户认证信息
     * @return AuthorizationInfo,包含用户的角色和权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("开始执行授权逻辑...");
        
        // 从PrincipalCollection中获取主身份信息(即用户名)
        // 这里我们之前在认证时放入的是SysUser对象
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        
        // 创建SimpleAuthorizationInfo对象用于存储授权信息
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        
        // 查询用户角色列表
        List<String> roles = sysUserService.getRolesByUserId(user.getId());
        log.info("用户[{}]的角色列表: {}", user.getUsername(), roles);
        
        // 将角色添加到授权信息中
        // Shiro支持基于角色的访问控制(RBAC)
        if (!CollectionUtils.isEmpty(roles)) {
            authorizationInfo.addRoles(roles);
        }
        
        // 查询用户权限列表
        List<String> permissions = sysUserService.getPermissionsByUserId(user.getId());
        log.info("用户[{}]的权限列表: {}", user.getUsername(), permissions);
        
        // 将权限添加到授权信息中
        // Shiro支持基于权限的访问控制
        if (!CollectionUtils.isEmpty(permissions)) {
            authorizationInfo.addStringPermissions(permissions);
        }
        
        log.info("授权逻辑执行完成");
        return authorizationInfo;
    }
    
    /**
     * 认证方法
     * 当用户登录时,Shiro会自动调用此方法
     * 用于验证用户身份是否合法
     * 
     * @param token AuthenticationToken,包含用户提交的认证信息(用户名、密码等)
     * @return AuthenticationInfo,包含从数据库查询到的用户信息
     * @throws AuthenticationException 认证失败时抛出的异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        log.info("开始执行认证逻辑...");
        
        // 从token中获取用户名
        // token是Subject.login()时传入的UsernamePasswordToken
        String username = (String) token.getPrincipal();
        
        // 根据用户名从数据库查询用户信息
        SysUser user = sysUserService.getUserByUsername(username);
        
        // 如果用户不存在,抛出UnknownAccountException异常
        if (user == null) {
            log.error("用户[{}]不存在", username);
            throw new UnknownAccountException("用户名或密码错误");
        }
        
        // 检查用户状态,如果被禁用则抛出DisabledAccountException
        if (user.getStatus() == 0) {
            log.error("用户[{}]已被禁用", username);
            throw new DisabledAccountException("账号已被禁用");
        }
        
        log.info("用户[{}]认证信息查询成功", username);
        
        // 创建SimpleAuthenticationInfo对象返回认证信息
        // 参数1: principal(身份信息),通常放用户对象,方便后续获取
        // 参数2: credentials(凭证),即数据库中的密码
        // 参数3: realmName,使用getName()方法获取当前Realm的名称
        // 注意:密码比对由Shiro自动完成,我们只需提供正确的密码
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,              // principal: 放入完整的用户对象,方便后续获取用户信息
                user.getPassword(), // credentials: 数据库中的加密密码
                getName()           // realmName: 当前Realm的名称
        );
        
        log.info("认证逻辑执行完成");
        return authenticationInfo;
    }
}

4.5 Shiro配置类

ShiroConfig.java

java 复制代码
package com.example.shiro.config;

import com.example.shiro.realm.CustomRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Shiro配置类
 * 负责配置Shiro的核心组件和过滤器链
 * 
 * @author example
 * @since 2024-01-01
 */
@Configuration  // 标记为Spring配置类
public class ShiroConfig {
    
    /**
     * 配置密码匹配器
     * 用于在认证时比对用户提交的密码和数据库中的密码
     * 
     * @return HashedCredentialsMatcher对象
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        // 创建HashedCredentialsMatcher对象
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        
        // 设置加密算法为BCrypt
        // BCrypt是一种单向哈希算法,每次加密结果都不同,但可以验证是否匹配
        matcher.setHashAlgorithmName("BCrypt");
        
        // 设置哈希迭代次数,BCrypt算法内部已包含迭代,设置为1即可
        matcher.setHashIterations(1);
        
        // 设置是否存储十六进制编码
        // BCrypt不需要此配置
        matcher.setStoredCredentialsHexEncoded(false);
        
        return matcher;
    }
    
    /**
     * 配置自定义Realm
     * Realm是Shiro连接安全数据源的桥梁
     * 
     * @param hashedCredentialsMatcher 密码匹配器
     * @return CustomRealm对象
     */
    @Bean
    public CustomRealm customRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
        // 创建自定义Realm实例
        CustomRealm customRealm = new CustomRealm();
        
        // 设置密码匹配器
        // Shiro在认证时会使用这个匹配器来比对密码
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        
        // 设置缓存管理器(可选)
        // 可以使用Redis缓存用户权限信息,提高性能
        // customRealm.setCacheManager(cacheManager);
        
        // 设置缓存名称(可选)
        customRealm.setCachingEnabled(true);
        customRealm.setAuthenticationCachingEnabled(true);
        customRealm.setAuthorizationCachingEnabled(true);
        customRealm.setAuthenticationCacheName("authenticationCache");
        customRealm.setAuthorizationCacheName("authorizationCache");
        
        return customRealm;
    }
    
    /**
     * 配置会话管理器
     * 负责管理用户会话的生命周期
     * 
     * @return DefaultWebSessionManager对象
     */
    @Bean
    public SessionManager sessionManager() {
        // 创建DefaultWebSessionManager实例
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        
        // 设置会话超时时间,单位毫秒
        // 这里设置为30分钟(1800000毫秒)
        sessionManager.setGlobalSessionTimeout(1800000L);
        
        // 设置是否在会话过期后删除会话
        sessionManager.setDeleteInvalidSessions(true);
        
        // 设置是否定时检查会话过期
        sessionManager.setSessionValidationSchedulerEnabled(true);
        
        // 设置会话验证调度器的执行间隔,单位毫秒
        // 每30分钟检查一次
        sessionManager.setSessionValidationInterval(1800000L);
        
        // 设置会话ID URL重写
        // 默认为true,会话ID会附加在URL后面,建议关闭以防止会话ID泄露
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        
        return sessionManager;
    }
    
    /**
     * 配置安全管理器
     * SecurityManager是Shiro的核心组件,协调各个组件工作
     * 
     * @param customRealm 自定义Realm
     * @param sessionManager 会话管理器
     * @return DefaultWebSecurityManager对象
     */
    @Bean
    public SecurityManager securityManager(CustomRealm customRealm, 
                                          SessionManager sessionManager) {
        // 创建DefaultWebSecurityManager实例
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        
        // 设置Realm
        // SecurityManager通过Realm来获取用户信息
        securityManager.setRealm(customRealm);
        
        // 设置会话管理器
        securityManager.setSessionManager(sessionManager);
        
        // 设置缓存管理器(可选)
        // securityManager.setCacheManager(cacheManager);
        
        // 设置记住我管理器(可选)
        // securityManager.setRememberMeManager(rememberMeManager);
        
        return securityManager;
    }
    
    /**
     * 配置Shiro过滤器工厂Bean
     * 负责配置URL过滤规则
     * 
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean对象
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        // 创建ShiroFilterFactoryBean实例
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        
        // 设置安全管理器
        shiroFilter.setSecurityManager(securityManager);
        
        // 设置登录页面URL
        // 当用户访问需要认证的资源但未登录时,会重定向到该URL
        shiroFilter.setLoginUrl("/login");
        
        // 设置登录成功后的跳转URL
        // 登录成功后默认跳转的页面
        shiroFilter.setSuccessUrl("/index");
        
        // 设置未授权页面URL
        // 当用户访问了需要权限的资源但没有权限时,会重定向到该URL
        shiroFilter.setUnauthorizedUrl("/unauthorized");
        
        // 配置过滤器链
        // 使用LinkedHashMap保证顺序
        // key是URL路径,value是过滤器名称
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        
        // 配置匿名访问的URL(不需要登录就能访问)
        // anon表示允许匿名访问
        filterChainDefinitionMap.put("/login", "anon");           // 登录页面
        filterChainDefinitionMap.put("/doLogin", "anon");         // 登录接口
        filterChainDefinitionMap.put("/logout", "anon");          // 登出接口
        filterChainDefinitionMap.put("/css/**", "anon");          // CSS静态资源
        filterChainDefinitionMap.put("/js/**", "anon");           // JS静态资源
        filterChainDefinitionMap.put("/images/**", "anon");       // 图片静态资源
        filterChainDefinitionMap.put("/druid/**", "anon");        // Druid监控页面
        filterChainDefinitionMap.put("/favicon.ico", "anon");    // 网站图标
        
        // 配置需要认证才能访问的URL
        // authc表示需要认证(登录)
        filterChainDefinitionMap.put("/", "authc");                // 首页
        filterChainDefinitionMap.put("/index", "authc");           // 首页
        filterChainDefinitionMap.put("/user/**", "authc");         // 用户相关
        filterChainDefinitionMap.put("/role/**", "authc");         // 角色相关
        
        // 配置需要特定权限才能访问的URL
        // perms表示需要特定权限
        filterChainDefinitionMap.put("/user/add", "perms[user:add]");           // 新增用户
        filterChainDefinitionMap.put("/user/edit", "perms[user:edit]");         // 修改用户
        filterChainDefinitionMap.put("/user/delete", "perms[user:delete]");     // 删除用户
        filterChainDefinitionMap.put("/role/add", "perms[role:add]");           // 新增角色
        
        // 配置特定角色才能访问的URL
        // roles表示需要特定角色
        filterChainDefinitionMap.put("/admin/**", "roles[ROLE_ADMIN]");         // 管理员专属
        
        // 配置需要记住我才能访问的URL
        // user表示通过记住我或认证都可以访问
        filterChainDefinitionMap.put("/remember/**", "user");
        
        // 设置所有其他URL都需要认证
        // /**表示匹配所有URL
        // 必须放在最后,因为过滤器链是按顺序匹配的
        filterChainDefinitionMap.put("/**", "authc");
        
        // 将过滤器链设置到ShiroFilterFactoryBean
        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        
        return shiroFilter;
    }
    
    /**
     * 配置生命周期Bean后置处理器
     * 用于自动调用Shiro组件的init()和destroy()方法
     * 
     * @return LifecycleBeanPostProcessor对象
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    
    /**
     * 配置DefaultAdvisorAutoProxyCreator
     * 用于启用Shiro的注解功能(如@RequiresPermissions)
     * 
     * @return DefaultAdvisorAutoProxyCreator对象
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")  // 依赖于LifecycleBeanPostProcessor
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = 
                new DefaultAdvisorAutoProxyCreator();
        
        // 设置使用CGLIB代理
        // CGLIB是基于继承的代理,可以代理类
        advisorAutoProxyCreator.setProxyTargetClass(true);
        
        return advisorAutoProxyCreator;
    }
    
    /**
     * 配置授权属性源通知器
     * 用于启用Shiro的注解功能
     * 
     * @param securityManager 安全管理器
     * @return AuthorizationAttributeSourceAdvisor对象
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            SecurityManager securityManager) {
        // 创建AuthorizationAttributeSourceAdvisor实例
        AuthorizationAttributeSourceAdvisor advisor = 
                new AuthorizationAttributeSourceAdvisor();
        
        // 设置安全管理器
        advisor.setSecurityManager(securityManager);
        
        return advisor;
    }
}

4.6 密码加密工具类

BCryptPasswordEncoder.java

java 复制代码
package com.example.shiro.util;

import org.mindrot.jbcrypt.BCrypt;

/**
 * BCrypt密码加密工具类
 * 使用BCrypt算法对密码进行加密和验证
 * BCrypt是一种单向哈希算法,每次加密结果都不同,但可以验证是否匹配
 * 
 * @author example
 * @since 2024-01-01
 */
public class BCryptPasswordEncoder {
    
    /**
     * 默认加密强度
     * BCrypt的work factor,范围4-31,默认10
     * 值越大加密计算越慢,安全性越高,但也会影响性能
     */
    private static final int DEFAULT_ROUNDS = 10;
    
    /**
     * 加密密码
     * 
     * @param rawPassword 原始密码
     * @return 加密后的密码
     */
    public static String encode(String rawPassword) {
        // 调用BCrypt的hashpw方法加密密码
        // 参数1: 原始密码
        // 参数2: 生成盐值的rounds,默认为10
        return BCrypt.hashpw(rawPassword, BCrypt.gensalt(DEFAULT_ROUNDS));
    }
    
    /**
     * 验证密码
     * 
     * @param rawPassword       原始密码(用户输入的密码)
     * @param encodedPassword   加密后的密码(数据库中存储的密码)
     * @return 密码匹配返回true,否则返回false
     */
    public static boolean matches(String rawPassword, String encodedPassword) {
        // 调用BCrypt的checkpw方法验证密码
        // BCrypt会从encodedPassword中自动提取盐值进行验证
        return BCrypt.checkpw(rawPassword, encodedPassword);
    }
    
    /**
     * 自定义加密强度的密码加密
     * 
     * @param rawPassword 原始密码
     * @param rounds      加密强度,范围4-31
     * @return 加密后的密码
     */
    public static String encode(String rawPassword, int rounds) {
        // 检查rounds范围
        if (rounds < 4 || rounds > 31) {
            throw new IllegalArgumentException("BCrypt rounds must be between 4 and 31");
        }
        return BCrypt.hashpw(rawPassword, BCrypt.gensalt(rounds));
    }
}

4.7 自定义异常类

AuthenticationException.java

java 复制代码
package com.example.shiro.exception;

/**
 * 自定义认证异常
 * 用于统一处理认证过程中的异常信息
 * 
 * @author example
 * @since 2024-01-01
 */
public class CustomAuthenticationException extends RuntimeException {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 异常码
     */
    private Integer code;
    
    /**
     * 构造方法
     * 
     * @param message 异常信息
     */
    public CustomAuthenticationException(String message) {
        super(message);
    }
    
    /**
     * 构造方法
     * 
     * @param code    异常码
     * @param message 异常信息
     */
    public CustomAuthenticationException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    
    /**
     * 获取异常码
     * 
     * @return 异常码
     */
    public Integer getCode() {
        return code;
    }
    
    /**
     * 设置异常码
     * 
     * @param code 异常码
     */
    public void setCode(Integer code) {
        this.code = code;
    }
}

4.8 全局异常处理器

GlobalExceptionHandler.java

java 复制代码
package com.example.shiro.exception;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

/**
 * 全局异常处理器
 * 统一处理系统中出现的各种异常
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@ControllerAdvice  // 标记为全局异常处理器
public class GlobalExceptionHandler {
    
    /**
     * 处理认证异常
     * 当用户登录失败时抛出此异常
     * 
     * @param e     异常对象
     * @param model 模型对象
     * @return 登录页面及错误信息
     */
    @ExceptionHandler(AuthenticationException.class)
    public ModelAndView handleAuthenticationException(AuthenticationException e, Model model) {
        log.error("认证异常: {}", e.getMessage());
        
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("login");  // 返回登录页面
        modelAndView.addObject("error", "用户名或密码错误");  // 添加错误信息
        modelAndView.addObject("username", "");  // 清空用户名
        
        return modelAndView;
    }
    
    /**
     * 处理未登录异常
     * 当用户未登录访问需要认证的资源时抛出此异常
     * 
     * @param e 异常对象
     * @return 登录页面
     */
    @ExceptionHandler(UnauthenticatedException.class)
    public String handleUnauthenticatedException(UnauthenticatedException e) {
        log.error("未登录异常: {}", e.getMessage());
        return "redirect:/login";  // 重定向到登录页面
    }
    
    /**
     * 处理未授权异常
     * 当用户访问了需要权限的资源但没有权限时抛出此异常
     * 
     * @param e     异常对象
     * @param model 模型对象
     * @return 未授权页面
     */
    @ExceptionHandler(UnauthorizedException.class)
    public ModelAndView handleUnauthorizedException(UnauthorizedException e, Model model) {
        log.error("未授权异常: {}", e.getMessage());
        
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("unauthorized");  // 返回未授权页面
        modelAndView.addObject("error", "您没有权限访问该资源");  // 添加错误信息
        
        return modelAndView;
    }
    
    /**
     * 处理授权异常
     * 通用授权异常处理器
     * 
     * @param e     异常对象
     * @param model 模型对象
     * @return 未授权页面
     */
    @ExceptionHandler(AuthorizationException.class)
    public ModelAndView handleAuthorizationException(AuthorizationException e, Model model) {
        log.error("授权异常: {}", e.getMessage());
        
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("unauthorized");  // 返回未授权页面
        modelAndView.addObject("error", "权限验证失败");  // 添加错误信息
        
        return modelAndView;
    }
    
    /**
     * 处理自定义异常
     * 
     * @param e     异常对象
     * @param model 模型对象
     * @return 错误页面
     */
    @ExceptionHandler(CustomAuthenticationException.class)
    public ModelAndView handleCustomException(CustomAuthenticationException e, Model model) {
        log.error("自定义异常: {}", e.getMessage());
        
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("error");  // 返回错误页面
        modelAndView.addObject("error", e.getMessage());  // 添加错误信息
        
        return modelAndView;
    }
    
    /**
     * 处理所有其他异常
     * 
     * @param e     异常对象
     * @param model 模型对象
     * @return 错误页面
     */
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception e, Model model) {
        log.error("系统异常: {}", e.getMessage(), e);
        
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("error");  // 返回错误页面
        modelAndView.addObject("error", "系统繁忙,请稍后重试");  // 添加错误信息
        
        return modelAndView;
    }
}

4.9 统一返回结果类

Result.java

java 复制代码
package com.example.shiro.common;

import java.io.Serializable;

/**
 * 统一返回结果类
 * 用于封装接口返回数据,统一返回格式
 * 
 * @author example
 * @since 2024-01-01
 */
public class Result<T> implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 响应码
     */
    private Integer code;
    
    /**
     * 响应消息
     */
    private String message;
    
    /**
     * 响应数据
     */
    private T data;
    
    /**
     * 时间戳
     */
    private Long timestamp;
    
    /**
     * 私有构造方法,禁止外部直接创建
     */
    private Result() {
        this.timestamp = System.currentTimeMillis();
    }
    
    /**
     * 成功返回结果(无数据)
     * 
     * @return Result对象
     */
    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.code = 200;
        result.message = "操作成功";
        return result;
    }
    
    /**
     * 成功返回结果(有数据)
     * 
     * @param data 返回的数据
     * @return Result对象
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.code = 200;
        result.message = "操作成功";
        result.data = data;
        return result;
    }
    
    /**
     * 成功返回结果(自定义消息)
     * 
     * @param message 自定义消息
     * @param data    返回的数据
     * @return Result对象
     */
    public static <T> Result<T> success(String message, T data) {
        Result<T> result = new Result<>();
        result.code = 200;
        result.message = message;
        result.data = data;
        return result;
    }
    
    /**
     * 失败返回结果
     * 
     * @param message 错误消息
     * @return Result对象
     */
    public static <T> Result<T> error(String message) {
        Result<T> result = new Result<>();
        result.code = 500;
        result.message = message;
        return result;
    }
    
    /**
     * 失败返回结果(自定义状态码)
     * 
     * @param code    状态码
     * @param message 错误消息
     * @return Result对象
     */
    public static <T> Result<T> error(Integer code, String message) {
        Result<T> result = new Result<>();
        result.code = code;
        result.message = message;
        return result;
    }
    
    /**
     * 判断是否成功
     * 
     * @return 成功返回true,失败返回false
     */
    public boolean isSuccess() {
        return this.code == 200;
    }
    
    // Getter和Setter方法
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
    
    public Long getTimestamp() {
        return timestamp;
    }
    
    public void setTimestamp(Long timestamp) {
        this.timestamp = timestamp;
    }
}

4.10 控制器

LoginController.java - 登录控制器

java 复制代码
package com.example.shiro.controller;

import com.example.shiro.common.Result;
import com.example.shiro.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

/**
 * 登录控制器
 * 处理用户登录、登出相关请求
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Controller  // 标记为Spring MVC控制器
public class LoginController {
    
    /**
     * 显示登录页面
     * 
     * @return 登录页面视图名称
     */
    @GetMapping("/login")
    public String loginPage() {
        return "login";  // 返回templates/login.html页面
    }
    
    /**
     * 执行登录操作
     * 
     * @param username 用户名
     * @param password 密码
     * @param model    模型对象,用于传递数据到视图
     * @return 登录成功跳转到首页,失败返回登录页面
     */
    @PostMapping("/doLogin")
    public String doLogin(@RequestParam("username") String username,
                          @RequestParam("password") String password,
                          Model model) {
        log.info("用户[{}]尝试登录", username);
        
        try {
            // 获取当前用户Subject
            // Subject是Shiro的核心概念,代表当前"用户"
            Subject subject = SecurityUtils.getSubject();
            
            // 判断用户是否已登录
            if (subject.isAuthenticated()) {
                log.warn("用户[{}]已经登录", username);
                return "redirect:/index";
            }
            
            // 创建用户名密码令牌
            // UsernamePasswordToken是Shiro提供的认证令牌
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            
            // 设置记住我功能(可选)
            // token.setRememberMe(true);
            
            // 执行登录
            // Shiro会自动调用Realm的认证方法
            subject.login(token);
            
            // 获取登录成功的用户信息
            SysUser user = (SysUser) subject.getPrincipal();
            log.info("用户[{}]登录成功", user.getUsername());
            
            // 登录成功后重定向到首页
            // 使用重定向而不是转发,避免重复提交表单
            return "redirect:/index";
            
        } catch (AuthenticationException e) {
            // 捕获认证异常
            log.error("用户[{}]登录失败: {}", username, e.getMessage());
            
            // 添加错误信息到模型
            model.addAttribute("error", "用户名或密码错误");
            model.addAttribute("username", username);
            
            // 返回登录页面
            return "login";
        }
    }
    
    /**
     * 执行登出操作
     * 
     * @return 登录页面
     */
    @GetMapping("/logout")
    public String logout() {
        // 获取当前用户Subject
        Subject subject = SecurityUtils.getSubject();
        
        if (subject != null && subject.isAuthenticated()) {
            // 获取用户名(方便日志记录)
            SysUser user = (SysUser) subject.getPrincipal();
            log.info("用户[{}]退出登录", user.getUsername());
            
            // 执行登出
            subject.logout();
        }
        
        // 重定向到登录页面
        return "redirect:/login";
    }
    
    /**
     * 获取当前登录用户信息
     * 
     * @return 用户信息
     */
    @GetMapping("/api/user/current")
    @ResponseBody  // 返回JSON数据
    public Result<SysUser> getCurrentUser() {
        // 获取当前用户Subject
        Subject subject = SecurityUtils.getSubject();
        
        // 获取用户信息
        SysUser user = (SysUser) subject.getPrincipal();
        
        // 清空密码等敏感信息
        user.setPassword(null);
        
        return Result.success(user);
    }
}

UserController.java - 用户控制器

java 复制代码
package com.example.shiro.controller;

import com.example.shiro.common.Result;
import com.example.shiro.entity.SysUser;
import com.example.shiro.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 用户控制器
 * 处理用户管理相关请求
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Controller
@RequestMapping("/user")  // 类级别请求路径前缀
public class UserController {
    
    @Autowired
    private SysUserService sysUserService;
    
    /**
     * 跳转到用户列表页面
     * 
     * @return 用户列表页面
     */
    @GetMapping("/list")
    public String userListPage(Model model) {
        // 查询用户列表
        List<SysUser> userList = sysUserService.list();
        
        // 添加到模型
        model.addAttribute("userList", userList);
        
        return "user/user-list";  // 返回templates/user/user-list.html页面
    }
    
    /**
     * 查询用户列表
     * 需要user:query权限
     * 
     * @return 用户列表
     */
    @GetMapping("/api/list")
    @ResponseBody
    @RequiresPermissions("user:query")  // 需要user:query权限
    public Result<List<SysUser>> getUserList() {
        log.info("查询用户列表");
        
        // 查询所有用户
        List<SysUser> userList = sysUserService.list();
        
        // 清空密码等敏感信息
        userList.forEach(user -> user.setPassword(null));
        
        return Result.success(userList);
    }
    
    /**
     * 新增用户
     * 需要user:add权限
     * 
     * @param user 用户信息
     * @return 操作结果
     */
    @PostMapping("/api/add")
    @ResponseBody
    @RequiresPermissions("user:add")  // 需要user:add权限
    public Result<String> addUser(@RequestBody SysUser user) {
        log.info("新增用户: {}", user.getUsername());
        
        try {
            // 保存用户
            boolean success = sysUserService.save(user);
            
            if (success) {
                return Result.success("新增用户成功");
            } else {
                return Result.error("新增用户失败");
            }
        } catch (Exception e) {
            log.error("新增用户异常: {}", e.getMessage(), e);
            return Result.error("系统异常,新增用户失败");
        }
    }
    
    /**
     * 修改用户
     * 需要user:edit权限
     * 
     * @param user 用户信息
     * @return 操作结果
     */
    @PostMapping("/api/edit")
    @ResponseBody
    @RequiresPermissions("user:edit")  // 需要user:edit权限
    public Result<String> editUser(@RequestBody SysUser user) {
        log.info("修改用户: {}", user.getId());
        
        try {
            // 更新用户
            boolean success = sysUserService.updateById(user);
            
            if (success) {
                return Result.success("修改用户成功");
            } else {
                return Result.error("修改用户失败");
            }
        } catch (Exception e) {
            log.error("修改用户异常: {}", e.getMessage(), e);
            return Result.error("系统异常,修改用户失败");
        }
    }
    
    /**
     * 删除用户
     * 需要user:delete权限
     * 
     * @param userId 用户ID
     * @return 操作结果
     */
    @PostMapping("/api/delete/{userId}")
    @ResponseBody
    @RequiresPermissions("user:delete")  // 需要user:delete权限
    public Result<String> deleteUser(@PathVariable("userId") Long userId) {
        log.info("删除用户: {}", userId);
        
        try {
            // 删除用户(逻辑删除)
            boolean success = sysUserService.removeById(userId);
            
            if (success) {
                return Result.success("删除用户成功");
            } else {
                return Result.error("删除用户失败");
            }
        } catch (Exception e) {
            log.error("删除用户异常: {}", e.getMessage(), e);
            return Result.error("系统异常,删除用户失败");
        }
    }
    
    /**
     * 管理员专属接口
     * 需要ROLE_ADMIN角色
     * 
     * @return 操作结果
     */
    @GetMapping("/api/admin")
    @ResponseBody
    @RequiresRoles("ROLE_ADMIN")  // 需要ROLE_ADMIN角色
    public Result<String> adminOnly() {
        log.info("访问管理员专属接口");
        return Result.success("您是管理员,可以访问该接口");
    }
}

IndexController.java - 首页控制器

java 复制代码
package com.example.shiro.controller;

import com.example.shiro.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 首页控制器
 * 处理首页相关请求
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Controller
public class IndexController {
    
    /**
     * 首页
     * 
     * @param model 模型对象
     * @return 首页视图
     */
    @GetMapping({"/", "/index"})
    @RequiresAuthentication  // 需要认证(登录)
    public String index(Model model) {
        // 获取当前登录用户
        Subject subject = SecurityUtils.getSubject();
        SysUser user = (SysUser) subject.getPrincipal();
        
        log.info("访问首页,当前用户: {}", user.getUsername());
        
        // 添加用户信息到模型
        model.addAttribute("user", user);
        
        return "index";  // 返回templates/index.html页面
    }
    
    /**
     * 未授权页面
     * 
     * @param model 模型对象
     * @return 未授权视图
     */
    @GetMapping("/unauthorized")
    public String unauthorized(Model model) {
        model.addAttribute("error", "您没有权限访问该资源");
        return "unauthorized";  // 返回templates/unauthorized.html页面
    }
}

五、测试验证

5.1 测试准备

  1. 启动MySQL数据库

    • 确保MySQL服务已启动
    • 执行数据库初始化SQL脚本
  2. 配置数据库连接

    • 修改application.yml中的数据库连接信息
    • 确保用户名、密码正确
  3. 启动应用

    • 运行SpringBootApplication主类
    • 观察启动日志,确保无错误

5.2 功能测试

测试1:用户登录

测试项 测试步骤 预期结果
管理员登录 输入用户名admin,密码admin123 登录成功,跳转到首页,显示"管理员"标签
普通用户登录 输入用户名user,密码admin123 登录成功,跳转到首页,显示"普通用户"标签
错误密码 输入正确的用户名,错误的密码 登录失败,显示"用户名或密码错误"
不存在的用户 输入不存在的用户名 登录失败,显示"用户名或密码错误"

测试2:权限控制

测试项 测试账号 操作 预期结果
查询用户列表 admin 访问/user/list 可以访问,显示用户列表
新增用户 admin 点击"新增用户"按钮 可以操作
编辑用户 admin 点击"编辑"按钮 可以操作
删除用户 admin 点击"删除"按钮 可以操作
查询用户列表 user 访问/user/list 可以访问,显示用户列表
新增用户 user 点击"新增用户"按钮 按钮不显示,无法操作
编辑用户 user 点击"编辑"按钮 按钮不显示,无法操作
删除用户 user 点击"删除"按钮 按钮不显示,无法操作
管理员专属接口 admin 访问/user/api/admin 可以访问
管理员专属接口 user 访问/user/api/admin 返回403未授权

测试3:会话管理

测试项 测试步骤 预期结果
登出 点击"退出登录"按钮 退出登录,跳转到登录页面
会话超时 登录后等待30分钟不操作 再次访问页面时跳转到登录页面
重复登录 已登录状态下再次登录 跳转到首页,不会重复创建会话

5.3 接口测试

使用Postman或curl进行接口测试:

bash 复制代码
# 1. 登录接口
curl -X POST http://localhost:8080/shiro-demo/doLogin \
     -d "username=admin&password=admin123" \
     -c cookies.txt

# 2. 获取当前用户信息
curl http://localhost:8080/shiro-demo/api/user/current \
     -b cookies.txt

# 3. 查询用户列表(需要user:query权限)
curl http://localhost:8080/shiro-demo/user/api/list \
     -b cookies.txt

# 4. 删除用户(需要user:delete权限)
curl -X POST http://localhost:8080/shiro-demo/user/api/delete/2 \
     -b cookies.txt

# 5. 管理员专属接口(需要ROLE_ADMIN角色)
curl http://localhost:8080/shiro-demo/user/api/admin \
     -b cookies.txt

六、常见问题解决

6.1 登录后Session丢失

问题描述:用户登录成功后,刷新页面或访问其他页面时需要重新登录。

可能原因

  1. Shiro Session未正确配置
  2. 浏览器禁用了Cookie
  3. 域名不一致导致Cookie无法共享

解决方案

java 复制代码
// ShiroConfig.java中配置会话管理器
@Bean
public SessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setGlobalSessionTimeout(1800000L);  // 30分钟
    sessionManager.setSessionIdUrlRewritingEnabled(false);  // 禁用URL重写
    return sessionManager;
}

6.2 权限注解不生效

问题描述 :使用@RequiresPermissions等注解时,权限校验不生效。

可能原因

  1. 没有配置DefaultAdvisorAutoProxyCreator
  2. 没有配置AuthorizationAttributeSourceAdvisor
  3. 使用了@RestController但类没有被Spring AOP代理

解决方案

java 复制代码
// ShiroConfig.java中确保配置了以下两个Bean
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = 
        new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
        SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = 
        new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

6.3 密码验证失败

问题描述:输入正确的密码但登录失败,提示"用户名或密码错误"。

可能原因

  1. 密码加密算法不一致
  2. 数据库中的密码格式错误
  3. Realm的密码匹配器配置错误

解决方案

java 复制代码
// 确保密码加密和验证使用相同的算法
// 加密密码
String encodedPassword = BCrypt.hashpw("admin123", BCrypt.gensalt());

// 验证密码
boolean matches = BCrypt.checkpw("admin123", encodedPassword);

// ShiroConfig中配置正确的密码匹配器
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
    matcher.setHashAlgorithmName("BCrypt");  // 使用BCrypt
    matcher.setHashIterations(1);
    return matcher;
}

6.4 多次重定向问题

问题描述:登录后访问页面时出现"重定向次数过多"错误。

可能原因

  1. 过滤器链配置错误,形成了循环
  2. 登录成功后的跳转URL配置错误

解决方案

java 复制代码
// 检查过滤器链配置,确保没有循环依赖
// ShiroConfig.java中的shiroFilterFactoryBean方法
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/index", "authc");
filterChainDefinitionMap.put("/**", "authc");  // 放在最后

6.5 Thymeleaf Shiro标签不生效

问题描述 :在Thymeleaf模板中使用<shiro:hasPermission>等标签时不起作用。

可能原因

  1. 没有引入thymeleaf-extras-shiro依赖
  2. 没有在html标签中声明shiro命名空间
  3. 没有配置ShiroDialect

解决方案

xml 复制代码
<!-- pom.xml中添加依赖 -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
java 复制代码
// ShiroConfig.java中添加ShiroDialect配置
@Bean
public ShiroDialect shiroDialect() {
    return new ShiroDialect();
}
html 复制代码
<!-- html文件中声明命名空间 -->
<html xmlns:th="http://www.thymeleaf.org" 
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

6.6 Remember Me功能不生效

问题描述:勾选"记住我"后,关闭浏览器再打开仍需要登录。

可能原因

  1. 没有配置Remember Me管理器
  2. 前端没有传递rememberMe参数
  3. Cookie保存失败

解决方案

java 复制代码
// ShiroConfig.java中配置Cookie
@Bean
public SimpleCookie rememberMeCookie() {
    SimpleCookie cookie = new SimpleCookie("rememberMe");
    cookie.setMaxAge(7 * 24 * 60 * 60);  // 7天
    cookie.setHttpOnly(true);
    return cookie;
}

@Bean
public CookieRememberMeManager rememberMeManager() {
    CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
    rememberMeManager.setCookie(rememberMeCookie());
    return rememberMeManager;
}

// 在SecurityManager中配置
securityManager.setRememberMeManager(rememberMeManager());
html 复制代码
<!-- 登录表单中添加rememberMe复选框 -->
<input type="checkbox" name="rememberMe" value="true"> 记住我

七、总结

7.1 Shiro的优点

  • 简单易用:API简洁明了,学习曲线平缓
  • 功能全面:提供认证、授权、加密、会话管理等完整功能
  • 灵活可扩展:支持自定义Realm、过滤器等
  • 社区活跃:文档完善,社区支持良好

7.2 后续优化方向

  1. 集成Redis:使用Redis缓存Session和权限信息,提高性能
  2. JWT整合:结合JWT实现无状态认证,适合分布式系统
  3. 动态权限:实现动态加载权限配置,无需重启应用
  4. OAuth2整合:集成OAuth2实现第三方登录
  5. 权限细化:实现数据级权限控制,如只能查看自己创建的数据
相关推荐
花间相见2 小时前
【JAVA开发】—— Git常用操作
java·开发语言·git
Java程序员威哥2 小时前
云原生Java应用优化实战:资源限制+JVM参数调优,容器启动快50%
java·开发语言·jvm·python·docker·云原生
多多*2 小时前
程序设计工作室1月21日内部训练赛
java·开发语言·网络·jvm·tcp/ip
Engineer邓祥浩2 小时前
设计模式学习(15) 23-13 模版方法模式
java·学习·设计模式
茶本无香2 小时前
设计模式之四:建造者模式(Builder Pattern)详解
java·设计模式·建造者模式
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 高校素拓分管理系统的设计与开发为例,包含答辩的问题和答案
java·eclipse
计算机学姐2 小时前
基于SpringBoot的社区互助系统
java·spring boot·后端·mysql·spring·信息可视化·推荐算法
源代码•宸2 小时前
Leetcode—3314. 构造最小位运算数组 I【简单】
开发语言·后端·算法·leetcode·面试·golang·位运算
lbb 小魔仙2 小时前
【Java】深入解析 Java 集合底层原理:HashMap 扩容与 TreeMap 红黑树实现
java·开发语言