1. 概述

本文将介绍 Apache Shiro —— 一个功能强大的 Java 安全框架。该框架具有高度可定制性和模块化特点,提供认证、授权、加密和会话管理四大核心功能。

Shiro 的设计哲学是简单粗暴:让安全控制变得直观易用,无论是 Web 应用、独立应用还是分布式系统都能轻松集成。

2. 依赖配置

Apache Shiro 提供多个模块,但本教程仅使用核心模块 shiro-core。在 pom.xml 中添加依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

最新版本可在 Maven Central 获取。

3. 安全管理器配置

SecurityManager 是 Shiro 框架的核心组件,应用中通常只存在一个实例。本教程以桌面环境为例进行演示。

在资源文件夹创建 shiro.ini 配置文件:

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

配置说明:

  • [users] 定义用户凭证:用户名 = 密码, 角色1, 角色2...
  • [roles] 定义角色权限:
    • admin 拥有所有权限(* 通配符)
    • editor 拥有文章相关全部权限
    • author 仅能撰写和保存文章

通过 IniRealm 加载配置并初始化 SecurityManager

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

现在 SecurityManager 已加载用户凭证和角色定义,接下来进行认证和授权操作。

4. 认证机制

在 Shiro 中,Subject 表示与系统交互的任何实体(用户、脚本或 REST 客户端)。通过 SecurityUtils.getSubject() 获取当前主体 currentUser

认证流程实现:

if (!currentUser.isAuthenticated()) {               
  UsernamePasswordToken token                       
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);                        
  try {                                             
      currentUser.login(token);                       
  } catch (UnknownAccountException uae) {           
      log.error("用户名不存在!", uae);        
  } catch (IncorrectCredentialsException ice) {     
      log.error("密码错误!", ice);       
  } catch (LockedAccountException lae) {            
      log.error("账户已锁定!", lae);    
  } catch (AuthenticationException ae) {            
      log.error("认证异常!", ae);           
  }                                                 
}

关键步骤:

  1. 检查当前主体是否未认证
  2. 创建包含用户名和密码的令牌
  3. 调用 login() 进行认证
  4. 捕获特定异常处理不同错误场景

⚠️ 注意:可通过继承 AccountException 创建自定义异常。

5. 授权机制

认证验证用户身份,授权控制资源访问权限。回顾 shiro.ini 中的角色定义:

角色 权限范围
admin 所有权限 (*)
editor 文章相关全部权限 (articles:*)
author 仅撰写和保存文章

根据角色显示欢迎信息:

if (currentUser.hasRole("admin")) {       
    log.info("欢迎管理员");              
} else if(currentUser.hasRole("editor")) {
    log.info("欢迎编辑!");           
} else if(currentUser.hasRole("author")) {
    log.info("欢迎作者");            
} else {                                  
    log.info("欢迎访客");             
}

检查具体操作权限:

if(currentUser.isPermitted("articles:compose")) {            
    log.info("可撰写文章");                    
} else {                                                     
    log.info("无撰写权限!");
}                                                            
                                                             
if(currentUser.isPermitted("articles:save")) {               
    log.info("可保存文章");                         
} else {                                                     
    log.info("无保存权限");                   
}                                                            
                                                             
if(currentUser.isPermitted("articles:publish")) {            
    log.info("可发布文章");                      
} else {                                                     
    log.info("无发布权限");                
}

6. 领域配置

实际应用中,用户凭证应从数据库而非 shiro.ini 获取。这时需要自定义 Realm —— Shiro 中用于访问安全数据的 DAO。

创建自定义领域继承 JdbcRealm

public class MyCustomRealm extends JdbcRealm {
    // 模拟数据库存储
    private Map<String, String> credentials = new HashMap<>();
    private Map<String, Set<String>> roles = new HashMap<>();
    private Map<String, Set<String>> perm = new HashMap<>();

    {
        credentials.put("user", "password");
        credentials.put("user2", "password2");
        credentials.put("user3", "password3");
                                          
        roles.put("user", new HashSet<>(Arrays.asList("admin")));
        roles.put("user2", new HashSet<>(Arrays.asList("editor")));
        roles.put("user3", new HashSet<>(Arrays.asList("author")));
                                                             
        perm.put("admin", new HashSet<>(Arrays.asList("*")));
        perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
        perm.put("author", 
          new HashSet<>(Arrays.asList("articles:compose", 
          "articles:save")));
    }
}

重写认证方法:

protected AuthenticationInfo 
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {
                                                                 
    UsernamePasswordToken uToken = (UsernamePasswordToken) token;
                                                                
    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("用户名不存在!");
    }
                                        
    return new SimpleAuthenticationInfo(
      uToken.getUsername(), 
      credentials.get(uToken.getUsername()), 
      getName()); 
}

✅ 密码加盐处理(推荐):

return new SimpleAuthenticationInfo(
  uToken.getUsername(), 
  credentials.get(uToken.getUsername()), 
  ByteSource.Util.bytes("salt"), 
  getName()
);

最后替换默认领域:

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

默认使用 SimpleCredentialsMatcher 进行凭证匹配。若使用哈希密码,需配置 HashedCredentialsMatcher(详见官方文档)。

7. 登出操作

登出只需调用单个方法,使会话失效并清除用户状态:

currentUser.logout();

8. 会话管理

Shiro 提供企业级会话管理:

  • Web 环境:默认使用 HttpSession
  • 独立应用:使用内置会话系统

会话操作示例:

Session session = currentUser.getSession();                
session.setAttribute("key", "value");                      
String value = (String) session.getAttribute("key");       
if (value.equals("value")) {                               
    log.info("获取到正确值! [" + value + "]");
}

9. Spring Web 应用集成

将 Shiro 集成到 Spring Boot 应用(重点在 Shiro 配置,非 Spring 实现)。

9.1 依赖配置

添加 Spring Boot 父依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
</parent>

添加核心依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

9.2 配置类

shiro-spring-boot-web-starter 自动配置基础功能,但仍需自定义领域和过滤器链:

@Bean
public Realm realm() {
    return new MyCustomRealm();
}
    
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc"); // 需认证
    filter.addPathDefinition("/**", "anon");      // 允许匿名访问

    return filter;
}

默认过滤器说明:

  • authc:表单认证
  • anon:匿名访问

更多默认过滤器见官方文档

application.properties 覆盖默认配置:

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

9.3 认证与授权实现

创建控制器处理登录逻辑:

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "凭证无效");
        return "redirect:/login";
    }
}

return "redirect:/secure";

安全页面处理授权逻辑:

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = "管理员权限";
} else if(currentUser.hasRole("editor")) {
    role = "编辑权限";
} else if(currentUser.hasRole("author")) {
    role = "作者权限";
}

if(currentUser.isPermitted("articles:compose")) {
    permission += "可撰写文章, ";
} else {
    permission += "无撰写权限!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission += "可保存文章, ";
} else {
    permission += "无保存权限, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission += "可发布文章";
} else {
    permission += "无发布权限";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

✅ 完成!Shiro 已成功集成到 Spring Boot 应用。可结合注解实现更细粒度的安全控制。

10. JEE 集成

在 JEE 应用中集成 Shiro 只需配置 web.xml。默认读取类路径下的 shiro.ini 文件。详细配置示例见官方文档,JSP 标签库见此处

11. 总结

本文深入探讨了 Apache Shiro 的认证授权机制,重点介绍了:

  • ✅ 核心组件 SecurityManager 的配置
  • ✅ 自定义 Realm 实现数据源集成
  • ✅ 会话管理与企业级应用集成
  • ✅ Spring Boot 环境下的最佳实践

完整源代码可在 GitHub 获取。踩坑提示:实际项目中务必使用哈希密码加盐存储,避免明文密码风险!


原始标题:Introduction to Apache Shiro