Apache Shiro 技术详解
Apache Shiro 技术详解
目录
- 1. Apache Shiro 简介
- 2. 架构流程图
- 2.1 Shiro 整体架构
- 2.2 认证流程
- 2.3 授权流程
- 2.4 CAS 单点登录认证成功流程
- 2.5 CAS 认证成功后的 Principal 生成流程
- 3. 核心类源码解析
- 4. 重难点分析
- 5. 结合 Spring Boot 使用
- 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 整体架构
2.2 认证流程
2.3 授权流程
2.4 CAS 单点登录认证成功流程
2.5 CAS 认证成功后的 Principal 生成流程
3. 核心类源码解析
3.1 Subject 接口
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 接口
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {// 登录Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;// 登出void logout(Subject subject);// 创建 SubjectSubject createSubject(SubjectContext context);// 获取 SubjectSubject getSubject(SubjectContext context);
}
3.3 DefaultWebSecurityManager 实现
public class DefaultWebSecurityManager extends DefaultSecurityManager {// 会话管理器private SessionManager sessionManager;// 会话存储private SessionStorageEvaluator sessionStorageEvaluator;// 会话模式private SessionMode sessionMode = SessionMode.HTTP;@Overrideprotected SubjectContext createSubjectContext() {WebSubjectContext wsc = new WebSubjectContext();wsc.setSessionMode(this.sessionMode);return wsc;}@Overrideprotected SubjectContext copy(SubjectContext subjectContext) {if (subjectContext instanceof WebSubjectContext) {return new WebSubjectContext(subjectContext);}return new WebSubjectContext(subjectContext);}
}
3.4 UserFilter 核心方法
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 接口
public interface Realm {// 获取 Realm 名称String getName();// 支持认证令牌boolean supports(AuthenticationToken token);// 获取认证信息AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}
3.6 AuthorizingRealm 抽象类
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;@Overrideprotected 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 会话生命周期
// 创建会话
Session session = subject.getSession();
session.setAttribute("key", "value");// 会话超时设置
session.setTimeout(1800000); // 30分钟// 会话销毁
session.stop();
4.2.2 会话存储策略
// 内存存储(默认)
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 认证缓存
// 启用认证缓存
realm.setAuthenticationCachingEnabled(true);
realm.setAuthenticationCacheName("authenticationCache");// 缓存配置
CacheManager cacheManager = new MemoryConstrainedCacheManager();
securityManager.setCacheManager(cacheManager);
4.3.2 授权缓存
// 启用授权缓存
realm.setAuthorizationCachingEnabled(true);
realm.setAuthorizationCacheName("authorizationCache");// 缓存失效
realm.clearCachedAuthorizationInfo(principals);
4.4 密码加密
4.4.1 哈希加密
// 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 对称加密
// 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 配置
// 配置多个 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 依赖配置
<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
shiro:web:enabled: trueurls:/login: anon/logout: logout/static/**: anon/**: authcsession-manager:session-id-cookie-enabled: truesession-id-url-rewriting-enabled: falsecache-manager:cache-manager: ehCacheManager
5.2.2 shiroFilter.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 配置类
@Configuration
public class ShiroConfig {@Beanpublic 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;}@Beanpublic SecurityManager securityManager(Realm realm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(realm);securityManager.setSessionManager(sessionManager());securityManager.setCacheManager(cacheManager());return securityManager;}@Beanpublic Realm realm() {CustomRealm realm = new CustomRealm();realm.setCredentialsMatcher(credentialsMatcher());realm.setCachingEnabled(true);realm.setAuthenticationCachingEnabled(true);realm.setAuthorizationCachingEnabled(true);return realm;}@Beanpublic CredentialsMatcher credentialsMatcher() {HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("SHA-256");matcher.setHashIterations(2);matcher.setStoredCredentialsHexEncoded(true);return matcher;}@Beanpublic SessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setGlobalSessionTimeout(1800000); // 30分钟sessionManager.setDeleteInvalidSessions(true);sessionManager.setSessionValidationSchedulerEnabled(true);return sessionManager;}@Beanpublic CacheManager cacheManager() {return new MemoryConstrainedCacheManager();}
}
5.3.2 自定义 Realm
@Component
public class CustomRealm extends AuthorizingRealm {@Autowiredprivate UserService userService;@Overrideprotected 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());}@Overrideprotected 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 控制器示例
@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")@RequiresAuthenticationpublic 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 异常处理
@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
@Configuration
@EnableAutoConfiguration
public class ShiroConfig {@Beanpublic 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;}@Beanpublic ShiroAuthService shiroAuthService() {return new ShiroAuthServiceImpl();}@Beanpublic ShiroRoleService shiroRoleService() {return new ShiroRoleServiceImpl();}// 自定义角色过滤器public AnyRolesAuthorizationFilter anyRolesAuthorizationFilter() {return new AnyRolesAuthorizationFilter();}// 自定义权限过滤器public AnyPermissionsAuthorizationFilter anyPermissionsAuthorizationFilter() {return new AnyPermissionsAuthorizationFilter();}// 禁止访问过滤器public static class DenyAccessFilter implements Filter {@Overridepublic 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
public class MyEcsUserFilter extends EcsUserFilter {@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {// 保存重定向 URLString 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 控制器权限控制
@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
# 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 版本,关注安全漏洞和性能优化。