详细介绍:Apache Shiro 技术详解
Apache Shiro 技术详解
目录
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);
// 创建 Subject
Subject createSubject(SubjectContext context);
// 获取 Subject
Subject getSubject(SubjectContext context);
}
3.3 DefaultWebSecurityManager 实现
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 核心方法
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;
@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 会话生命周期
// 创建会话
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: 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
# 登录页面
/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 {
@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
@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 控制器示例
@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 异常处理
@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 {
@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
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 控制器权限控制
@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 版本,关注安全漏洞和性能优化。