知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 與 Apache Shiro 比較

Security,Spring Security
HongKong
5
12:52 PM · Dec 06 ,2025

1. 概述

安全性是應用程序開發領域,尤其是在企業級 Web 和移動應用程序領域中的首要關注點。

在本快速教程中,我們將比較兩個流行的 Java 安全框架——Apache ShiroSpring Security

2. 背景介紹

Apache Shiro 起源於2004年,當時名為JSecurity,並在2008年被Apache基金會接受。截至本文撰寫時,它已經發布了多個版本,最新版本為1.5.3。

Spring Security始於2003年的Acegi,並在2008年與Spring框架一起發佈了其首次公共版本。自成立以來,它經歷了多個迭代版本,截至本文撰寫時,當前GA版本為5.3.2。

這兩種技術都提供 身份驗證和授權支持,以及密碼學和會話管理解決方案。 此外,Spring Security還提供對諸如CSRF和會話固定等攻擊的“第一級”保護。

在接下來的幾個部分中,我們將看到這兩種技術如何處理身份驗證和授權的示例。為了保持簡單,我們將使用基於Spring Boot的MVC應用程序以及FreeMarker模板。

3. 配置 Apache Shiro

為了開始,讓我們看看兩種框架之間的配置差異。

3.1. Maven 依賴

由於我們將 Shiro 用在 Spring Boot 應用中,因此需要它的 starter 和 shiro-core 模塊:

<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 的 <em>JdbcRealm</em> 的 Realm。我們將定義兩個用户——Tom 和 Jerry,分別擁有 USER 和 ADMIN 角色:

public class CustomRealm extends JdbcRealm {

    private Map<String, String> credentials = new HashMap<>();
    private Map<String, Set> roles = new HashMap<>();
    private Map<String, Set> 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")));
    }
}

接下來,為了啓用對該身份驗證和授權的檢索,我們需要覆蓋幾個方法:

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

    if (userToken.getUsername() == null || userToken.getUsername().isEmpty() ||
      !credentials.containsKey(userToken.getUsername())) {
        throw new UnknownAccountException("User doesn't exist");
    }
    return new SimpleAuthenticationInfo(userToken.getUsername(), 
      credentials.get(userToken.getUsername()), getName());
}

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

    for (Object user : principals) {
        try {
            roles.addAll(getRoleNamesForUser(null, (String) user));
            permissions.addAll(getPermissions(null, null, roles));
        } catch (SQLException e) {
            logger.error(e.getMessage());
        }
    }
    SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles);
    authInfo.setStringPermissions(permissions);
    return authInfo;
}

方法 doGetAuthorizationInfo 使用了幾個輔助方法來獲取用户的角色和權限:

@Override
protected Set 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 getPermissions(Connection conn, String username, Collection roles) 
  throws SQLException {
    Set 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;
}

接下來,我們需要將該 CustomRealm 作為 Bean 包含到我們的 Boot Application 中:

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

此外,為了配置我們端點的身份驗證,我們還需要另一個 Bean:

@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/home", "authc");
    filter.addPathDefinition("/**", "anon");
    return filter;
}

在這裏,我們使用了一個 DefaultShiroFilterChainDefinition 實例,指定我們的 /home 端點只能被認證用户訪問。

對於配置,這已經足夠了,Shiro 會自動處理其餘部分。

4. 配置 Spring 安全

現在讓我們看看如何在 Spring 中實現相同的功能。

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. 配置類

接下來,我們將 Spring Security 的配置定義在一個名為 SecurityConfig 的類中:

@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() throws Exception {
        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();
    }

}

如我們所見,我們構建了一個 UserDetails 對象來聲明我們的用户及其角色和權限。此外,我們使用 BCryptPasswordEncoder 對密碼進行了編碼。

Spring Security 還為我們提供了 HttpSecurity 對象,用於進一步配置。對於我們的示例,我們允許:

  • 任何人訪問我們的 indexlogin 頁面
  • 只有經過身份驗證的用户才能進入 home 頁面和 logout 頁面
  • 只有具有 ADMIN 角色用户才能訪問 admin 頁面

我們還定義了對基於表單身份驗證的支持,將用户重定向到 login 端點。如果身份驗證失敗,我們的用户將被重定向到 /login-error

5. 控制器和端點

現在讓我們來查看這兩個應用程序的 Web 控制器映射。 儘管它們將使用相同的端點,但某些實現可能會有所不同。

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";
}

我們雙方的控制器實現,Shiro 以及 Spring Security,都會在根端點返回 index.ftl,在登錄端點返回 login.ftl,在主頁端點返回 home.ftl

然而,在主頁端點(/home)addUserAttributes方法的定義將在這兩個控制器之間有所不同。該方法會檢查當前已登錄用户的屬性。

Shiro 提供了一個 SecurityUtils#getSubject 用於檢索當前的 Subject 以及其角色和權限:

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 = permission + " READ";
    }
    if (currentUser.isPermitted("WRITE")) {
        permission = permission + " WRITE";
    }
    model.addAttribute("username", currentUser.getPrincipal());
    model.addAttribute("permission", permission);
}

另一方面,Spring Security 從其 SecurityContextHolder 的上下文中提供一個 Authentication 對象,用於此目的:

private void addUserAttributes(Model model) {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null && !auth.getClass().equals(AnonymousAuthenticationToken.class)) {
        User user = (User) auth.getPrincipal();
        model.addAttribute("username", user.getUsername());
        Collection<GrantedAuthority> authorities = user.getAuthorities();

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

5.2. POST 登錄端點

在 Shiro 中,我們將用户輸入的憑據映射到一個 POJO:

public class UserCredentials {

    private String username;
    private String password;

    // getters and setters
}

然後,我們將創建一個 UsernamePasswordToken 以登錄用户,或使用 Subject

@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 方面,這只是簡單的重定向到主頁的問題。Spring 的身份驗證流程,由其 <em >UsernamePasswordAuthenticationFilter</em> 處理,對我們來説是透明的。

@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
    return "redirect:/home";
}

5.3. 僅限管理員接口

現在,我們來看一個需要進行基於角色的訪問的場景。假設我們有一個 /admin 接口,只有 ADMIN 角色才能訪問。

讓我們看看如何在 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";
}

我們提取了當前已登錄的用户,檢查其是否具有 ADMIN 角色,並據此添加了相應內容。

在 Spring Security 中,無需通過編程方式檢查用户角色,我們已經在 SecurityConfig 中定義了哪些用户可以訪問該端點。因此,現在只需要添加業務邏輯即可:

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

5.4. 註銷端點

最後,讓我們實現註銷端點。

在 Shiro 中,我們只需調用 Subject#logout

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

對於 Spring,我們未定義任何註銷映射。在這種情況下,其默認註銷機制會啓動,因為我們已經在配置中創建了一個 SecurityFilterChain Bean。

6. Apache Shiro 與 Spring Security

現在我們已經瞭解了實現差異,接下來讓我們探討一些其他方面。

在社區支持方面,Spring Framework 總體擁有龐大的開發者社區,積極參與其開發和使用。由於 Spring Security 是 Spring Framework 的一部分,因此它也必須享受同樣的優勢。儘管 Shiro 比較流行,但其社區支持遠不及 Spring Security 強大。

在文檔方面,Spring 再次勝出。

然而,Spring Security 存在一定的學習曲線。 相比之下,Shiro 則易於理解。 對於桌面應用程序,通過 shiro.ini 進行配置更加簡單。

但正如我們在示例代碼片段中所看到的,Spring Security 能夠很好地將業務邏輯和安全功能分離,真正將安全作為一種橫向關注點。

7. 結論

在本教程中,我們對比了 Apache Shiro 與 Spring Security

我們只是對這些框架提供了初步的瞭解,還有很多地方值得進一步探索。 還有許多其他的替代方案,例如 JAASOACC。 儘管如此,憑藉其優勢,Spring Security 在目前看來似乎佔據了主導地位。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.