Рубрики
Без рубрики

Spring Security против Apache Shiro

Узнайте о Spring Security и Apache Shiro.

Автор оригинала: Sampada Wagde.

1. Обзор

Безопасность является основной проблемой в мире разработки приложений, особенно в области корпоративных веб-и мобильных приложений.

В этом кратком руководстве мы сравним две популярные платформы безопасности Java – Apache Shiro и Spring Security .

2. Немного Предыстории

Apache Shiro родился в 2004 году в качестве безопасности и был принят фондом Apache в 2008 году. На сегодняшний день он видел много релизов, последний на момент написания этой статьи-1.5.3.

Spring Security началась как Acegi в 2003 году и была включена в структуру Spring с ее первым публичным выпуском в 2008 году. С момента своего создания он прошел несколько итераций, и текущая версия GA на момент написания этой статьи-5.3.2.

Обе технологии предлагают поддержку аутентификации и авторизации, а также решения для криптографии и управления сеансами . Кроме того, Spring Security обеспечивает первоклассную защиту от таких атак, как CSRF и фиксация сеанса.

В следующих нескольких разделах мы рассмотрим примеры того, как эти две технологии обрабатывают аутентификацию и авторизацию. Чтобы все было просто, мы будем использовать базовые приложения MVC на основе Spring Boot с шаблонами FreeMarker .

3. Настройка Apache Shiro

Для начала давайте посмотрим, как различаются конфигурации между двумя фреймворками.

3.1. Зависимости Maven

Поскольку мы будем использовать Shiro в приложении Spring Boot, нам понадобится его стартер и модуль shiro-core :


    org.apache.shiro
    shiro-spring-boot-web-starter
    1.5.3


    org.apache.shiro
    shiro-core
    1.5.3

Последние версии можно найти на Maven Central .

3.2. Создание области

Чтобы объявить пользователей с их ролями и разрешениями в памяти, нам нужно создать область, расширяющую JDBCRealm Shiro . Мы определим двух пользователей – Тома и Джерри, с ролями ПОЛЬЗОВАТЕЛЯ и АДМИНИСТРАТОРА соответственно:

public class CustomRealm extends JdbcRealm {

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

Затем нам нужно включить эту Пользовательскую область в качестве компонента в ваше загрузочное приложение:

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

Кроме того, чтобы настроить аутентификацию для наших конечных точек, нам нужен еще один компонент:

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

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

Здесь, используя экземпляр Default Shiro FilterChainDefinition , мы указали, что наша конечная точка home/| может быть доступна только аутентифицированным пользователям.

Это все, что нам нужно для настройки, остальное за нас сделает Широ.

4. Настройка безопасности Spring

Теперь давайте посмотрим, как добиться того же весной.

4.1. Зависимости Maven

Во-первых, зависимости:


    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-security

Последние версии можно найти на Maven Central .

4.2. Класс конфигурации

Далее мы определим нашу конфигурацию безопасности Spring в классе SecurityConfig , расширяющем 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 также предоставляет нам свой объект Http Security для дальнейших конфигураций. Для нашего примера мы позволили:

  • все желающие могут получить доступ к нашим индексам и логинам страницам
  • только аутентифицированные пользователи могут войти на домашнюю страницу и выйти из системы
  • доступ к страницам admin имеют только пользователи с ролью АДМИНИСТРАТОРА

Мы также определили поддержку аутентификации на основе форм для отправки пользователей в конечную точку login . В случае сбоя входа в систему наши пользователи будут перенаправлены на /ошибка входа в систему .

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

Обе наши реализации контроллера, Shiro, а также Spring Security, возвращают index.ftl на корневой конечной точке, login.ftl на конечной точке входа и home.ftl на домашней конечной точке.

Однако определение метода добавить атрибуты пользователя в конечной точке /home будет отличаться между двумя контроллерами. Этот метод интроспектирует атрибуты текущего пользователя, вошедшего в систему.

Shiro предоставляет SecurityUtils#getSubject для получения текущего Субъекта , а также его ролей и разрешений:

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 предоставляет для этой цели объект Authentication из своего контекста SecurityContextHolder :

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 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. Конечная точка ПОСЛЕ входа в систему

В Shiro мы сопоставляем учетные данные, которые вводит пользователь, с POJO:

public class UserCredentials {

    private String username;
    private String password;

    // getters and setters
}

Затем мы создадим UsernamePasswordToken для входа пользователя или субъекта в систему:

@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 это всего лишь вопрос перенаправления на домашнюю страницу. Процесс входа в систему Spring, обрабатываемый его UsernamePasswordAuthenticationFilter , прозрачен для нас :

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

5.3. Конечная точка только для администратора

Теперь давайте рассмотрим сценарий, в котором мы должны выполнять доступ на основе ролей. Допустим, у нас есть конечная точка /admin , доступ к которой должен быть разрешен только для роли АДМИНИСТРАТОРА.

Давайте посмотрим, как это сделать в Широ:

@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 нет необходимости проверять роль программно, мы уже определили, кто может достичь этой конечной точки в нашем 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 vs Spring Security

Теперь, когда мы рассмотрели различия в реализации, давайте рассмотрим несколько других аспектов.

С точки зрения поддержки сообщества, фреймворк Spring в целом имеет огромное сообщество разработчиков , активно участвующих в его разработке и использовании. Поскольку весенняя безопасность является частью зонтика, она должна пользоваться теми же преимуществами. Широ, хотя и популярен, не имеет такой огромной поддержки.

Что касается документации, весна снова является победителем.

Тем не менее, есть небольшая кривая обучения, связанная с безопасностью Spring. Широ, с другой стороны, легко понять . Для настольных приложений настройка через shiro.ini тем проще.

Но опять же, как мы видели в наших примерах фрагментов, Spring Security отлично справляется с сохранением бизнес-логики и безопасности | отдельно и действительно предлагает безопасность как сквозную проблему.

7. Заключение

В этом уроке мы сравнили Apache Shiro с Spring Security .

Мы только что коснулись поверхности того, что могут предложить эти фреймворки, и нам еще многое предстоит изучить. Существует довольно много альтернатив, таких как JAAS и OACC . Тем не менее, с его преимуществами, Spring Security , похоже, выигрывает на данный момент.

Как всегда, исходный код доступен на GitHub .