Java 安全框架Apache Shiro和Spring Security比较

1. 概述

安全性是应用程序开发领域的主要关注点,尤其是在企业 Web 和移动应用程序领域。

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

2. 一点背景

Apache Shiro 诞生于 2004 年,当时名为 JSecurity,并于 2008 年被 Apache 基金会接受。迄今为止,它已经发布了许多版本,截至撰写本文时最新的是 1.5.3。

Spring Security 于 2003 年作为 Acegi 开始,并在 2008 年首次公开发布时并入 Spring Framework。自成立以来,它已经经历了多次迭代,截至撰写本文时,当前的 GA 版本是 5.3.2。

两种技术都提供身份验证和授权支持以及加密和会话管理解决方案。此外,Spring Security 提供一流的保护,以抵御 CSRF 和会话固定等攻击。

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

3. 配置 Apache Shiro

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

3.1。Maven 依赖项

由于我们将在 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>

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

3.2. 创建领域

为了在内存中声明用户的角色和权限,我们需要创建一个扩展 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")));
    }
}

接下来,要启用此身份验证和授权的检索,我们需要重写一些方法:

@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 包含在我们的引导应用程序中:

@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. 配置类

接下来,我们将在类SecurityConfig中定义我们的 Spring Security 配置,扩展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();
    }
}

如我们所见,我们构建了一个AuthenticationManagerBuilder对象来声明我们的用户及其角色和权限。此外,我们使用BCryptPasswordEncoder对密码进行了编码。

Spring Security 还为我们提供了它的HttpSecurity对象以进行进一步的配置。对于我们的示例,我们允许:

  • 每个人都可以访问我们的索引登录页面
  • 只有经过身份验证的用户才能进入主页注销
  • 只有具有 ADMIN 角色的用户才能访问管理页面

我们还定义了对基于表单的身份验证的支持,以将用户发送到登录端点。如果登录失败,我们的用户将被重定向到/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 的登录过程,由它的UsernamePasswordAuthenticationFilter处理,对我们来说是透明的

@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,我们没有为注销定义任何映射。在这种情况下,它的默认注销机制启动,这是自动应用的,因为我们在配置中扩展了WebSecurityConfigurerAdapter

6. Apache Shiro 与 Spring Security

现在我们已经了解了实现差异,让我们看看其他一些方面。

在社区支持方面,Spring Framework 总体上拥有庞大的开发者社区,积极参与其开发和使用。由于 Spring Security 是保护伞的一部分,它必须享有同样的优势。Shiro虽然很受欢迎,但没有如此巨大的支持。

关于文档,Spring 再次成为赢家。

但是,有一点与 Spring Security 相关的学习曲线。另一方面,Shiro 很容易理解。对于桌面应用程序,通过shiro.ini进行配置更加容易。

但是,正如我们在示例代码片段中看到的那样, Spring Security 在保持业务逻辑和安全性分离方面做得很好, 并且真正将安全性作为横切关注点提供。

7. 结论

在本教程中,我们将 Apache Shiro 与 Spring Security 进行了比较

我们刚刚了解了这些框架所提供的功能,还有很多需要进一步探索。有很多替代品,例如JAASOACC。尽管如此,凭借其优势,Spring Security似乎在这一点上取得了胜利。

与往常一样,源代码可在 GitHub 上获得。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值