Статьи

Apache Shiro, часть 2 — области, базы данных и сертификаты PGP

Это вторая часть серии, посвященная Apache Shiro. Мы начали предыдущую часть с простого незащищенного веб-приложения. Когда мы закончили, приложение прошло базовую аутентификацию и авторизацию. Пользователи могут войти и выйти. Все веб-страницы и кнопки имеют права доступа, назначенные и применяемые. Данные авторизации и аутентификации были сохранены в файле статической конфигурации.

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

Другими словами, мы покажем, как создавать пользовательские области и как обрабатывать сценарии с несколькими областями. Мы создадим три разные версии SimpleShiroSecuredApplication:

Каждая версия имеет тестовый класс RunWaitTest. Класс запускает веб-сервер с приложением, развернутым по адресу http: // localhost: 9180 / simpleshirosecuredapplication / url.

Примечание. Мы обновили предыдущую часть с момента первого выпуска. Наиболее заметным изменением является новый раздел, в котором показано, как добавить сообщение об ошибке на страницу входа. Спасибо всем за отзывы.

Realms

Сначала мы объясним, что такое сферы и как их создавать. Если вас не интересует теория, перейдите к следующей главе .
Области отвечают за аутентификацию и авторизацию. Каждый раз, когда пользователь хочет войти в приложение, информация об аутентификации собирается и передается в область. Область проверяет предоставленные данные и решает, следует ли разрешить пользователю входить в систему, иметь доступ к ресурсу или свою собственную роль.
Информация аутентификации состоит из двух частей:

  • принципал — представляет уникальный идентификатор учетной записи, например, имя пользователя, идентификатор учетной записи, сертификат PGP,…
  • учетные данные — подтверждают личность пользователя, например, пароль, сертификат PGP, отпечаток пальца,….

Shiro предоставляет области, способные читать данные авторизации из активного каталога , ldap , ini-файла , файла свойств и базы данных . Области настраиваются в основном разделе файла Shiro.ini:

1
realmName=org.apache.shiro.realm.jdbc.JdbcRealm

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

Все сферы реализуют интерфейс Realm . Важны два метода интерфейса: support и getAuthenticationInfo. Оба получают принципал и учетные данные внутри объекта токена аутентификации .
Метод Поддержки решает, может ли область аутентифицировать пользователя на основе предоставленного токена аутентификации. Например, если моя область проверяет имя пользователя и пароль, она отклоняет маркер аутентификации только с сертификатом X509.
Метод getAuthenticationInfo выполняет аутентификацию самостоятельно. Если принципал и учетные данные из токена аутентификации представляют достоверную информацию для входа в систему, метод возвращает объект информации аутентификации . в противном случае область возвращает ноль.

авторизация

Если область хочет также выполнить авторизацию, она должна реализовать интерфейс Authorizer . Каждый метод Authorizer принимает принципала в качестве параметра и проверяет либо роль (и), либо разрешение (и). Важно понимать, что область получает все запросы на авторизацию, даже если они поступили от пользователя, аутентифицированного другой областью. Конечно, область может решить игнорировать любой запрос авторизации.
Разрешения предоставляются либо в виде строк, либо в виде объектов разрешений. Если у вас нет веских причин поступать иначе, используйте WildcardPermissionResolver для преобразования строк в объекты разрешений.

Другие опции

Среда Shiro исследует области во время выполнения для дополнительных интерфейсов. Если область реализует их, она может использовать:

Эти функции доступны для любой области, которая реализует дополнительный интерфейс. Никаких других настроек не требуется.
Пользовательские Королевства

Самый простой способ создать новую область — это расширить класс AuthenticatingRealm или AuthorizingRealm . Они имеют разумную реализацию всех полезных интерфейсов, упомянутых в предыдущем разделе. Если они не подходят для ваших нужд, вы можете расширить CachingRealm или создать новую область с нуля.

Переместить в базу данных

Текущая версия SimpleShiroSecuredApplication использует область по умолчанию для аутентификации и авторизации. Область по умолчанию — IniRealm считывает информацию об учетной записи пользователя из файла конфигурации. Такое хранение приемлемо только для самых простых приложений. Для чего-то более сложного необходимо хранить учетные данные в более надежном постоянном хранилище.
Новые требования: учетные данные и права доступа хранятся в базе данных. Сохраненные пароли хешируются и засоляются.
В этой главе мы подключим приложение к базе данных и создадим таблицы для хранения всех данных учетной записи пользователя. Затем мы заменим IniRealm областью, способной считывать из базы данных и солить пароли.

Инфраструктура базы данных

В разделе описывается пример инфраструктуры приложения. Он не содержит информации о Широ, поэтому вы можете свободно его пропустить .
Пример приложения использует базу данных Apache Derby во встроенном режиме.
Мы используем Liquibase для развертывания и обновления баз данных. Это библиотека с открытым исходным кодом для отслеживания, управления и применения изменений в базе данных. Изменения базы данных (новые таблицы, новые столбцы, внешние ключи) хранятся в файле журнала изменений базы данных. После запуска Liquibase исследует базу данных и применяет все новые изменения. В результате база данных всегда последовательна и актуальна без каких-либо реальных усилий с нашей стороны.
Добавьте зависимость от Derby и Liquibase в SimpleShiroSecuredApplication pom.xml :

01
02
03
04
05
06
07
08
09
10
<dependency>
    <groupid>org.apache.derby</groupid>
    <artifactid>derby</artifactid>
    <version>10.7.1.1</version>
</dependency>
<dependency>
    <groupid>org.liquibase</groupid>
    <artifactid>liquibase-core</artifactid>
    <version>2.0.1</version>
</dependency>

Добавьте jndi к пристани:

01
02
03
04
05
06
07
08
09
10
11
12
<dependency>
   <groupid>org.mortbay.jetty</groupid>
   <artifactid>jetty-naming</artifactid>
   <version>${jetty.version}</version>
   <scope>test</scope>
</dependency
<dependency>
   <groupid>org.mortbay.jetty</groupid>
   <artifactid>jetty-plus</artifactid>
   <version>${jetty.version}</version>
   <scope>test</scope>
</dependency

Создайте файл db.changelog.xml с описанием структуры базы данных. Он создает таблицы, в которых хранятся пользователи, роли и разрешения. Он также заполняет эти таблицы начальными данными. Мы использовали random_salt_value_username в качестве соли и следующий метод для создания хэшированных соленых паролей:

1
2
3
4
5
6
7
public static String simpleSaltedHash(String username, String password) {
   Sha256Hash sha256Hash = new Sha256Hash(password, (new SimpleByteSource('random_salt_value_' + username)).getBytes());
  String result = sha256Hash.toHex();
 
   System.out.println(username + ' simple salted hash: ' + result);
   return result;
}

Создайте источник данных, указывающий на derby, в файле WEB-INF / jetty-web.xml :

01
02
03
04
05
06
07
08
09
10
11
<configure class='org.mortbay.jetty.webapp.WebAppContext' id='SimpleShiroSecuredApplication'>
 <new class='org.mortbay.jetty.plus.naming.Resource' id='SimpleShiroSecuredApplication'>
  <arg>jdbc/SimpleShiroSecuredApplicationDB</arg>
  <arg>
   <new class='org.apache.derby.jdbc.EmbeddedDataSource'>
    <set name='DatabaseName'>../SimpleShiroSecuredApplicationDatabase</set>
    <set name='createDatabase'>create</set>
   </new>
  </arg>
 </new>
</configure>

Настройте источник данных и liquibase в файле web.xml :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<resource-ref>
  <description>Derby Connection</description>
  <res-ref-name>jdbc/SimpleShiroSecuredApplicationDB</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>
  
<context-param>
  <param-name>liquibase.changelog</param-name>
  <param-value>src/main/resources/db.changelog.xml</param-value>
</context-param>
 
<context-param>
  <param-name>liquibase.datasource</param-name>
  <param-value>jdbc/SimpleShiroSecuredApplicationDB</param-value>
</context-param>
 
<listener>
  <listener-class>
    liquibase.integration.servlet.LiquibaseServletListener
  </listener-class>
</listener>

Наконец, jetty, настроенный для чтения jetty-web.xml с включенным jndi, находится в классе AbstractContainerTest .

Создать новое царство

Широ предоставил JDBCRealm способный выполнять аутентификацию и авторизацию. Он использует настраиваемые запросы SQL для чтения имен пользователей, паролей, разрешений и ролей из базы данных. К сожалению, у царства есть два недостатка:

Мы расширяем его и создаем новый класс JNDIAndSaltAwareJdbcRealm . Поскольку все свойства настраиваются в ini-файле, новое свойство jndiDataSourceName также будет настраиваться автоматически. Область ищет источник данных в JNDI всякий раз, когда устанавливается новое свойство:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
protected String jndiDataSourceName;
 
public String getJndiDataSourceName() {
 return jndiDataSourceName;
}
 
public void setJndiDataSourceName(String jndiDataSourceName) {
 this.jndiDataSourceName = jndiDataSourceName;
 this.dataSource = getDataSourceFromJNDI(jndiDataSourceName);
}
 
private DataSource getDataSourceFromJNDI(String jndiDataSourceName) {
 try {
  InitialContext ic = new InitialContext();
  return (DataSource) ic.lookup(jndiDataSourceName);
 } catch (NamingException e) {
  log.error('JNDI error while retrieving ' + jndiDataSourceName, e);
  throw new AuthorizationException(e);
 }
}

Метод doGetAuthenticationInfo считывает информацию аутентификации учетной записи из базы данных и преобразует ее в объект информации аутентификации . Возвращает ноль, если информация об учетной записи не найдена. Родительский класс AuthenticatingRealm сравнивает объект информации аутентификации с исходными данными, предоставленными пользователем.
Мы переопределяем doGetAuthenticationInfo, чтобы прочитать хэш пароля и соль из базы данных и сохранить их в объекте информации аутентификации

01
02
03
04
05
06
07
08
09
10
11
doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
 ...
 // read password hash and salt from db
 PasswdSalt passwdSalt = getPasswordForUser(username);
 ...
 // return salted credentials
 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, passwdSalt.password, getName());
 info.setCredentialsSalt(new SimpleByteSource(passwdSalt.salt));
 
 return info;
}

Пример здесь содержит только самый важный фрагмент кода. Полный класс доступен на Github.

Настроить новое царство

Настройте область и имя jndi в файле Shiro.ini :

1
2
3
4
5
6
7
[main]
# realm to be used
saltedJdbcRealm=org.meri.simpleshirosecuredapplication.realm.JNDIAndSaltAwareJdbcRealm
# any object property is automatically configurable in Shiro.ini file
saltedJdbcRealm.jndiDataSourceName=jdbc/SimpleShiroSecuredApplicationDB
# the realm should handle also authorization
saltedJdbcRealm.permissionsLookupEnabled=true

Настройте запросы SQL:

1
2
3
4
5
6
7
# If not filled, subclasses of JdbcRealm assume 'select password from users where username = ?'
# first result column is password, second result column is salt
saltedJdbcRealm.authenticationQuery = select password, salt from sec_users where name = ?
# If not filled, subclasses of JdbcRealm assume 'select role_name from user_roles where username = ?'
saltedJdbcRealm.userRolesQuery = select role_name from sec_users_roles where user_name = ?
# If not filled, subclasses of JdbcRealm assume 'select permission from roles_permissions where role_name = ?'
saltedJdbcRealm.permissionsQuery = select permission from sec_roles_permissions where role_name = ?

JdbcRealm использует сопоставление учетных данных точно так же, как IniRealm:

1
2
3
4
# password hashing specification
sha256Matcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
sha256Matcher.hashAlgorithmName=SHA-256
saltedJdbcRealm.credentialsMatcher = $sha256Matcher

ПРИМЕЧАНИЕ: мы удалили разделы [пользователи] и [роли] из файла конфигурации. В противном случае Широ использовал бы и IniRealm, и JdbcRealm. Это создаст многоцелевой сценарий, который выходит за рамки данной главы.

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

Полный исходный код доступен в ветке «authentication_stored_in_database» на Github.

Альтернативный вход — Сертификаты

Некоторые системы допускают несколько способов аутентификации для входа в систему пользователя. Например, пользователь может предоставить имя пользователя / пароль, войти в систему с учетной записью Google, учетной записью Facebook или любым другим. Мы добавим что-то похожее на наше простое приложение. Мы дадим нашим пользователям возможность аутентифицировать себя с помощью сертификатов PGP.
Новые требования: приложение поддерживает сертификаты PGP в качестве альтернативного механизма аутентификации. Экран входа в систему отображается только в том случае, если у пользователя нет действующего сертификата, связанного с учетной записью приложения. Если у пользователя есть действующий известный сертификат PGP, он регистрируется автоматически.
Когда пользователь пытается войти в приложение, он должен предоставить данные аутентификации. Эти данные захватываются фильтром сервлетов. Фильтр преобразует данные в токен аутентификации и передает токен в области. Если какая-либо область желает аутентифицировать пользователя, она преобразует токен аутентификации в объект информации аутентификации. Если область не желает этого делать, она возвращает ноль.
Готовые фильтры Shiro Framework игнорируют сертификаты PGP в запросе. Доступные токены аутентификации не могут их содержать, а области вообще не знают о сертификатах PGP. Поэтому мы должны создать:

  • токен аутентификации для перемещения сертификата,
  • фильтр сервлетов, умеющий читать сертификаты,
  • область для проверки сертификатов и сопоставления их с учетными записями пользователей.

Наше приложение будет иметь два разных мира. Один использует имена для идентификации учетных записей и пароли для аутентификации пользователей, другой использует сертификаты PGP для обеих целей.
Прежде чем приступить к написанию кода, мы должны разобраться с сертификатами PGP и инфраструктурой, которая окружает наше приложение. Если вас не интересует настройка сертификатов PGP,

инфраструктура

Когда пользователь посещает веб-приложение, его веб-браузер может отправлять копию сертификата PGP на веб-сервер. Сертификат подписан каким-либо центром сертификации или самим сертификатом (самозаверяющий сертификат). Веб-сервер хранит список сертификатов, которым он доверяет, в хранилище, которое называется хранилище доверенных сертификатов. Если доверенное хранилище содержит либо сертификат пользователя, либо сертификат органа, подписавшего его, веб-сервер доверяет сертификату пользователя. Доверенные сертификаты передаются в приложение.
Мы будем:

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

Создание и управление сертификатами в Portecle . Образцы сертификатов для SimpleShiroSecuredApplication находятся в каталоге src \ test \ resources \ clients . Все магазины и сертификаты имеют общий пароль «секретный».

Создать сертификат

Создайте самоподписанный сертификат для каждого пользователя в portecle:

  • Создать новое хранилище ключей jks: Файл -> Новое хранилище ключей, выберите jks.
  • Создать новый сертификат: Инструменты -> Создать пару ключей. Оставьте поля пароля пустыми, сертификат унаследует пароль от хранилища ключей.
  • Экспорт публичного сертификата: выберите новый сертификат -> щелкните правой кнопкой мыши -> Экспорт, выберите Сертификат руководителя. Это создает файл .cer.
  • Экспорт закрытого ключа и сертификата: выберите новый сертификат -> щелкните правой кнопкой мыши -> Экспорт, выберите Закрытый ключ и Сертификаты. Это создает файл .p12.

Файл .cer содержит только публичный сертификат, поэтому вы можете отдать его любому. С другой стороны, файл .p12 содержит закрытый ключ пользователя и должен храниться в секрете. Раздайте его только пользователю (например, импортируйте в браузер для тестирования).

Создать Truststore

Создайте новое хранилище доверенных сертификатов и импортируйте в него файлы открытого сертификата .cer:

  • Файл -> Новый Keystore, выберите jks.
  • Инструменты -> Импорт доверенного сертификата.

Настроить веб-сервер

Веб-сервер должен запрашивать сертификаты и проверять их на наличие доверенного хранилища. Невозможно запросить сертификат у Java. Каждый веб-сервер настроен по-разному. Конфигурация Jetty доступна в разделе Посмотрите на класс AbstractContainerTest на Github.

Ассоциированные сертификаты с аккаунтами

Каждый сертификат уникально идентифицируется серийным номером и именем центра сертификации, который его подписал. Мы храним их вместе с именами пользователей и паролями в таблице базы данных. Изменения базы данных находятся в файле db.changelog.xml, см. Набор изменений 3 для новых столбцов и набор изменений 4 для инициализации данных.

Токен аутентификации

Токен аутентификации представляет пользовательские данные и учетные данные во время попытки аутентификации. Он должен реализовывать интерфейс токена аутентификации и хранить любые данные, которые мы хотим передать между фильтром сервлета и областью.
Поскольку мы хотим использовать для аутентификации как имена пользователей / пароли, так и сертификаты, мы расширяем класс UsernamePasswordToken и добавляем к нему свойство сертификата. Новый токен аутентификации X509CertificateUsernamePasswordToken реализует новый интерфейс X509CertificateAuthenticationToken, и оба они доступны на Github:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class X509CertificateUsernamePasswordToken extends UsernamePasswordToken implements X509CertificateAuthenticationToken {
 
    private X509Certificate certificate;
 
    @Override
    public X509Certificate getCertificate() {
      return certificate;
    }
 
    public void setCertificate(X509Certificate certificate) {
      this.certificate = certificate;
    }
 
}

Фильтр сервлетов

Фильтр Широ преобразует данные пользователя в токены аутентификации. До сих пор мы использовали FormAuthenticationFilter . Если входящий запрос поступает от зарегистрированного пользователя, фильтр пропускает пользователя. Если пользователь пытается аутентифицировать себя, фильтр создает токен аутентификации и передает его в платформу. В противном случае он перенаправляет пользователя на экран входа в систему.
Наш фильтр CertificateOrFormAuthenticationFilter расширяет FormAuthenticationFilter .

Во-первых, мы должны убедить его, что не только запросы с именем пользователя и паролями, но также любой запрос с сертификатом PGP может рассматриваться как попытка входа в систему. Во-вторых, мы должны изменить фильтр для отправки сертификата PGP вместе с именем пользователя и паролем в токене аутентификации.
Метод isLoginSubmission определяет, представляет ли запрос попытку аутентификации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
    protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
        return super.isLoginSubmission(request, response) || isCertificateLogInAttempt(request, response);
    }
     
    private boolean isCertificateLogInAttempt(ServletRequest request, ServletResponse response) {
        return hasCertificate(request) && !getSubject(request, response).isAuthenticated();
    }
     
    private boolean hasCertificate(ServletRequest request) {
        return null != getCertificate(request);
    }
     
    private X509Certificate getCertificate(ServletRequest request) {
        X509Certificate[] attribute = (X509Certificate[]) request.getAttribute('javax.servlet.request.X509Certificate');
        return attribute==null? null : attribute[0];
    }

Метод createToken создает токен аутентификации:

01
02
03
04
05
06
07
08
09
10
11
@Override
    protected AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response) {
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        X509Certificate certificate = getCertificate(request);
        return createToken(username, password, rememberMe, host, certificate);
    }
     
    protected AuthenticationToken createToken(String username, String password, boolean rememberMe, String host, X509Certificate certificate) {
        return new X509CertificateUsernamePasswordToken(username, password, rememberMe, host, certificate);
    }

Замените FormAuthenticationFilter на фильтр CertificateOrFormAuthenticationFilter в файле конфигурации :

01
02
03
04
05
06
07
08
09
10
11
12
13
[main]
# filter configuration
certificateFilter = org.meri.simpleshirosecuredapplication.servlet.CertificateOrFormAuthenticationFilter
# specify login page
certificateFilter.loginUrl = /simpleshirosecuredapplication/account/login.jsp
# name of request parameter with username; if not present filter assumes 'username'
certificateFilter.usernameParam = user
# name of request parameter with password; if not present filter assumes 'password'
certificateFilter.passwordParam = pass
# does the user wish to be remembered?; if not present filter assumes 'rememberMe'
certificateFilter.rememberMeParam = remember
# redirect after successful login
certificateFilter.successUrl  = /simpleshirosecuredapplication/account/personalaccountpage.jsp

Перенаправить все URL в новый фильтр:

01
02
03
04
05
06
07
08
09
10
11
12
[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443], certificateFilter
 
# only users with some roles are allowed to use role-specific pages
/simpleshirosecuredapplication/repairmen/**=certificateFilter, roles[repairman]
/simpleshirosecuredapplication/sales/**=certificateFilter, roles[sales]
/simpleshirosecuredapplication/scientists/**=certificateFilter, roles[scientist]
/simpleshirosecuredapplication/adminarea/**=certificateFilter, roles[Administrator]
 
# enable certificateFilter filter for all application pages
/simpleshirosecuredapplication/**=certificateFilter

Custom Realm

Наша новая сфера будет отвечать только за аутентификацию. Авторизация (права доступа) будет обрабатываться JNDIAndSaltAwareJdbcRealm . Такая конфигурация работает до тех пор, пока сертификат PGP аутентифицирует пользователя в той же учетной записи, что и имя пользователя / пароль. В противном случае первичный принципал, возвращаемый новой областью, должен совпадать с первичным принципалом, возвращаемым JNDIAndSaltAwareJdbcRealm.
Наша область не нуждается ни в кэшировании, ни в каких-либо других услугах, предоставляемых дополнительными интерфейсами . Поэтому мы должны реализовать только два интерфейса: Realm и Nameable.
X509CertificateRealm поддерживает только токены аутентификации с сертификатом PGP:

1
2
3
4
5
6
7
@Override
 public boolean supports(AuthenticationToken token) {
  if (token!=null)
   return  token instanceof X509CertificateAuthenticationToken;
 
  return false;
 }

Метод getAuthentcationInfo отвечает за аутентификацию. Если предоставленный сертификат действителен и связан с учетной записью пользователя, область создает объект информации аутентификации. Помните, что первичный принципал должен быть таким же, как тот, который возвращается JNDIAndSaltAwareJdbcRealm:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // the cast is legal, since Shiro will let in only X509CertificateAuthenticationToken tokens
    X509CertificateAuthenticationToken certificateToken = (X509CertificateAuthenticationToken) token;
    X509Certificate certificate = certificateToken.getCertificate();
     
    // verify certificate
    if (!certificateOK(certificate)) {
        return null;
    }
     
    // the issuer name and serial number uniquely identifies certificate
    BigInteger serialNumber = certificate.getSerialNumber();
    String issuerName = certificate.getIssuerDN().getName();
     
    // find account associated with certificate
    String username = findUsernameToCertificate(issuerName, serialNumber);
    if (username == null) {
        // return null as no account was found
        return null;
    }
     
    // sucesfull verification, return authentication info
    return new SimpleAuthenticationInfo(username, certificate, getName());
}

Обратите внимание, что у области есть два новых свойства: trustStore и trustStorePassword. Оба необходимы для проверки сертификата PGP. Как и любое другое свойство, оба настраиваются в файле конфигурации.
Добавьте новую область в файл Shiro.ini :

1
2
3
4
[main]
certificateRealm = org.meri.simpleshirosecuredapplication.realm.X509CertificateRealm
certificateRealm.trustStore=src/main/resources/truststore
certificateRealm.trustStorePassword=secret

Теперь можно войти в приложение с сертификатом PGP. Если сертификат недоступен, имя пользователя и пароль тоже работают.

Исходный код приложения доступен в ветке «certificate_as_alternative_log_in_method» на Github.

Несколько Царств

Если файл конфигурации содержит более одной области, все они используются. В этом случае Широ пытается аутентифицировать пользователя со всеми настроенными областями, и результаты аутентификации объединяются. Ответственный за объединение объект называется стратегией аутентификации. Framework предоставляет три стратегии аутентификации:

По умолчанию используется «хотя бы одна успешная стратегия», которая хорошо подходит для наших целей. Опять же, можно создать собственную стратегию аутентификации. Например, мы можем потребовать, чтобы пользователь представил как сертификат PGP, так и имя пользователя / пароль для входа в систему.
Новые требования: пользователь должен представить и сертификат PGP, и имя пользователя / пароль для входа в систему.

Другими словами, нам нужна стратегия, которая:

  • терпит неудачу, если какая-то область не поддерживает токен,
  • не удается, если какая-то область не аутентифицирует пользователя,
  • терпит неудачу, если две области аутентифицируют различных принципалов.

Стратегия аутентификации — это объект, который реализует интерфейс стратегии аутентификации . Методы интерфейса вызываются после и до попытки аутентификации. Мы создаем « основной принцип такой же стратегии аутентификации » из «всех успешных стратегий», ближайших доступных стратегий. Мы сравниваем принципы после каждой попытки аутентификации области:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
public AuthenticationInfo afterAttempt(...) {
    validatePrimaryPrincipals(info, aggregate, realm);
    return super.afterAttempt(realm, token, info, aggregate, t);
}
 
private void validatePrimaryPrincipals(...) {
     ...
 
    Object aggregPrincipal = aggregPrincipals.getPrimaryPrincipal();
    Object infoPrincipal = infoPrincipals.getPrimaryPrincipal();
    if (!aggregPrincipal.equals(infoPrincipal)) {
        String message = 'All realms are required to return the same primary principal. Offending realm: ' + realm.getName();
        log.debug(message);
        throw new AuthenticationException(message);
    }
}

Стратегия аутентификации настраивается в файле Shiro.ini :

1
2
3
4
# multi-realms strategy
authenticationStrategy=org.meri.simpleshirosecuredapplication.authc.
PrimaryPrincipalSameAuthenticationStrategy
securityManager.authenticator.authenticationStrategy = $authenticationStrategy

Наконец, мы должны изменить метод isLoginSubmission в CertificateOrFormAuthenticationFilter обратно. Только запросы с именем пользователя и паролем теперь считаются попытками входа в систему. Сертификат больше не является достаточным:

1
2
3
4
@Override
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
  return super.isLoginSubmission(request, response);
}

Если вы запустите приложение сейчас, методы сертификата и имени пользователя / пароля в журнале являются обязательными.

Эта версия доступна в ветке «certificate_as_mandatory_log_in_method» на Github.

Конец

Эта часть была посвящена сферам Сиро. Мы создали три разные версии приложений, все они доступны на Github. Они охватили основные и, вероятно, наиболее важные функции области.

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

Ссылка: Apache Shiro, часть 2 — Королевства, базы данных и сертификаты PGP от нашего партнера JCG Марии Юрковичовой в блоге This is Stuff .