详细介绍: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 整体架构

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 接口

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 安全配置最佳实践
  1. 最小权限原则:只授予必要的权限
  2. 分层权限控制:URL 级别 + 方法级别 + 数据级别
  3. 敏感路径保护:禁止访问监控和文档页面
  4. 会话安全:设置合理的会话超时时间
  5. 密码安全:使用强加密算法和盐值
6.5.2 性能优化建议
  1. 启用缓存:认证和授权信息缓存
  2. 会话存储:使用 Redis 等外部存储
  3. 连接池:数据库连接池优化
  4. 异步处理:耗时操作异步化
6.5.3 监控和日志
  1. 访问日志:记录用户访问行为
  2. 异常监控:监控认证和授权异常
  3. 性能监控:监控 Shiro 组件性能
  4. 安全审计:记录敏感操作日志

总结

Apache Shiro 是一个功能强大、易于使用的 Java 安全框架。通过本文的详细介绍,您应该能够:

  1. 理解 Shiro 的核心概念和架构
  2. 掌握核心类的源码实现
  3. 学会在 Spring Boot 中集成 Shiro
  4. 了解实际项目中的最佳实践

在实际项目中,建议根据具体需求选择合适的配置策略,并注重安全性和性能的平衡。同时,要定期更新 Shiro 版本,关注安全漏洞和性能优化。

posted @ 2025-10-01 11:12  wzzkaifa  阅读(8)  评论(0)    收藏  举报