1. 概述

在企业级 Web 和移动应用开发中,安全(Security)是绕不开的核心问题。选择一个合适的安全框架,往往直接影响系统的可维护性与扩展性。

本文将对两个主流的 Java 安全框架 —— Apache ShiroSpring Security 进行横向对比,从配置方式、使用习惯到生态支持,帮你快速判断哪个更适合你的项目。

✅ 适用读者:已有 Spring 或安全框架使用经验的开发者
❌ 不适合:第一次接触认证授权概念的初学者(建议先补基础)


2. 背景简介

Apache Shiro

  • 起源于 2004 年,原名 JSecurity,2008 年进入 Apache 基金会
  • 当前稳定版本为 1.5.3(截至本文写作时)
  • 设计目标是简单、通用、可嵌入任意 Java 应用(Web、非 Web、Spring、非 Spring)

Spring Security

  • 前身为 2003 年的 Acegi Security,2008 年正式成为 Spring 项目的一部分
  • 当前 GA 版本为 5.3.2
  • 深度集成 Spring 生态,强调“安全即切面”(security as cross-cutting concern)

共同能力

两者均提供:

  • ✅ 认证(Authentication)
  • ✅ 授权(Authorization)
  • ✅ 加密(Cryptography)
  • ✅ 会话管理(Session Management)

但 Spring Security 更进一步,原生支持 CSRF 防护、会话固定攻击防御、OAuth2、OpenID Connect 等企业级特性,这是 Shiro 目前难以匹敌的。

⚠️ 注意:本文使用 Spring Boot + FreeMarker 模板引擎构建示例,便于统一比较。


3. Apache Shiro 配置实战

3.1 Maven 依赖

在 Spring Boot 项目中使用 Shiro,需引入以下依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
</dependency>

最新版本可参考 Maven Central

3.2 自定义 Realm

Shiro 的权限数据通过 Realm 提供。我们创建一个内存版的 CustomRealm,模拟两个用户:

  • Tom(角色:USER,权限:READ)
  • Jerry(角色:ADMIN,权限:READ + WRITE)
public class CustomRealm extends JdbcRealm {

    private Map<String, String> credentials = new HashMap<>();
    private Map<String, Set<String>> roles = new HashMap<>();
    private Map<String, Set<String>> permissions = new HashMap<>();

    {
        credentials.put("Tom", "password");
        credentials.put("Jerry", "password");

        roles.put("Jerry", new HashSet<>(Arrays.asList("ADMIN")));
        roles.put("Tom", new HashSet<>(Arrays.asList("USER")));

        permissions.put("ADMIN", new HashSet<>(Arrays.asList("READ", "WRITE")));
        permissions.put("USER", new HashSet<>(Arrays.asList("READ")));
    }
}

接下来重写认证与授权方法:

认证逻辑(doGetAuthenticationInfo

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
    throws AuthenticationException {
    
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    String username = userToken.getUsername();

    if (username == null || !credentials.containsKey(username)) {
        throw new UnknownAccountException("User doesn't exist");
    }

    return new SimpleAuthenticationInfo(
        username,
        credentials.get(username),
        getName()
    );
}

授权逻辑(doGetAuthorizationInfo

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    Set<String> roles = new HashSet<>();
    Set<String> perms = new HashSet<>();

    for (Object user : principals) {
        try {
            roles.addAll(getRoleNamesForUser(null, (String) user));
            perms.addAll(getPermissions(null, null, roles));
        } catch (SQLException e) {
            logger.error(e.getMessage());
        }
    }

    SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles);
    authInfo.setStringPermissions(perms);
    return authInfo;
}

辅助方法(获取角色与权限):

@Override
protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
    if (!roles.containsKey(username)) {
        throw new SQLException("User doesn't exist");
    }
    return roles.get(username);
}

@Override
protected Set<String> getPermissions(Connection conn, String username, Collection<String> roles) 
    throws SQLException {
    
    Set<String> userPermissions = new HashSet<>();
    for (String role : roles) {
        if (!permissions.containsKey(role)) {
            throw new SQLException("Role doesn't exist");
        }
        userPermissions.addAll(permissions.get(role));
    }
    return userPermissions;
}

3.3 注册 Bean

CustomRealm 注册为 Spring Bean:

@Bean
public Realm customRealm() {
    return new CustomRealm();
}

配置拦截链(哪些接口需要登录):

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition();
    filter.addPathDefinition("/home", "authc"); // 需认证
    filter.addPathDefinition("/**", "anon");     // 其他放行
    return filter;
}

✅ 至此,Shiro 配置完成。框架会自动处理登录、会话、权限校验。


4. Spring Security 配置实战

4.1 Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

最新版本见 Maven Central

4.2 安全配置类

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests(authorize -> authorize
                .antMatchers("/index", "/login").permitAll()
                .antMatchers("/home", "/logout").authenticated()
                .antMatchers("/admin/**").hasRole("ADMIN")
            )
            .formLogin(formLogin -> formLogin
                .loginPage("/login")
                .failureUrl("/login-error")
            );
        return http.build();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails jerry = User.withUsername("Jerry")
            .password(passwordEncoder().encode("password"))
            .authorities("READ", "WRITE")
            .roles("ADMIN")
            .build();

        UserDetails tom = User.withUsername("Tom")
            .password(passwordEncoder().encode("password"))
            .authorities("READ")
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(jerry, tom);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

关键点说明:

  • HttpSecurity 配置访问规则,清晰声明每个路径的权限
  • InMemoryUserDetailsManager 提供内存用户
  • ✅ 密码使用 BCryptPasswordEncoder 加密存储
  • ✅ 登录失败跳转 /login-error

⚠️ 注意:Spring Security 默认开启 CSRF,测试时建议关闭(如上所示)


5. 控制器与接口实现

5.1 页面渲染接口

两者共用以下接口:

@GetMapping("/")
public String index() {
    return "index";
}

@GetMapping("/login")
public String showLoginPage() {
    return "login";
}

@GetMapping("/home")
public String getMeHome(Model model) {
    addUserAttributes(model);
    return "home";
}

模板文件:index.ftl, login.ftl, home.ftl

Shiro 获取用户信息

private void addUserAttributes(Model model) {
    Subject currentUser = SecurityUtils.getSubject();
    String permission = "";

    if (currentUser.hasRole("ADMIN")) {
        model.addAttribute("role", "ADMIN");
    } else if (currentUser.hasRole("USER")) {
        model.addAttribute("role", "USER");
    }

    if (currentUser.isPermitted("READ")) permission += " READ";
    if (currentUser.isPermitted("WRITE")) permission += " WRITE";

    model.addAttribute("username", currentUser.getPrincipal());
    model.addAttribute("permission", permission.trim());
}

Spring Security 获取用户信息

private void addUserAttributes(Model model) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null && !(auth instanceof AnonymousAuthenticationToken)) {
        User user = (User) auth.getPrincipal();
        model.addAttribute("username", user.getUsername());

        for (GrantedAuthority authority : user.getAuthorities()) {
            String authStr = authority.getAuthority();
            if (authStr.contains("USER")) {
                model.addAttribute("role", "USER");
                model.addAttribute("permissions", "READ");
            } else if (authStr.contains("ADMIN")) {
                model.addAttribute("role", "ADMIN");
                model.addAttribute("permissions", "READ WRITE");
            }
        }
    }
}

✅ 小贴士:Spring 的 Authentication 对象更规范,适合做统一上下文处理


5.2 登录接口(POST)

Shiro 手动处理登录

@PostMapping("/login")
public String doLogin(HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {
    Subject subject = SecurityUtils.getSubject();
    if (!subject.isAuthenticated()) {
        UsernamePasswordToken token = new UsernamePasswordToken(
            credentials.getUsername(), 
            credentials.getPassword()
        );
        try {
            subject.login(token);
        } catch (AuthenticationException ae) {
            logger.error(ae.getMessage());
            attr.addFlashAttribute("error", "Invalid Credentials");
            return "redirect:/login";
        }
    }
    return "redirect:/home";
}

Spring Security 自动处理

@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
    // 登录由 UsernamePasswordAuthenticationFilter 自动处理
    return "redirect:/home";
}

✅ 赢家:Spring Security —— 登录流程完全透明,业务代码零侵入


5.3 管理员专用接口

Shiro:手动校验角色

@GetMapping("/admin")
public String adminOnly(ModelMap modelMap) {
    addUserAttributes(modelMap);
    Subject currentUser = SecurityUtils.getSubject();
    if (currentUser.hasRole("ADMIN")) {
        modelMap.addAttribute("adminContent", "only admin can view this");
    }
    return "home";
}

Spring Security:配置即生效

@GetMapping("/admin")
public String adminOnly(HttpServletRequest req, Model model) {
    addUserAttributes(model);
    model.addAttribute("adminContent", "only admin can view this");
    return "home";
}

✅ 春风拂面:权限控制写在配置里,业务逻辑干净清爽


5.4 注销接口

Shiro 手动调用 logout

@PostMapping("/logout")
public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/";
}

Spring Security 自动处理

无需编写 Controller 方法。只要配置了 SecurityFilterChain,默认的 /logout 接口已自动启用。

✅ 再次体现:Spring Security 的“约定优于配置”哲学


6. 框架对比总结

维度 Apache Shiro Spring Security
✅ 学习曲线 简单直观,上手快 较陡,概念多(Filter、Provider、Token 等)
✅ 集成难度 轻量,可独立使用 强依赖 Spring,非 Spring 项目难用
✅ 功能丰富度 基础功能完备 支持 OAuth2、SAML、LDAP、CAS 等企业级特性
✅ 社区生态 小而美 巨大,文档全,Stack Overflow 回答多
✅ 权限控制方式 代码中手动判断(易混乱) 配置驱动,AOP 式切面控制(推荐)
✅ CSRF 防护 无原生支持 开箱即用

一句话总结

  • Shiro 适合轻量级项目、非 Spring 环境、快速接入
  • Spring Security 适合中大型项目、Spring Boot 生态、需要高级安全特性

⚠️ 踩坑提醒:不要在业务代码里写 if (hasRole("ADMIN")),这迟早会变成技术债。用配置驱动才是正道。


7. 结论

本文通过真实代码对比,展示了 Shiro 与 Spring Security 在配置、认证、授权、登出等环节的差异。

虽然 Shiro 简单易懂,但 Spring Security 凭借其强大的生态、声明式安全控制和企业级特性支持,已成为当前 Java 安全框架的事实标准

对于新项目,尤其是基于 Spring Boot 的应用,强烈推荐直接上 Spring Security,避免后期迁移成本。

🔗 示例源码已托管至 GitHub:https://github.com/yourname/tutorials/tree/master/security-modules/apache-shiro


原始标题:Spring Security vs Apache Shiro