1. Обзор
В этой статье мы рассмотрим Apache Shiro , универсальную платформу безопасности Java.
Платформа является высоко настраиваемой и модульной, поскольку она предлагает аутентификацию, авторизацию, криптографию и управление сеансами.
2. Зависимость
Apache Shiro имеет много модулей . Однако в этом уроке мы используем только артефакт shiro-core .
Давайте добавим его в ваш pom.xml :
org.apache.shiro shiro-core 1.4.0
Последнюю версию модулей Apache Shiro можно найти на Maven Central.
3. Настройка Менеджера безопасности
Менеджер безопасности является центральным элементом фреймворка Apache Shiro. Приложения, как правило, имеют один запущенный экземпляр.
В этом уроке мы рассмотрим фреймворк в среде рабочего стола. Чтобы настроить фреймворк, нам нужно создать файл shiro.ini в папке ресурсов со следующим содержимым:
[users] user = password, admin user2 = password2, editor user3 = password3, author [roles] admin = * editor = articles:* author = articles:compose,articles:save
Раздел [пользователи] файла конфигурации shiro.ini определяет учетные данные пользователя, которые распознаются менеджером безопасности . Формат: p rincipal, role1, role2, …, role .
Роли и связанные с ними разрешения объявляются в разделе [роли] . Роль admin получает разрешение и доступ к каждой части приложения. Это обозначается символом подстановочного знака ( * ) .
Роль редактор имеет все разрешения, связанные с статьями , в то время как роль автор может только составлять и сохранять статью.
Менеджер безопасности используется для настройки класса 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 Shiro Субъект – это любая сущность, взаимодействующая с системой. Это может быть человек, скрипт или клиент REST.
Вызов SecurityUtils.getSubject() возвращает экземпляр текущего Субъекта , то есть текущего пользователя .
Теперь, когда у нас есть объект текущий пользователь , мы можем выполнить аутентификацию по предоставленным учетным данным:
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); } }
Во-первых, мы проверяем, не был ли текущий пользователь уже аутентифицирован. Затем мы создаем токен аутентификации с именем пользователя (имя пользователя) и учетными данными (пароль).
Затем мы попытаемся войти в систему с помощью токена. Если предоставленные учетные данные верны, все должно идти нормально.
Существуют разные исключения для разных случаев. Также можно создать пользовательское исключение, которое лучше соответствует требованиям приложения. Это можно сделать, создав подкласс класса Account Exception .
5. Авторизация
Аутентификация пытается проверить личность пользователя, в то время как авторизация пытается контролировать доступ к определенным ресурсам в системе.
Напомним, что мы назначаем одну или несколько ролей каждому пользователю, которого мы создали в файле shiro.ini . Кроме того, в разделе роли мы определяем различные разрешения или уровни доступа для каждой роли.
Теперь давайте посмотрим, как мы можем использовать это в нашем приложении для обеспечения контроля доступа пользователей.
В файле shiro.ini мы предоставляем администратору полный доступ ко всем частям системы.
Редактор имеет полный доступ ко всем ресурсам/операциям , связанным с статьями , а автор ограничен только составлением и сохранением статей .
Давайте поприветствуем текущего пользователя на основе роли:
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 Shiro, Realm – это DAO, который указывает на хранилище учетных данных пользователя, необходимых для аутентификации и авторизации.
Чтобы создать область, нам нужно только реализовать интерфейс Realm . Это может быть утомительно; однако фреймворк поставляется с реализациями по умолчанию, из которых мы можем создавать подклассы. Одной из таких реализаций является JDBCRealm .
Мы создаем пользовательскую реализацию области, которая расширяет класс JDBCRealm и переопределяет следующие методы: doGetAuthenticationInfo () , doGetAuthorizationInfo() , getRoleNamesForUser() и getPermissions() .
Давайте создадим область, создав подкласс класса JDBCRealm :
public class MyCustomRealm extends JdbcRealm { //... }
Для простоты мы используем java.util.Map для имитации базы данных:
private Mapcredentials = 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()); }
Сначала мы приводим токен аутентификации , предоставленный UsernamePasswordToken . Из uToken мы извлекаем имя пользователя ( token.GetUserName() ) и используем его для получения учетных данных пользователя (пароля) из базы данных.
Если запись не найдена – мы создаем исключение UnknownAccountException , в противном случае мы используем учетные данные и имя пользователя для создания объекта SimpleAuthenticatioInfo , возвращаемого методом.
Если учетные данные пользователя хэшируются с помощью соли, нам нужно вернуть SimpleAuthenticationInfo с соответствующей солью:
return new SimpleAuthenticationInfo( uToken.getUsername(), credentials.get(uToken.getUsername()), ByteSource.Util.bytes("salt"), getName() );
Нам также необходимо переопределить doGetAuthorizationInfo() , а также getRoleNamesForUser() и getPermissions() .
Наконец, давайте подключим пользовательскую область в security Manager . Все, что нам нужно сделать, это заменить IniRealm выше на нашу пользовательскую область и передать ее в конструктор DefaultSecurityManager :
Realm realm = new MyCustomRealm(); SecurityManager securityManager = new DefaultSecurityManager(realm);
Все остальные части кода такие же, как и раньше. Это все, что нам нужно, чтобы правильно настроить security Manager с пользовательской областью.
Теперь вопрос в том, как фреймворк соответствует учетным данным?
По умолчанию JDBCRealm использует SimpleCredentialsMatcher , который просто проверяет равенство , сравнивая учетные данные в AuthenticationToken и AuthenticationInfo .
Если мы хэшируем наши пароли, нам нужно сообщить фреймворку, чтобы вместо этого использовать HashedCredentialsMatcher . Конфигурации INI для областей с хэшированными паролями можно найти здесь .
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. Широ для веб – приложения с пружиной
До сих пор мы описали базовую структуру Apache Shiro и реализовали ее в среде рабочего стола. Давайте продолжим интеграцию фреймворка в приложение Spring Boot.
Обратите внимание, что основное внимание здесь уделяется Shiro, а не приложению Spring – мы собираемся использовать его только для простого примера приложения.
9.1. Зависимости
Во-первых, нам нужно добавить родительскую зависимость Spring Boot в ваш pom.xml :
org.springframework.boot spring-boot-starter-parent 2.4.0
Затем мы должны добавить следующие зависимости к тому же 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 и Shirosecurityfilters. Мы будем использовать ту же пользовательскую область, определенную выше.
Итак, в основном классе, в котором запускается приложение 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; }
В определении Shiro FilterChainDefinition мы применили фильтр authc к безопасному/| пути и применили фильтр canon к другим путям , используя шаблон Ant.
По умолчанию для веб-приложений используются фильтры auth и anon . Другие фильтры по умолчанию можно найти здесь .
Если мы не определили Realm bean, Shiro Auto Configuration по умолчанию предоставит Начальную реализацию, которая ожидает найти файл shiro.ini в src/main/resources или src/main/resources/META-INF.
Если мы не определяем Shiro FilterChainDefinition bean, фреймворк защищает все пути и устанавливает URL-адрес входа в систему как login.jsp .
Мы можем изменить этот URL-адрес входа по умолчанию и другие значения по умолчанию, добавив следующие записи в наш application.properties :
shiro.loginUrl = /login shiro.successUrl = /secure shiro.unauthorizedUrl = /login
Теперь , когда фильтр authc был применен к /secure , все запросы к этому маршруту потребуют проверки подлинности формы.
9.3. Аутентификация и авторизация
Давайте создадим контроллер Shiro Spring со следующими сопоставлениями путей: /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() текущий пользователь был получен путем вызова 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";
И мы закончили. Вот как мы можем интегрировать Apache Shiro в приложение Spring Boot.
Кроме того, обратите внимание, что фреймворк предлагает дополнительные аннотации , которые можно использовать вместе с определениями filterchaindefinitions для защиты нашего приложения.
10. Интеграция JEE
Интеграция Apache Shiro в приложение JEE-это всего лишь вопрос настройки web.xml файл. Как обычно, конфигурация ожидает, что shiro.ini будет находиться в пути к классу. Подробный пример конфигурации доступен здесь . Кроме того, теги JSP можно найти здесь .
11. Заключение
В этом уроке мы рассмотрели механизмы аутентификации и авторизации Apache Shiro. Мы также сосредоточились на том, как определить пользовательскую область и подключить ее к Менеджеру безопасности .
Как всегда, полный исходный код доступен на GitHub .