本文详细讲解如何在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 测试准备
-
启动MySQL数据库
- 确保MySQL服务已启动
- 执行数据库初始化SQL脚本
-
配置数据库连接
- 修改
application.yml中的数据库连接信息 - 确保用户名、密码正确
- 修改
-
启动应用
- 运行
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丢失
问题描述:用户登录成功后,刷新页面或访问其他页面时需要重新登录。
可能原因:
- Shiro Session未正确配置
- 浏览器禁用了Cookie
- 域名不一致导致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等注解时,权限校验不生效。
可能原因:
- 没有配置
DefaultAdvisorAutoProxyCreator - 没有配置
AuthorizationAttributeSourceAdvisor - 使用了
@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 密码验证失败
问题描述:输入正确的密码但登录失败,提示"用户名或密码错误"。
可能原因:
- 密码加密算法不一致
- 数据库中的密码格式错误
- 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 多次重定向问题
问题描述:登录后访问页面时出现"重定向次数过多"错误。
可能原因:
- 过滤器链配置错误,形成了循环
- 登录成功后的跳转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>等标签时不起作用。
可能原因:
- 没有引入
thymeleaf-extras-shiro依赖 - 没有在html标签中声明shiro命名空间
- 没有配置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功能不生效
问题描述:勾选"记住我"后,关闭浏览器再打开仍需要登录。
可能原因:
- 没有配置Remember Me管理器
- 前端没有传递rememberMe参数
- 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 后续优化方向
- 集成Redis:使用Redis缓存Session和权限信息,提高性能
- JWT整合:结合JWT实现无状态认证,适合分布式系统
- 动态权限:实现动态加载权限配置,无需重启应用
- OAuth2整合:集成OAuth2实现第三方登录
- 权限细化:实现数据级权限控制,如只能查看自己创建的数据