Apache Shiro 技术详解
目录
- [1. Apache Shiro 简介](#1. Apache Shiro 简介)
- [2. 架构流程图](#2. 架构流程图)
- [2.1 Shiro 整体架构](#2.1 Shiro 整体架构)
- [2.2 认证流程](#2.2 认证流程)
- [2.3 授权流程](#2.3 授权流程)
- [2.4 CAS 单点登录认证成功流程](#2.4 CAS 单点登录认证成功流程)
- [2.5 CAS 认证成功后的 Principal 生成流程](#2.5 CAS 认证成功后的 Principal 生成流程)
- [3. 核心类源码解析](#3. 核心类源码解析)
- [4. 重难点分析](#4. 重难点分析)
- [5. 结合 Spring Boot 使用](#5. 结合 Spring Boot 使用)
- [6. 项目实战案例](#6. 项目实战案例)
1. Apache Shiro 简介
1.1 什么是 Apache Shiro
Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能。它被设计为直观和易用,同时提供强大的安全特性。
1.2 核心特性
- 认证(Authentication):验证用户身份
- 授权(Authorization):控制用户访问权限
- 会话管理(Session Management):管理用户会话
- 加密(Cryptography):提供加密解密功能
- Web 支持:与 Web 应用无缝集成
- 缓存支持:提供缓存机制提高性能
1.3 优势
- 简单易用:API 设计直观,学习成本低
- 功能完整:涵盖安全框架的所有核心功能
- 灵活配置:支持多种配置方式
- 性能优秀:轻量级框架,性能表现良好
- 社区活跃:Apache 基金会维护,社区支持良好
2. 架构流程图
2.1 Shiro 整体架构
Subject 主体 SecurityManager 安全管理器 Authenticator 认证器 Authorizer 授权器 SessionManager 会话管理器 Cryptography 加密器 Realm 域 SessionDAO 会话数据访问对象 Hash 哈希 Cipher 密码 数据源 缓存
2.2 认证流程
用户 Subject SecurityManager Authenticator Realm 数据源 1. 提交认证信息 2. 调用 login() 3. 执行认证 4. 获取认证信息 5. 查询用户数据 6. 返回用户信息 7. 返回认证结果 8. 认证成功/失败 9. 更新 Subject 状态 10. 返回认证结果 用户 Subject SecurityManager Authenticator Realm 数据源
2.3 授权流程
用户 Subject SecurityManager Authorizer Realm 数据源 1. 访问受保护资源 2. 检查权限 3. 执行授权检查 4. 获取权限信息 5. 查询用户权限 6. 返回权限数据 7. 返回权限信息 8. 权限检查结果 9. 允许/拒绝访问 10. 返回访问结果 用户 Subject SecurityManager Authorizer Realm 数据源
2.4 CAS 单点登录认证成功流程
用户 应用系统 CAS服务器 Shiro框架 Redis缓存 数据库 用户首次访问受保护资源 1. 访问受保护资源 2. 检查用户认证状态 3. subject.getPrincipal() == null 4. 重定向到CAS登录页面 CAS认证过程 5. 在CAS页面输入凭据 6. 验证用户凭据 7. 携带ticket回调应用系统 8. 验证ticket有效性 9. 返回用户信息(loginName, userId) Shiro认证成功处理 10. 调用ShiroAuthService.doAuthenticationInfo() 11. 存储用户信息到Session 12. 创建Principal对象 13. 设置到Subject的PrincipalCollection 14. 缓存用户信息到Redis 15. 重定向到原始请求资源 后续请求处理 16. 访问原始资源 17. 检查认证状态 18. subject.getPrincipal() != null 19. 认证通过,允许访问 20. 返回资源内容 会话恢复机制 21. 携带SHIROJSESSIONID的后续请求 22. 从Session中恢复Principal 23. 验证会话有效性 24. 重新设置Principal到Subject 25. 认证成功,允许访问 用户 应用系统 CAS服务器 Shiro框架 Redis缓存 数据库
2.5 CAS 认证成功后的 Principal 生成流程
CAS认证成功回调 ShiroAuthService.doAuthenticationInfo 存储用户信息到Session 创建PrincipalCollection 添加用户ID到PrincipalCollection 添加登录名到PrincipalCollection 设置Realm名称 创建Subject对象 设置Principal到Subject 存储Principal到Session 缓存用户信息到Redis 重定向到原始请求
3. 核心类源码解析
3.1 Subject 接口
java
public interface Subject {
// 获取用户身份
Object getPrincipal();
// 检查是否已认证
boolean isAuthenticated();
// 检查是否记住我
boolean isRemembered();
// 执行登录
void login(AuthenticationToken token) throws AuthenticationException;
// 执行登出
void logout();
// 检查权限
boolean hasRole(String roleIdentifier);
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
// 检查权限(多个)
boolean[] hasRoles(List<String> roleIdentifiers);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
}
3.2 SecurityManager 接口
java
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
// 登录
Subject login(Subject subject, AuthenticationToken authenticationToken)
throws AuthenticationException;
// 登出
void logout(Subject subject);
// 创建 Subject
Subject createSubject(SubjectContext context);
// 获取 Subject
Subject getSubject(SubjectContext context);
}
3.3 DefaultWebSecurityManager 实现
java
public class DefaultWebSecurityManager extends DefaultSecurityManager {
// 会话管理器
private SessionManager sessionManager;
// 会话存储
private SessionStorageEvaluator sessionStorageEvaluator;
// 会话模式
private SessionMode sessionMode = SessionMode.HTTP;
@Override
protected SubjectContext createSubjectContext() {
WebSubjectContext wsc = new WebSubjectContext();
wsc.setSessionMode(this.sessionMode);
return wsc;
}
@Override
protected SubjectContext copy(SubjectContext subjectContext) {
if (subjectContext instanceof WebSubjectContext) {
return new WebSubjectContext(subjectContext);
}
return new WebSubjectContext(subjectContext);
}
}
3.4 UserFilter 核心方法
java
public class UserFilter extends AccessControlFilter {
// 访问控制核心方法
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginRequest(request, response)) {
return true; // 登录请求直接允许
} else {
Subject subject = getSubject(request, response);
// 检查用户是否已认证
return subject.getPrincipal() != null;
}
}
// 访问被拒绝时的处理
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
saveRequestAndRedirectToLogin(request, response);
return false;
}
// 检查是否为登录请求
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}
}
3.5 Realm 接口
java
public interface Realm {
// 获取 Realm 名称
String getName();
// 支持认证令牌
boolean supports(AuthenticationToken token);
// 获取认证信息
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException;
}
3.6 AuthorizingRealm 抽象类
java
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer {
// 授权缓存
private Cache<Object, AuthorizationInfo> authorizationCache;
// 获取授权信息
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
// 获取认证信息
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException;
@Override
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
// 从缓存获取
if (log.isTraceEnabled()) {
log.trace("Retrieving AuthorizationInfo for principals [{}]", principals);
}
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
if (cache != null) {
Object key = getAuthorizationCacheKey(principals);
info = cache.get(key);
if (log.isTraceEnabled()) {
if (info == null) {
log.trace("No AuthorizationInfo found in cache for principals [{}]", principals);
} else {
log.trace("AuthorizationInfo found in cache for principals [{}]", principals);
}
}
}
if (info == null) {
// 从 Realm 获取
info = doGetAuthorizationInfo(principals);
if (info != null && cache != null) {
Object key = getAuthorizationCacheKey(principals);
cache.put(key, info);
}
}
return info;
}
}
4. 重难点分析
4.1 认证与授权的区别
特性 | 认证(Authentication) | 授权(Authorization) |
---|---|---|
目的 | 验证用户身份 | 控制用户访问权限 |
时机 | 用户登录时 | 访问资源时 |
输入 | 用户名/密码 | 用户身份+资源信息 |
输出 | 认证成功/失败 | 允许/拒绝访问 |
实现 | doGetAuthenticationInfo() |
doGetAuthorizationInfo() |
4.2 会话管理机制
4.2.1 会话生命周期
java
// 创建会话
Session session = subject.getSession();
session.setAttribute("key", "value");
// 会话超时设置
session.setTimeout(1800000); // 30分钟
// 会话销毁
session.stop();
4.2.2 会话存储策略
java
// 内存存储(默认)
DefaultSessionManager sessionManager = new DefaultSessionManager();
// 数据库存储
JdbcSessionDAO sessionDAO = new JdbcSessionDAO();
sessionDAO.setDataSource(dataSource);
sessionManager.setSessionDAO(sessionDAO);
// Redis 存储
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager);
sessionManager.setSessionDAO(redisSessionDAO);
4.3 缓存机制
4.3.1 认证缓存
java
// 启用认证缓存
realm.setAuthenticationCachingEnabled(true);
realm.setAuthenticationCacheName("authenticationCache");
// 缓存配置
CacheManager cacheManager = new MemoryConstrainedCacheManager();
securityManager.setCacheManager(cacheManager);
4.3.2 授权缓存
java
// 启用授权缓存
realm.setAuthorizationCachingEnabled(true);
realm.setAuthorizationCacheName("authorizationCache");
// 缓存失效
realm.clearCachedAuthorizationInfo(principals);
4.4 密码加密
4.4.1 哈希加密
java
// MD5 加密
Md5Hash md5Hash = new Md5Hash("password", "salt", 2);
String hashedPassword = md5Hash.toHex();
// SHA-256 加密
Sha256Hash sha256Hash = new Sha256Hash("password", "salt", 2);
String hashedPassword = sha256Hash.toHex();
// 自定义哈希
SimpleHash hash = new SimpleHash("SHA-256", "password", "salt", 2);
4.4.2 对称加密
java
// AES 加密
AesCipherService cipherService = new AesCipherService();
cipherService.setKeySize(128);
byte[] key = cipherService.generateNewKey().getEncoded();
byte[] encrypted = cipherService.encrypt("plaintext".getBytes(), key).getBytes();
byte[] decrypted = cipherService.decrypt(encrypted, key).getBytes();
4.5 多 Realm 配置
java
// 配置多个 Realm
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
List<Realm> realms = new ArrayList<>();
realms.add(databaseRealm);
realms.add(ldapRealm);
realms.add(activeDirectoryRealm);
authenticator.setRealms(realms);
securityManager.setAuthenticator(authenticator);
// 认证策略
FirstSuccessfulStrategy strategy = new FirstSuccessfulStrategy();
authenticator.setAuthenticationStrategy(strategy);
5. 结合 Spring Boot 使用
5.1 依赖配置
xml
<dependencies>
<!-- Shiro Spring Boot Starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.9.1</version>
</dependency>
<!-- Shiro Web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.9.1</version>
</dependency>
<!-- Shiro Ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.9.1</version>
</dependency>
</dependencies>
5.2 配置文件
5.2.1 application.yml
yaml
shiro:
web:
enabled: true
urls:
/login: anon
/logout: logout
/static/**: anon
/**: authc
session-manager:
session-id-cookie-enabled: true
session-id-url-rewriting-enabled: false
cache-manager:
cache-manager: ehCacheManager
5.2.2 shiroFilter.properties
properties
# 登录页面
/login=anon
# 登出
/logout=logout
# 静态资源
/static/**=anon
/**.js=anon
/**.css=anon
/**.png=anon
# API 文档
/swagger-ui/**=anon
/v2/api-docs=anon
# 所有其他路径需要认证
/**=authc
5.3 配置类
5.3.1 Shiro 配置类
java
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置登录成功页面
shiroFilterFactoryBean.setSuccessUrl("/index");
// 设置未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// 配置过滤器链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/swagger-ui/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(cacheManager());
return securityManager;
}
@Bean
public Realm realm() {
CustomRealm realm = new CustomRealm();
realm.setCredentialsMatcher(credentialsMatcher());
realm.setCachingEnabled(true);
realm.setAuthenticationCachingEnabled(true);
realm.setAuthorizationCachingEnabled(true);
return realm;
}
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("SHA-256");
matcher.setHashIterations(2);
matcher.setStoredCredentialsHexEncoded(true);
return matcher;
}
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(1800000); // 30分钟
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true);
return sessionManager;
}
@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
}
5.3.2 自定义 Realm
java
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
// 查询用户信息
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
if (!user.isEnabled()) {
throw new DisabledAccountException("用户已被禁用");
}
// 返回认证信息
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userService.findByUsername(username);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 设置角色
Set<String> roles = userService.getRolesByUsername(username);
authorizationInfo.setRoles(roles);
// 设置权限
Set<String> permissions = userService.getPermissionsByUsername(username);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
}
5.4 控制器示例
java
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/user/info")
public Result<UserInfo> getUserInfo() {
Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
UserInfo userInfo = userService.getUserInfo(username);
return Result.success(userInfo);
}
@PostMapping("/user/change-password")
@RequiresAuthentication
public Result<String> changePassword(@RequestBody ChangePasswordRequest request) {
Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
userService.changePassword(username, request.getOldPassword(), request.getNewPassword());
return Result.success("密码修改成功");
}
@GetMapping("/admin/users")
@RequiresRoles("admin")
public Result<List<User>> getUsers() {
List<User> users = userService.getAllUsers();
return Result.success(users);
}
@PostMapping("/admin/user/{id}/enable")
@RequiresPermissions("user:enable")
public Result<String> enableUser(@PathVariable Long id) {
userService.enableUser(id);
return Result.success("用户启用成功");
}
}
5.5 异常处理
java
@ControllerAdvice
public class ShiroExceptionHandler {
@ExceptionHandler(UnauthenticatedException.class)
public String handleUnauthenticatedException() {
return "redirect:/login";
}
@ExceptionHandler(UnauthorizedException.class)
public String handleUnauthorizedException() {
return "redirect:/403";
}
@ExceptionHandler(ExpiredCredentialsException.class)
public String handleExpiredCredentialsException() {
return "redirect:/login?error=expired";
}
@ExceptionHandler(IncorrectCredentialsException.class)
public String handleIncorrectCredentialsException() {
return "redirect:/login?error=incorrect";
}
}
6. 项目实战案例
6.1 项目结构
data-backend/
├── data-common/ # 公共模块
├── data-core/ # 核心模块
│ ├── config/
│ │ ├── ShiroConfig.java # Shiro 配置
│ │ └── GlobalExceptionHandler.java # 全局异常处理
│ ├── ecs/
│ │ └── MyEcsUserFilter.java # 自定义用户过滤器
│ └── shiro/
│ ├── realm/
│ │ ├── ShiroAuthServiceImpl.java
│ │ └── ShiroRoleServiceImpl.java
│ └── filter/
└── data-web/ # Web 模块
├── controller/
├── service/
└── resources/
└── shiroFilter.properties # 过滤器配置
6.2 核心配置实现
6.2.1 ShiroConfig.java
java
@Configuration
@EnableAutoConfiguration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactory shiroFilter = new ShiroFilterFactory();
ShiroFilterFactoryBean shiroFilterFactoryBean =
shiroFilter.shiroFilterFactoryBean(securityManager,
shiroAuthService(), shiroRoleService());
// 添加自定义过滤器
Map<String, Filter> shiroFilterMap = shiroFilterFactoryBean.getFilters();
shiroFilterMap.put("anyRoles", anyRolesAuthorizationFilter());
shiroFilterMap.put("anyPerms", anyPermissionsAuthorizationFilter());
shiroFilterMap.put("deny", new DenyAccessFilter());
// 自定义用户过滤器
Map<String, Filter> ecsFilterMap = shiroFilterFactoryBean.getFilters();
MyEcsUserFilter myEcsUserFilter = new MyEcsUserFilter();
ecsFilterMap.put("user", myEcsUserFilter);
shiroFilterFactoryBean.setFilters(ecsFilterMap);
return shiroFilterFactoryBean;
}
@Bean
public ShiroAuthService shiroAuthService() {
return new ShiroAuthServiceImpl();
}
@Bean
public ShiroRoleService shiroRoleService() {
return new ShiroRoleServiceImpl();
}
// 自定义角色过滤器
public AnyRolesAuthorizationFilter anyRolesAuthorizationFilter() {
return new AnyRolesAuthorizationFilter();
}
// 自定义权限过滤器
public AnyPermissionsAuthorizationFilter anyPermissionsAuthorizationFilter() {
return new AnyPermissionsAuthorizationFilter();
}
// 禁止访问过滤器
public static class DenyAccessFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpResponse.setContentType("text/plain;charset=UTF-8");
httpResponse.getWriter().write("Access Denied - 访问被拒绝");
}
}
}
6.2.2 MyEcsUserFilter.java
java
public class MyEcsUserFilter extends EcsUserFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
// 保存重定向 URL
String initUrl = request.getParameter(Constants.LOGIN_SUCCESS_REDIRECT_PARAM);
if (!StringUtils.isEmpty(initUrl)) {
SecurityUtils.getSubject().getSession()
.setAttribute(Constants.LOGIN_SUCCESS_REDIRECT_PARAM, initUrl);
}
// 重定向到登录页面
this.saveRequestAndRedirectToLogin(request, response);
return false;
}
}
6.3 权限控制实现
6.3.1 控制器权限控制
java
@RestController
@RequestMapping("/api/demo")
public class DemoController {
@PostMapping("/V1")
public Result<Res> V1(
@RequestBody DTO dto) {
// 获取当前用户信息
UserInfoVO user = userService.getUserInfo();
Assert.notNull(user, "用户未登录");
// 角色权限检查
if (!user.ifAreaManager()) {
return Result.error("当前角色非区管,无权限");
}
// 执行业务逻辑
Res result = AService.getSchoolCompliancePage(dto, user);
return Result.success(result);
}
}
6.4 配置文件
6.4.1 shiroFilter.properties
properties
# Swagger 相关路径 - 禁止访问
/swagger-ui.html=deny
/swagger-ui/**=deny
/v2/api-docs=deny
/swagger-resources/**=deny
/webjars/**=deny
# 包含 context-path 的 Swagger 路径
/data/swagger-ui.html=deny
/data/swagger-ui/**=deny
/data/v2/api-docs=deny
/data/swagger-resources/**=deny
/data-quality-monitor/webjars/**=deny
# 监控页面 - 禁止访问
/druid/**=deny
/data/druid/**=deny
/actuator/**=deny
/data/actuator/**=deny
/management/**=deny
/data/management/**=deny
# 所有其他路径需要用户认证
/**=user
6.5 最佳实践总结
6.5.1 安全配置最佳实践
- 最小权限原则:只授予必要的权限
- 分层权限控制:URL 级别 + 方法级别 + 数据级别
- 敏感路径保护:禁止访问监控和文档页面
- 会话安全:设置合理的会话超时时间
- 密码安全:使用强加密算法和盐值
6.5.2 性能优化建议
- 启用缓存:认证和授权信息缓存
- 会话存储:使用 Redis 等外部存储
- 连接池:数据库连接池优化
- 异步处理:耗时操作异步化
6.5.3 监控和日志
- 访问日志:记录用户访问行为
- 异常监控:监控认证和授权异常
- 性能监控:监控 Shiro 组件性能
- 安全审计:记录敏感操作日志
总结
Apache Shiro 是一个功能强大、易于使用的 Java 安全框架。通过本文的详细介绍,您应该能够:
- 理解 Shiro 的核心概念和架构
- 掌握核心类的源码实现
- 学会在 Spring Boot 中集成 Shiro
- 了解实际项目中的最佳实践
在实际项目中,建议根据具体需求选择合适的配置策略,并注重安全性和性能的平衡。同时,要定期更新 Shiro 版本,关注安全漏洞和性能优化。