Введение в Apache Shiro

Введение в Apache Shiro

1. обзор

В этой статье мы рассмотримApache Shiro, универсальную платформу безопасности Java.

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

2. зависимость

У Apache Shiro многоmodules. Однако в этом руководстве мы используем только артефактshiro-core.

Добавим его к нашемуpom.xml:


    org.apache.shiro
    shiro-core
    1.4.0

Последнюю версию модулей Apache Shiro можно найтиon Maven Central.

3. Настройка Security Manager

SecurityManager - это центральная часть структуры Apache Shiro. Приложения обычно имеют один запущенный экземпляр.

В этом уроке мы исследуем фреймворк в среде рабочего стола. Чтобы настроить фреймворк, нам нужно создать файлshiro.ini в папке ресурсов со следующим содержимым:

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

Раздел[users] конфигурационного файлаshiro.ini определяет учетные данные пользователя, которые распознаютсяSecurityManager. Формат:principal (username) = password, role1, role2, …, role.

Роли и связанные с ними разрешения объявлены в разделе[roles]. Рольadmin предоставляет разрешение и доступ ко всем частям приложения. На это указывает подстановочный знак(*).

Рольeditor имеет все разрешения, связанные сarticles, в то время как рольauthor может толькоcompose иsave для статьи.

SecurityManager используется для настройки классаSecurityUtils. ИзSecurityUtils мы можем получить текущего пользователя, взаимодействующего с системой, и выполнить операции аутентификации и авторизации.

Давайте воспользуемсяIniRealm для загрузки наших определений пользователей и ролей из файлаshiro.ini, а затем воспользуемся им для настройки объектаDefaultSecurityManager:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

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

4. Аутентификация

В терминологии Apache ShiroSubject - это любая сущность, взаимодействующая с системой. Это может быть человек, сценарий или клиент REST.

ВызовSecurityUtils.getSubject() возвращает экземпляр текущегоSubject, то естьcurrentUser.

Теперь, когда у нас есть объектcurrentUser, мы можем выполнить аутентификацию по предоставленным учетным данным:

if (!currentUser.isAuthenticated()) {
  UsernamePasswordToken token
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);
  try {
      currentUser.login(token);
  } catch (UnknownAccountException uae) {
      log.error("Username Not Found!", uae);
  } catch (IncorrectCredentialsException ice) {
      log.error("Invalid Credentials!", ice);
  } catch (LockedAccountException lae) {
      log.error("Your Account is Locked!", lae);
  } catch (AuthenticationException ae) {
      log.error("Unexpected Error!", ae);
  }
}

Сначала мы проверяем, не был ли текущий пользователь уже аутентифицирован. Затем мы создаем токен аутентификации с принципалом пользователя(username) и учетными данными(password)..

Далее мы пытаемся войти с помощью токена. Если предоставленные учетные данные верны, все должно идти хорошо.

Существуют разные исключения для разных случаев. Также можно создать собственное исключение, которое лучше соответствует требованиям приложения. Это можно сделать, создав подкласс классаAccountException.

5. авторизация

Аутентификация пытается проверить личность пользователя, в то время как авторизация пытается контролировать доступ к определенным ресурсам в системе.

Напомним, что мы назначаем одну или несколько ролей каждому пользователю, которого создали в файлеshiro.ini. Кроме того, в разделе ролей мы определяем различные разрешения или уровни доступа для каждой роли.

Теперь давайте посмотрим, как мы можем использовать это в нашем приложении для обеспечения контроля доступа пользователей.

В файлеshiro.ini мы предоставляем администратору полный доступ ко всем частям системы.

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

Поприветствуем текущего пользователя в зависимости от роли:

if (currentUser.hasRole("admin")) {
    log.info("Welcome Admin");
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");
} else {
    log.info("Welcome, Guest");
}

Теперь давайте посмотрим, что текущему пользователю разрешено делать в системе:

if(currentUser.isPermitted("articles:compose")) {
    log.info("You can compose an article");
} else {
    log.info("You are not permitted to compose an article!");
}

if(currentUser.isPermitted("articles:save")) {
    log.info("You can save articles");
} else {
    log.info("You can not save articles");
}

if(currentUser.isPermitted("articles:publish")) {
    log.info("You can publish articles");
} else {
    log.info("You can not publish articles");
}

6. Конфигурация области

В реальных приложениях нам понадобится способ получить учетные данные пользователя из базы данных, а не из файлаshiro.ini. Вот где в игру вступает понятие Царство.

В терминологии Apache ShiroRealm - это DAO, указывающий на хранилище учетных данных пользователя, необходимых для аутентификации и авторизации.

Чтобы создать область, нам нужно только реализовать интерфейсRealm. Это может быть утомительно; тем не менее, фреймворк поставляется с реализациями по умолчанию, из которых мы можем создать подкласс. Одна из этих реализаций -JdbcRealm.

Мы создаем собственную реализацию области, которая расширяет классJdbcRealm и переопределяет следующие методы:doGetAuthenticationInfo(),doGetAuthorizationInfo(),getRoleNamesForUser() иgetPermissions().

Давайте создадим область, создав подкласс классаJdbcRealm:

public class MyCustomRealm extends JdbcRealm {
    //...
}

Для простоты мы используемjava.util.Map для моделирования базы данных:

private Map credentials = new HashMap<>();
private Map> roles = new HashMap<>();
private Map> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");

    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));

    perm.put("admin", new HashSet<>(Arrays.asList("*")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
    perm.put("author",
      new HashSet<>(Arrays.asList("articles:compose",
      "articles:save")));
}

Давайте продолжим и заменимdoGetAuthenticationInfo():

protected AuthenticationInfo
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {

    UsernamePasswordToken uToken = (UsernamePasswordToken) token;

    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }

    return new SimpleAuthenticationInfo(
      uToken.getUsername(),
      credentials.get(uToken.getUsername()),
      getName());
}

Сначала мы приводим предоставленныйAuthenticationToken кUsernamePasswordToken. ИзuToken мы извлекаем имя пользователя (uToken.getUsername()) и используем его для получения учетных данных пользователя (пароля) из базы данных.

Если запись не найдена - мы бросаемUnknownAccountException, иначе мы используем учетные данные и имя пользователя для создания объектаSimpleAuthenticatioInfo, который возвращается из метода.

Если учетные данные пользователя хешируются с солью, нам нужно вернутьSimpleAuthenticationInfo с соответствующей солью:

return new SimpleAuthenticationInfo(
  uToken.getUsername(),
  credentials.get(uToken.getUsername()),
  ByteSource.Util.bytes("salt"),
  getName()
);

Нам также необходимо переопределитьdoGetAuthorizationInfo(), а такжеgetRoleNamesForUser() иgetPermissions().

Наконец, давайте подключим настраиваемую область кsecurityManager. Все, что нам нужно сделать, это заменитьIniRealm выше на нашу настраиваемую область и передать его конструкторуDefaultSecurityManager:

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

Каждая другая часть кода такая же, как и раньше. Это все, что нам нужно для правильной настройкиsecurityManager с настраиваемой областью.

Теперь вопрос - как фреймворк соответствует учетным данным?

По умолчаниюJdbcRealm используетSimpleCredentialsMatcher, который просто проверяет равенство, сравнивая учетные данные вAuthenticationToken иAuthenticationInfo.

Если мы хэшируем наши пароли, нам нужно сообщить фреймворку, чтобы вместо этого использовалсяHashedCredentialsMatcher. Конфигурации INI для областей с хешированными паролями можно найти вhere.

7. Выйти

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

currentUser.logout();

8. Управление сессиями

Фреймворк, естественно, поставляется с системой управления сессиями Если используется в веб-среде, по умолчанию используется реализацияHttpSession.

Для автономного приложения используется корпоративная система управления сеансами. Преимущество заключается в том, что даже в среде рабочего стола вы можете использовать объект сеанса, как в обычной веб-среде.

Давайте посмотрим на быстрый пример и поработаем с сеансом текущего пользователя:

Session session = currentUser.getSession();
session.setAttribute("key", "value");
String value = (String) session.getAttribute("key");
if (value.equals("value")) {
    log.info("Retrieved the correct value! [" + value + "]");
}

9. Широ для веб-приложения с Spring

До сих пор мы обрисовали базовую структуру Apache Shiro и реализовали ее в среде рабочего стола. Давайте продолжим интеграцию фреймворка в приложение Spring Boot.

Обратите внимание, что основное внимание здесь уделяется Широ, а не приложению Spring - мы собираемся использовать его только для работы с простым примером приложения.

9.1. зависимости

Во-первых, нам нужно добавить родительскую зависимость Spring Boot к нашемуpom.xml:


    org.springframework.boot
    spring-boot-starter-parent
    1.5.2.RELEASE

Затем мы должны добавить следующие зависимости в тот же файлpom.xml:


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


    org.springframework.boot
    spring-boot-starter-freemarker


    org.apache.shiro
    shiro-spring-boot-web-starter
    ${apache-shiro-core-version}

9.2. конфигурация

Добавление зависимостиshiro-spring-boot-web-starter к нашемуpom.xml по умолчанию настроит некоторые функции приложения Apache Shiro, такие какSecurityManager.

Однако нам все еще нужно настроить фильтры безопасностиRealm и Shiro. Мы будем использовать ту же пользовательскую область, определенную выше.

Итак, в главном классе, где запускается приложение Spring Boot, добавим следующие определенияBean:

@Bean
public Realm realm() {
    return new MyCustomRealm();
}

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

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

ВShiroFilterChainDefinition мы применили фильтрauthc к пути/secure и применили фильтрanon к другим путям, используя шаблон Ant.

Фильтрыauthc иanon используются по умолчанию для веб-приложений. Другие фильтры по умолчанию можно найтиhere.

Если мы не определили bean-компонентRealm,ShiroAutoConfiguration по умолчанию предоставит реализациюIniRealm, которая ожидает найти файлshiro.ini вsrc/main/resources илиsrc/main/resources/META-INF.с

Если мы не определяем bean-компонентShiroFilterChainDefinition, фреймворк защищает все пути и устанавливает URL-адрес входа какlogin.jsp.

Мы можем изменить этот URL-адрес входа по умолчанию и другие значения по умолчанию, добавив следующие записи в нашapplication.properties:

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

Теперь, когда фильтрauthc применен к/secure, все запросы к этому маршруту потребуют аутентификации с помощью формы.

9.3. Аутентификация и Авторизация

Давайте создадимShiroSpringController со следующими сопоставлениями путей:/index,/login, /logout и/secure.

В методеlogin() мы реализуем фактическую аутентификацию пользователя, как описано выше. Если аутентификация прошла успешно, пользователь перенаправляется на защищенную страницу:

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

И теперь в реализацииsecure()currentUser был получен путем вызоваSecurityUtils.getSubject().. Роль и разрешения пользователя передаются на защищенную страницу, также как и принципал пользователя:

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

And we’re done. Вот как мы можем интегрировать Apache Shiro в приложение Spring Boot.

Также обратите внимание, что платформа предлагает дополнительныеannotations, которые можно использовать вместе с определениями цепочки фильтров для защиты нашего приложения.

10. Интеграция JEE

Интеграция Apache Shiro в приложение JEE - это всего лишь вопрос настройки файлаweb.xml. Как обычно, конфигурация ожидает, чтоshiro.ini находится в пути к классам. Доступен подробный пример конфигурацииhere. Также можно найти теги JSPhere.

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

В этом руководстве мы рассмотрели механизмы аутентификации и авторизации Apache Shiro. Мы также сосредоточились на том, как определить настраиваемую область и подключить ее кSecurityManager.

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