Spring Security 與 Apache Shiro 比較

Security,Spring Security
Remote
1
05:19 AM · Nov 30 ,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. Configuring Apache Shiro

為了開始,讓我們看看配置在兩個框架之間是如何不同的。

3.1. Maven Dependencies

由於我們將 Shiro 用在 Spring Boot App 中,我們需要它的 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. Creating a Realm

為了聲明具有角色和權限的用户,我們需要創建一個擴展 Shiro 的 JdbcRealm 的 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 Security

現在我們來看如何在 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. Controllers and Endpoints

Now let’s have a look at our web controller mappings for the two applications. While they’ll use the same endpoints, some implementations will differ.

5.1. Endpoints for View Rendering

For endpoints rendering the view, the implementations are the same:

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

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

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

Both our controller implementations, Shiro as well as Spring Security, return the index.ftl on the root endpoint, login.ftl on the login endpoint, and home.ftl on the home endpoint.

However, the definition of the method addUserAttributes at the /home endpoint will differ between the two controllers. This method introspects the currently logged in user’s attributes.

Shiro provides a SecurityUtils#getSubject to retrieve the current Subject, and its roles and permissions:

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

On the other hand, Spring Security provides an Authentication object from its SecurityContextHolder‘s context for this purpose:

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 Login Endpoint

In Shiro, we map the credentials the user enters to a POJO:

public class UserCredentials {

    private String username;
    private String password;

    // getters and setters
}

Then we’ll create a UsernamePasswordToken to log the user, or Subject, in:

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

On the Spring Security side, this is just a matter of redirection to the home page. Spring’s logging-in process, handled by its UsernamePasswordAuthenticationFilter, is transparent to us:

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

5.3. Admin-Only Endpoint

Now let’s look at a scenario where we have to perform role-based access. Let’s say we have an /admin endpoint, access to which should only be allowed for the ADMIN role.

Let’s see how to do this in 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";
}

Here we extracted the currently logged in user, checked if they have the ADMIN role, and added content accordingly.

In Spring Security, there is no need for checking the role programmatically, we’ve already defined who can reach this endpoint in our SecurityConfig. So now, it’s just a matter of adding business logic:

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

5.4. Logout Endpoint

Finally, let’s implement the logout endpoint.

In Shiro, we’ll simply call Subject#logout:

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

For Spring, we’ve not defined any mapping for logout. In this case, its default logout mechanism kicks in, which is automatically applied since we created a SecurityFilterChain bean in our configuration.

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.