1. Обзор
В этой статье мы рассмотрим Apache Shiro , универсальную среду безопасности Java.
Платформа обладает широкими возможностями настройки и модульности, поскольку предлагает аутентификацию, авторизацию, криптографию и управление сеансами.
2. Зависимость
Apache Shiro имеет множество модулей . Однако в этом уроке мы используем только артефакт широ-ядра .
Давайте добавим его в наш pom.xml
:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
Последнюю версию модулей Apache Shiro можно найти на Maven Central.
3. Настройка диспетчера безопасности
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
. Формат следующий: главный (имя пользователя) = пароль, роль1
, роль2, …, роль
.
Роли и связанные с ними разрешения объявляются в разделе [roles]
. Роль администратора
предоставляет разрешение и доступ ко всем частям приложения. На это указывает подстановочный знак (*)
.
Роль редактора
имеет все разрешения, связанные со статьями
, в то время как роль автора
может только создавать
и сохранять
статьи.
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 Shiro субъект
— это любой объект, взаимодействующий с системой. Это может быть человек, сценарий или клиент 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);
}
}
Во-первых, мы проверяем, не прошел ли уже текущий пользователь аутентификацию. Затем мы создаем токен аутентификации с основным пользователем (имя пользователя)
и учетными данными (пароль).
Затем мы пытаемся войти в систему с токеном. Если предоставленные учетные данные верны, все должно пройти нормально.
Для разных случаев есть разные исключения. Также можно создать пользовательское исключение, которое лучше соответствует требованиям приложения. Это можно сделать, создав подкласс класса AccountException
.
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
. Здесь в игру вступает концепция Realm.
В терминологии Apache Shiro Realm — это DAO, указывающий на хранилище учетных данных пользователя, необходимых для аутентификации и авторизации.
Чтобы создать область, нам нужно только реализовать интерфейс Realm .
Это может быть утомительно; однако фреймворк поставляется с реализациями по умолчанию, из которых мы можем создать подклассы. Одной из таких реализаций является JdbcRealm
.
Мы создаем собственную реализацию области, которая расширяет класс JdbcRealm
и переопределяет следующие методы: doGetAuthenticationInfo()
, doGetAuthorizationInfo()
, getRoleNamesForUser()
и getPermissions()
.
Давайте создадим область, создав подкласс класса JdbcRealm
:
public class MyCustomRealm extends JdbcRealm {
//...
}
Для простоты мы используем java.util.Map
для имитации базы данных:
private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> 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 для реалмов с хешированными паролями можно найти здесь .
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
:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
</parent>
Затем мы должны добавить следующие зависимости в тот же файл pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>${apache-shiro-core-version}</version>
</dependency>
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
и применили фильтр анонимности
к другим путям, используя шаблон Ant.
Оба фильтра authc
и anon
по умолчанию поставляются с веб-приложениями. Другие фильтры по умолчанию можно найти здесь .
Если мы не определили 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 ()
текущий пользователь
был получен вызовом 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.
Также обратите внимание, что платформа предлагает дополнительные аннотации , которые можно использовать вместе с определениями цепочки фильтров для защиты нашего приложения.
10. Интеграция JEE
Для интеграции Apache Shiro в JEE-приложение достаточно настроить файл web.xml
. Как обычно, конфигурация предполагает, что shiro.ini
находится в пути к классам. Подробный пример конфигурации доступен здесь . Также теги JSP можно найти здесь .
11. Заключение
В этом руководстве мы рассмотрели механизмы аутентификации и авторизации Apache Shiro. Мы также сосредоточились на том, как определить пользовательскую область и подключить ее к SecurityManager
.
Как всегда, полный исходный код доступен на GitHub .