Spring Security vs Apache Shiro – Spring Security vs Apache Shiro

最后修改: 2020年 7月 13日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Security is a primary concern in the world of application development, especially in the area of enterprise web and mobile applications.

安全是应用开发领域的首要问题,特别是在企业网络和移动应用方面。

In this quick tutorial, we’ll compare two popular Java Security frameworks – Apache Shiro and Spring Security.

在这个快速教程中,我们将比较两个流行的Java安全框架–Apache ShiroSpring Security

2. A Little Background

2.小小的背景

Apache Shiro was born in 2004 as JSecurity and was accepted by the Apache Foundation in 2008. To date, it has seen many releases, the latest as of writing this is 1.5.3.

Apache Shiro作为JSecurity诞生于2004年,并在2008年被Apache基金会接受。到目前为止,它已经有了很多版本,截至本文写作时,最新的版本是1.5.3。

Spring Security started as Acegi in 2003 and was incorporated into the Spring Framework with its first public release in 2008. Since its inception, it has gone through several iterations and the current GA version as of writing this is 5.3.2.

Spring Security始于2003年的Acegi,在2008年首次公开发布时被纳入Spring框架。自成立以来,它已经经历了几次迭代,目前的GA版本是5.3.2。

Both technologies offer authentication and authorization support along with cryptography and session management solutions. Additionally, Spring Security provides first-class protection against attacks such as CSRF and session fixation.

这两项技术都提供了认证和授权支持,以及加密和会话管理解决方案。此外,Spring Security对CSRF和会话固定等攻击提供一流的保护。

In the next few sections, we’ll see examples of how the two technologies handle authentication and authorization. To keep things simple, we’ll be using basic Spring Boot based MVC applications with FreeMarker templates.

在接下来的几节中,我们将看到这两种技术如何处理认证和授权的例子。为了保持简单,我们将使用基本的基于Spring Boot的MVC应用程序和FreeMarker模板

3. Configuring Apache Shiro

3.配置Apache Shiro

To start with, let’s see how configurations differ between the two frameworks.

首先,让我们看看这两个框架的配置有何不同。

3.1. Maven Dependencies

3.1.Maven的依赖性

Since we’ll use Shiro in a Spring Boot App, we’ll need its starter and the shiro-core module:

由于我们将在Spring Boot应用程序中使用Shiro,我们需要它的启动器和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>

The latest versions can be found on Maven Central.

最新的版本可以在Maven Central上找到。

3.2. Creating a Realm

3.2.创建一个境界

To declare users with their roles and permissions in-memory, we need to create a realm extending Shiro’s JdbcRealm. We’ll define two users – Tom and Jerry, with roles USER and ADMIN, respectively:

为了在内存中声明用户及其角色和权限,我们需要创建一个扩展Shiro的JdbcRealm的境界。我们将定义两个用户–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")));
    }
}

Next, to enable retrieval of this authentication and authorization, we need to override a few methods:

接下来,为了能够检索到这个认证和授权,我们需要覆盖一些方法。

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

The method doGetAuthorizationInfo is using a couple of helper methods to get the user’s roles and permissions:

方法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;
}

Next, we need to include this CustomRealm as a bean in our Boot Application:

接下来,我们需要将这个CustomRealm作为Boot Application中的一个bean。

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

Additionally, to configure authentication for our endpoints, we need another bean:

此外,为了给我们的端点配置认证,我们需要另一个Bean。

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

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

Here, using a DefaultShiroFilterChainDefinition instance, we specified that our /home endpoint can only be accessed by authenticated users.

在这里,使用DefaultShiroFilterChainDefinition实例,我们指定我们的/home端点只能被认证的用户访问。

That’s all we need for the configuration, Shiro does the rest for us.

这就是我们所需要的配置,Shiro为我们做其余的事情。

4. Configuring Spring Security

4.配置Spring安全

Now let’s see how to achieve the same in Spring.

现在让我们看看如何在Spring中实现同样的目的。

4.1. Maven Dependencies

4.1.Maven的依赖性

First, the dependencies:

首先是依赖性。

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

The latest versions can be found on Maven Central.

最新的版本可以在Maven Central上找到。

4.2. Configuration Class

4.2.配置类

Next, we’ll define our Spring Security configuration in a class SecurityConfig, extending WebSecurityConfigurerAdapter:

接下来,我们将在一个SecurityConfig类中定义我们的Spring安全配置,扩展WebSecurityConfigurerAdapter

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
          .authorizeRequests(authorize -> authorize
            .antMatchers("/index", "/login").permitAll()
            .antMatchers("/home", "/logout").authenticated()
            .antMatchers("/admin/**").hasRole("ADMIN"))
          .formLogin(formLogin -> formLogin
            .loginPage("/login")
            .failureUrl("/login-error"));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
          .withUser("Jerry")
            .password(passwordEncoder().encode("password"))
            .authorities("READ", "WRITE")
            .roles("ADMIN")
            .and()
          .withUser("Tom")
            .password(passwordEncoder().encode("password"))
            .authorities("READ")
            .roles("USER");
    }

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

As we can see, we built an AuthenticationManagerBuilder object to declare our users with their roles and authorities. Additionally, we encoded the passwords using a BCryptPasswordEncoder.

我们可以看到,我们建立了一个AuthenticationManagerBuilder对象来声明我们的用户及其角色和权限。此外,我们使用BCryptPasswordEncoder对密码进行编码。

Spring Security also provides us with its HttpSecurity object for further configurations. For our example, we’ve allowed:

Spring Security还为我们提供了HttpSecurity对象,以便进一步配置。对于我们的例子,我们已经允许。

  • everyone to access our index and login pages
  • only authenticated users to enter the home page and logout
  • only users with ADMIN role to access the admin pages

We’ve also defined support for form-based authentication to send users to the login endpoint. In case login fails, our users will be redirected to /login-error.

我们还定义了对基于表单的认证的支持,将用户发送到login端点。如果登录失败,我们的用户将被重定向到/login-error

5. Controllers and Endpoints

5.控制器和端点

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

5.1.视图渲染的端点

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.

我们的控制器实现,Shiro以及Spring Security,都在根端点上返回index.ftl,在登录端点上返回login.ftl,在首页端点上返回home.ftl

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.

然而,在/home端点的方法addUserAttributes的定义在两个控制器之间会有所不同。这个方法反省了当前登录的用户的属性。

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

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

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

另一方面,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 Login Endpoint

5.2.POST登录端点

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

在Shiro中,我们将用户输入的凭证映射到一个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:

然后我们将创建一个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";
}

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:

在Spring安全方面,这只是一个重定向到主页的问题。Spring的登录过程,由其UsernamePasswordAuthenticationFilter处理,对我们是透明的

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

5.3. Admin-Only Endpoint

5.3.仅限管理员的端点

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.

现在让我们看一下我们必须执行基于角色的访问的情况。假设我们有一个/admin端点,只有ADMIN角色才可以访问它。

Let’s see how to do this in Shiro:

让我们看看如何在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.

在这里,我们提取了当前登录的用户,检查他们是否有ADMIN角色,并添加相应的内容。

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:

在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. Logout Endpoint

5.4.注销端点

Finally, let’s implement the logout endpoint.

最后,我们来实现注销端点。

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

在Shiro中,我们将简单地调用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 extended WebSecurityConfigurerAdapter in our configuration.

对于Spring,我们没有定义任何注销的映射。在这种情况下,它的默认注销机制就会启动,由于我们在配置中扩展了WebSecurityConfigurerAdapter,它就会自动应用。

6. Apache Shiro vs Spring Security

6.Apache Shiro vs Spring Security

Now that we’ve looked at the implementation differences, let’s look at a few other aspects.

现在我们已经看了实施方面的差异,让我们看看其他几个方面。

In terms of community support, the Spring Framework in general has a huge community of developers, actively involved in its development and usage. Since Spring Security is part of the umbrella, it must enjoy the same advantages. Shiro, though popular, does not have such humongous support.

在社区支持方面,Spring框架总体上有一个巨大的开发者社区,积极参与其开发和使用。既然Spring Security是伞状结构的一部分,它肯定也享有同样的优势。Shiro虽然很受欢迎,但没有这样巨大的支持。

Concerning documentation, Spring again is the winner.

在文件方面,Spring再次成为赢家。

However, there’s a bit of a learning curve associated with Spring Security. Shiro, on the other hand, is easy to understand. For desktop applications, configuration via shiro.ini is all the easier.

然而,与Spring Security相关的学习曲线有点多。而Shiro则很容易理解。对于桌面应用程序,通过shiro.ini进行配置是非常容易的。

But again, as we saw in our example snippets, Spring Security does a great job of keeping business logic and security separate and truly offers security as a cross-cutting concern.

但是,正如我们在示例片段中看到的那样,Spring Security在保持业务逻辑和安全分离方面做得很好,并且真正将安全作为一个跨领域的关注点。

7. Conclusion

7.结语

In this tutorial, we compared Apache Shiro with Spring Security.

在本教程中,我们比较了Apache Shiro和Spring Security

We’ve just grazed the surface of what these frameworks have to offer and there is a lot to explore further. There are quite a few alternatives out there such as JAAS and OACC. Still, with its advantages, Spring Security seems to be winning at this point.

我们只是掠过了这些框架所能提供的表面,还有很多东西需要进一步探索。现在有相当多的替代方案,如JAASOACC。尽管如此,凭借其优势,Spring Security似乎在这一点上获胜。

As always, source code is available over on GitHub.

一如既往,源代码可在GitHub上获取。