一、引言:为什么选择 Shiro?
Apache Shiro 是一个强大且易用的 Java 安全框架,它提供了身份认证、授权、会话管理和加密等功能。相比其他安全框架,Shiro具有以下优势:
- 简单易用:API 设计简洁直观,学习成本低
- 全面的安全功能:一站式解决认证、授权、会话管理问题
- 灵活可扩展:易于与 Spring Boot等主流框架整合
- 轻量级:核心包体积小,不依赖其他框架 在现代 Web 应用中,无论是单用户系统还是多用户并发系统,Shiro都能提供可靠的安全保障。
二、环境准备与项目搭建
2.1 开发环境要求
组件 版本要求
JDK 1.8 及以上
Maven 3.6 及以上
Spring Boot 2.7.x(本文中使用 2.7.5 版本)
Shiro 1.10.0
2.2 创建 Spring Boot 项目
可以通过 Spring Initializr 快速创建项目,也可使用 IDE 内置的项目创建功能。
2.2.1 添加核心依赖
在 pom.xml 文件中添加以下依赖:
java
<dependencies>
<!-- Spring Boot Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Shiro 整合 Spring Boot 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.10.0</version>
</dependency>
<!-- Lombok 依赖(可选,用于简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.2.2 项目结构设计
推荐的项目结构如下:
bash
com.example.shirodemo
├── ShiroDemoApplication.java // 应用程序入口
├── config
│ └── ShiroConfig.java // Shiro 核心配置类
├── controller
│ ├── LoginController.java // 登录相关控制器
│ ├── UserController.java // 普通用户控制器
│ └── AdminController.java // 管理员控制器
├── entity
│ └── User.java // 用户实体类
├── realm
│ └── UserRealm.java // 自定义 Realm 实现
├── service
│ └── UserService.java // 用户服务类
└── listener
└── MySessionListener.java // 会话监听器(多用户扩展)
三、单用户认证授权实现
3.1 核心概念解析
在开始编码前,先了解 Shiro 的几个核心概念:
- Subject:当前用户的安全操作对象,代表与系统交互的用户
- SecurityManager:Shiro 的核心,管理所有用户的安全操作
- Realm:充当数据源角色,负责用户认证和授权信息的获取
- Authentication:身份认证,验证用户是否为合法用户
- Authorization:授权,验证合法用户是否有权限执行某个操作
3.2 实体类设计
创建 User.java 实体类,存储用户基本信息:
java
package com.example.shirodemo.entity;
import lombok.Data;
/**
* 用户实体类
*/
@Data
public class User {
private Integer id; // 用户ID
private String username; // 用户名
private String password; // 密码(加密存储)
private String salt; // 盐值(用于加密)
private String nickname; // 昵称
// 构造方法
public User(Integer id, String username, String password, String salt, String nickname) {
this.id = id;
this.username = username;
this.password = password;
this.salt = salt;
this.nickname = nickname;
}
}
注意:实际项目中密码应加密存储,这里使用 MD5 加密示例,密码 "123456" 加密后为 "202cb962ac59075b964b07152d234b70"
3.3 Shiro 核心配置
创建 ShiroConfig.java 配置类,配置 Shiro 的核心组件:
java
package com.example.shirodemo.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.shirodemo.realm.UserRealm;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置类
*/
@Configuration
public class ShiroConfig {
/**
* 自定义 Realm
*/
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
/**
* 安全管理器
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义 Realm
securityManager.setRealm(userRealm());
return securityManager;
}
/**
* Shiro 过滤器工厂
* 负责拦截所有请求并进行安全控制
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 配置登录页面和未授权页面
factoryBean.setLoginUrl("/loginPage"); // 未认证时跳转的登录页面
factoryBean.setUnauthorizedUrl("/unauthorized"); // 未授权时跳转的页面
// 配置过滤器链,LinkedHashMap 保证顺序
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 公开访问的路径,无需认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/loginPage", "anon");
filterChainDefinitionMap.put("/logout", "logout"); // 退出登录过滤器
// 角色控制:管理员角色才能访问的路径
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
// 角色控制:普通用户角色才能访问的路径
filterChainDefinitionMap.put("/user/**", "roles[user]");
// 其他所有路径需要认证
filterChainDefinitionMap.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
}
过滤器说明
Shiro 提供了多种内置过滤器,常用的有:
- anon:匿名访问,无需认证
- authc:需要认证才能访问
- roles[角色名]:需要特定角色才能访问
- perms[权限名]:需要特定权限才能访问
- logout:退出登录过滤器
3.4 自定义 Realm 实现
Realm 是 Shiro 与数据交互的桥梁,负责认证和授权逻辑的实现。创建 UserRealm.java:
java
package com.example.shirodemo.realm;
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.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.shirodemo.entity.User;
import com.example.shirodemo.service.UserService;
import java.util.Set;
/**
* 自定义 Realm,实现认证和授权逻辑
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权逻辑:获取用户的角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 从 principals 中获取用户名
String username = (String) principals.getPrimaryPrincipal();
// 从服务层获取用户角色
Set<String> roles = userService.findRolesByUsername(username);
// 创建授权信息对象并设置角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(roles);
// 实际项目中还可以添加权限信息
// authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
/**
* 认证逻辑:验证用户身份
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// 从 token 中获取用户名
String username = (String) token.getPrincipal();
// 查询用户信息
User user = userService.findByUsername(username);
// 用户不存在
if (user == null) {
throw new UnknownAccountException("用户名不存在");
}
// 返回认证信息,Shiro 会自动进行密码比对
return new SimpleAuthenticationInfo(
user.getUsername(), // 身份标识(通常为用户名)
user.getPassword(), // 数据库中的密码(加密后)
ByteSource.Util.bytes(user.getSalt()), // 盐值
getName() // Realm 名称
);
}
}
3.5 用户服务实现
创建 UserService.java 提供用户数据访问服务:
java
package com.example.shirodemo.service;
import com.example.shirodemo.entity.User;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 用户服务类,提供用户信息和角色查询
*/
@Service
public class UserService {
// 模拟数据库存储用户信息
private static final Map<String, User> users = new HashMap<>();
// 初始化用户数据
static {
// 管理员用户,密码123456(已加密存储)
users.put("admin", new User(1, "admin", "202cb962ac59075b964b07152d234b70", "admin_salt", "管理员"));
// 普通用户
users.put("user", new User(2, "user", "202cb962ac59075b964b07152d234b70", "user_salt", "普通用户"));
}
/**
* 根据用户名查询用户
*/
public User findByUsername(String username) {
return users.get(username);
}
/**
* 根据用户名查询角色
*/
public Set<String> findRolesByUsername(String username) {
Set<String> roles = new HashSet<>();
if ("admin".equals(username)) {
roles.add("admin");
}
if ("user".equals(username)) {
roles.add("user");
}
return roles;
}
}
3.6 控制器实现
3.6.1 登录控制器
创建 LoginController.java 处理登录、登出等请求:
java
package com.example.shirodemo.controller;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录相关控制器
*/
@RestController
public class LoginController {
/**
* 登录页面提示
*/
@GetMapping("/loginPage")
public String loginPage() {
return "请登录,访问 /login 进行登录(POST方法,参数:username和password)";
}
/**
* 登录处理
*/
@PostMapping("/login")
public String login(String username, String password) {
// 获取当前用户
Subject subject = SecurityUtils.getSubject();
// 创建用户名密码令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 执行登录
subject.login(token);
return "登录成功!当前用户:" + username;
} catch (AuthenticationException e) {
return "登录失败:" + e.getMessage();
}
}
/**
* 退出登录
*/
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "退出登录成功!";
}
/**
* 未授权页面
*/
@GetMapping("/unauthorized")
public String unauthorized() {
return "未授权,无法访问!";
}
/**
* 首页
*/
@GetMapping("/")
public String index() {
// 获取当前登录用户
String username = (String) SecurityUtils.getSubject().getPrincipal();
return "欢迎访问首页,当前登录用户:" + username;
}
}
3.6.2 用户控制器
创建 UserController.java 处理普通用户请求:
java
package com.example.shirodemo.controller;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 普通用户控制器
* 需拥有 user 角色才能访问
*/
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/info")
@RequiresRoles("user") // 要求 user 角色
public String userInfo() {
return "这是普通用户可以看到的信息";
}
@GetMapping("/profile")
@RequiresRoles("user") // 要求 user 角色
public String userProfile() {
return "用户个人资料页面";
}
}
3.6.3 管理员控制器
创建 AdminController.java 处理管理员请求:
java
package com.example.shirodemo.controller;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 管理员控制器
* 需拥有 admin 角色才能访问
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/info")
@RequiresRoles("admin") // 要求 admin 角色
public String adminInfo() {
return "这是管理员可以看到的信息";
}
@GetMapping("/manage")
@RequiresRoles("admin") // 要求 admin 角色
public String manageUsers() {
return "用户管理页面";
}
}
3.7 单用户功能测试
3.7.1 启动应用
创建主类 ShiroDemoApplication.java:
java
package com.example.shirodemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShiroDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroDemoApplication.class, args);
}
}
运行主类启动应用,默认端口为 8080。
3.7.2 测试步骤
- 未登录访问受保护资源 访问 http://localhost:8080/user/info 结果:会被重定向到登录提示页面
- 登录管理员账号 使用 POST 方法访问
http://localhost:8080/login?username=admin\&password=123456 结果:返回
"登录成功!当前用户:admin" - 管理员访问权限测试 访问 http://localhost:8080/admin/info → 成功访问 访问
http://localhost:8080/user/info → 访问失败(未授权) - 登录普通用户账号 使用 POST 方法访问
http://localhost:8080/login?username=user\&password=123456 结果:返回 "登录成功!当前用户:user" - 普通用户访问权限测试 访问 http://localhost:8080/user/info → 成功访问 访问
http://localhost:8080/admin/info → 访问失败(未授权) - 退出登录 访问 http://localhost:8080/logout 结果:返回 "退出登录成功!"
四、扩展到多用户会话管理
在实际应用中,我们需要支持多个用户同时登录,并对在线用户进行管理。下面扩展上述示例以支持多用户场景。
4.1 会话管理配置
修改 ShiroConfig.java,添加会话管理器配置:
java
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import java.util.Collections;
// ... 其他代码保持不变
/**
* 会话DAO,负责会话的CRUD操作
*/
@Bean
public SessionDAO sessionDAO() {
// 内存会话DAO,适合单机环境
return new MemorySessionDAO();
}
/**
* 会话管理器
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 设置会话DAO
sessionManager.setSessionDAO(sessionDAO());
// 设置会话超时时间,单位毫秒(30分钟)
sessionManager.setGlobalSessionTimeout(1800000);
// 配置会话ID Cookie
SimpleCookie cookie = new SimpleCookie("sid");
cookie.setHttpOnly(true); // 防止JavaScript访问
cookie.setPath("/"); // 所有路径都有效
sessionManager.setSessionIdCookie(cookie);
// 启用会话ID Cookie
sessionManager.setSessionIdCookieEnabled(true);
// 配置会话监听
sessionManager.setSessionListeners(Collections.singletonList(new MySessionListener()));
return sessionManager;
}
// 更新安全管理器配置,添加会话管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setSessionManager(sessionManager()); // 添加会话管理器
return securityManager;
}
4.2 实现会话监听器
创建 MySessionListener.java 监听会话的创建、销毁等事件:
java
package com.example.shirodemo.listener;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 会话监听器,监听会话的创建、停止和过期
*/
public class MySessionListener implements SessionListener {
private static final Logger logger = LoggerFactory.getLogger(MySessionListener.class);
// 在线用户数量,使用原子类保证线程安全
private final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 会话创建时触发
*/
@Override
public void onStart(Session session) {
onlineCount.incrementAndGet();
logger.info("用户登录,当前在线人数:{}", onlineCount.get());
}
/**
* 会话停止时触发(用户退出登录)
*/
@Override
public void onStop(Session session) {
onlineCount.decrementAndGet();
logger.info("用户退出,当前在线人数:{}", onlineCount.get());
}
/**
* 会话过期时触发
*/
@Override
public void onExpiration(Session session) {
onlineCount.decrementAndGet();
logger.info("会话过期,当前在线人数:{}", onlineCount.get());
}
/**
* 获取当前在线人数
*/
public int getOnlineCount() {
return onlineCount.get();
}
}
4.3 扩展用户服务
更新 UserService.java,添加更多测试用户:
java
// 初始化用户数据
static {
// 管理员用户
users.put("admin", new User(1, "admin", "202cb962ac59075b964b07152d234b70", "admin_salt", "管理员"));
// 普通用户1
users.put("user1", new User(2, "user1", "202cb962ac59075b964b07152d234b70", "user1_salt", "用户1"));
// 普通用户2
users.put("user2", new User(3, "user2", "202cb962ac59075b964b07152d234b70", "user2_salt", "用户2"));
// 同时拥有admin和user角色的用户
users.put("super", new User(4, "super", "202cb962ac59075b964b07152d234b70", "super_salt", "超级用户"));
}
// 更新角色查询方法
public Set<String> findRolesByUsername(String username) {
Set<String> roles = new HashSet<>();
if ("admin".equals(username) || "super".equals(username)) {
roles.add("admin");
}
if ("user1".equals(username) || "user2".equals(username) || "super".equals(username)) {
roles.add("user");
}
return roles;
}
4.4 实现在线用户管理功能
扩展 AdminController.java,添加在线用户管理接口:
java
package com.example.shirodemo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private SessionDAO sessionDAO;
// ... 原有代码保持不变
/**
* 获取在线用户列表
*/
@GetMapping("/onlineUsers")
@RequiresRoles("admin")
public List<Map<String, Object>> getOnlineUsers() {
// 获取所有活跃会话
Collection<Session> sessions = sessionDAO.getActiveSessions();
List<Map<String, Object>> onlineUsers = new ArrayList<>();
for (Session session : sessions) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("sessionId", session.getId());
// 从会话中获取用户名
userInfo.put("username", session.getAttribute(
"org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY"));
userInfo.put("host", session.getHost()); // 客户端IP
userInfo.put("startTime", session.getStartTimestamp()); // 登录时间
userInfo.put("lastAccessTime", session.getLastAccessTime()); // 最后活动时间
userInfo.put("timeout", session.getTimeout()); // 超时时间(毫秒)
onlineUsers.add(userInfo);
}
return onlineUsers;
}
/**
* 强制用户登出
*/
@GetMapping("/forceLogout/{sessionId}")
@RequiresRoles("admin")
public String forceLogout(@PathVariable String sessionId) {
Session session = sessionDAO.readSession(sessionId);
if (session != null) {
sessionDAO.delete(session); // 删除会话,强制用户登出
return "已强制用户登出,会话ID:" + sessionId;
}
return "会话不存在或已过期,会话ID:" + sessionId;
}
}
4.5 多用户功能测试
4.5.1 多用户登录测试
使用不同浏览器登录不同用户
浏览器 1:登录 admin/123456
浏览器 2:登录 user1/123456
浏览器 3:登录 user2/123456
浏览器 4:登录 super/123456
验证权限隔离
user1 访问 http://localhost:8080/admin/info → 未授权 admin 访问
http://localhost:8080/user/info → 未授权 super 访问
http://localhost:8080/admin/info 和 http://localhost:8080/user/info →都能访问
4.5.2 在线用户管理测试
-
查看在线用户列表
使用 admin 账号访问 http://localhost:8080/admin/onlineUsers
结果:返回所有当前登录用户的会话信息
-
强制用户登出
从在线用户列表中获取 user1 的 sessionId
访问 http://localhost:8080/admin/forceLogout/{sessionId}
在登录 user1 的浏览器中刷新页面 → 会被要求重新登录
五、进阶扩展建议
五、总结
本文详细介绍了 Spring Boot 整合 Shiro 的完整过程,从单用户认证授权到多用户会话管理,涵盖了核心配置、代码实现和测试方法。通过本文的学习,你可以:
- 理解 Shiro 的核心概念和工作原理
- 掌握 Spring Boot 整合 Shiro 的基本配置
- 实现用户认证和基于角色的授权
- 管理多用户会话和在线用户
Shiro 作为一款优秀的安全框架,能够满足大多数 Web
应用的安全需求。在实际项目中,可根据具体需求进行扩展和优化,构建更安全、更可靠的认证授权系统。
shiro官网