Статьи

Spring Security — две области безопасности в одном приложении

Этот пост в основном о конфигурации Spring Security.
Более конкретно, он намерен показать, как настроить две разные области безопасности в одном веб-приложении.

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

Вторая область безопасности предназначена для запросов веб-службы REST, поступающих из приложения Android. При каждом запросе клиент REST должен отправлять необходимую информацию на сервер, и эта информация будет использоваться для принятия решения о том, следует ли разрешить передачу запроса RESTfull.
Две области безопасности (конфигурации) различаются по разным шаблонам URL ресурсов в веб-приложении. В обеих конфигурациях мы можем использовать одну и ту же логику аутентификации.

Первая сфера безопасности

У нас есть классическое веб-приложение с некоторыми защищенными ресурсами (страницами). Чтобы получить доступ к этим ресурсам, пользователь должен войти в приложение на странице входа. Если вход был успешным, пользователь перенаправляется на запрошенный ресурс. Если по какой-либо причине происходит сбой процесса входа пользователя (например, неверное имя пользователя или пароль), то пользователь не может получить защищенный ресурс, и он снова перенаправляется на страницу входа с соответствующим сообщением.
Случай, который я только что описал в разделе выше, может рассматриваться как «классическое поведение веб-приложения». Среднестатистический интернет-пользователь сталкивался как минимум с сотнями онлайн-приложений, которые ведут себя так Такое поведение предназначено для работы с клиентами (браузерами). Поскольку такого рода поведение сегодня довольно распространено, Spring Security позволяет легко реализовать это. Очевидно, что механизм аутентификации на основе форм подходит нам лучше всего. В безопасности Spring, когда вы хотите определить действия, связанные со статусом аутентификации клиента, вы можете определить точку входа. Вот предварительный просмотр нашей стандартной точки входа браузер-клиент:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
        <http  entry-point-ref="loginUrlAuthenticationEntryPoint" use-expressions="true">
 <intercept-url pattern="/includes/content/administration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
 <intercept-url pattern="/includes/content/userAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
 <intercept-url pattern="/includes/content/groupAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
 <intercept-url pattern="/includes/content/departmentAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
 <intercept-url pattern="/includes/content/shiftAdministration.jsp" access="hasAnyRole('ROLE_100','ROLE_101','ROLE_1000') />
 <custom-filter position="FORM_LOGIN_FILTER" ref="userAuthenticationProcessingFilter" />
 <logout logout-url='/logout' />
</http>
  
<beans:bean id="loginUrlAuthenticationEntryPoint"
 class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
 <beans:property name="loginFormUrl" value="/login.jsp" />
</beans:bean>

Надеюсь, это довольно очевидно. loginUrlAuthenticationEntryPoint — это точка входа, где вы можете настроить страницу входа в систему, где вы реализовали свою функцию входа в систему. Затем в элементе http мы настроили поведение этой точки входа на более подробную информацию. Сначала мы определили список элементов intercept-url . Эта точка входа будет активирована, только если один из этих ресурсов был запрошен. Мы также заменили стандартный файл FORM_LOGIN_FILTER на нашу собственную настроенную версию. Spring Security функционирует, применяя цепочку фильтров, которые вы определяете в своей точке входа. Это в основном стандартные фильтры сервлетов. Вы можете использовать предопределенные фильтры Spring или расширить их и подключить свой собственный фильтр. Здесь мы использовали один из фильтров безопасности Spring.

Это Имя пользователяPasswordAuthenticationFilter . Он используется в ситуации, когда у нас есть страница входа в систему с полями имени пользователя и пароля. Этот фильтр позволяет нам включить наш собственный механизм, который будет использоваться для аутентификации. Это также позволяет нам определять действия, которые будут предприняты в случае успешной и неудачной аутентификации. Давайте посмотрим, как выглядит эта конфигурация:

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
26
27
       <beans:bean id="loginSuccessHandler"
 class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
 <beans:property name="defaultTargetUrl" value="/main.jsp" />
</beans:bean>
  
<beans:bean id="userAuthenticationProcessingFilter"
 class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
 <beans:property name="authenticationManager" ref="authenticationManager" />
 <beans:property name="authenticationFailureHandler"
  ref="loginMappingFailureHandler" />
 <beans:property name="authenticationSuccessHandler"
  ref="loginSuccessHandler" />
</beans:bean>
  
<beans:bean id="loginMappingFailureHandler"
 class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
 <beans:property name="exceptionMappings" ref="failureUrlMap" />
</beans:bean>
  
<util:map id="failureUrlMap" map-class="java.util.HashMap">
 <beans:entry
  key="org.springframework.security.authentication.BadCredentialsException"
  value="/login.jsp?errorMessage=bad.credentials" />
 <beans:entry
  key="org.springframework.security.authentication.DisabledException"
  value="/login.jsp?errorMessage=disabled.user" />
</util:map>

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

Во-вторых, мы определили обработчик ошибок входа в систему. По сути, это карта исключений и действий Spring, которые предпринимаются для этих исключений. Исключения выдает AuthenticationProvider, который описан ниже. Например, когда пользователь вводит неправильное имя пользователя или пароль, выдается исключение BadCredentialsException . И когда это происходит, пользователь снова перенаправляется на страницу входа. Также определенный параметр добавляется к URL-адресу страницы входа в систему, чтобы мы могли отображать правильное сообщение об ошибке.

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

Теперь давайте пару слов о менеджере аутентификации. Это просто интерфейс, который использует Spring. Это может быть что угодно. Это может быть база данных пользователей, сервер LDAP или что-то еще. Менеджер аутентификации не выполняет работу самостоятельно. Он использует только провайдеров аутентификации для выполнения фактической работы аутентификации для него. Поставщики аутентификации, когда они вызываются, могут делать две вещи:

  1. Может вернуть успешно заполненный объект (который является экземпляром интерфейса аутентификации Spring)
  2. Может выдать одно из соответствующих исключений безопасности Spring

Вот как выглядит конфигурация менеджера аутентификации:

1
2
3
4
5
6
7
8
       <authentication-manager alias="authenticationManager">
 <authentication-provider ref="authenticationProvider" />
</authentication-manager>
  
<beans:bean id="authenticationProvider" class="ba.codecentric.medica.security.UserAuthenticationProvider">
 <beans:property name="userService" ref="userService"/>
 <beans:property name="licenseInformationWrapper" ref="licenseInformationWrapper"/>
</beans:bean>

А вот исходный код моего пользовательского провайдера аутентификации:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package ba.codecentric.medica.security;
  
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
  
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
  
import ba.codecentric.medica.administration.service.UserService;
import ba.codecentric.medica.model.Group;
import ba.codecentric.medica.model.LicenseInformationWrapper;
import ba.codecentric.medica.model.Role;
import ba.codecentric.medica.model.User;
  
public class UserAuthenticationProvider implements AuthenticationProvider {
  
 private static final String ROLE_PREFIX = "ROLE_";
  
 private UserService userService;
  
 private LicenseInformationWrapper licenseInformationWrapper;
  
 @Override
 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  User user = userService.getUserByUsernameAndPassword(authentication.getName(), authentication.getCredentials()
    .toString(), true);
  
  if (user != null) {
   Collection authorities = new ArrayList(buildRolesFromUser(user));
   authorities.addAll(getActivatedModulesAsRoles());
   return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),
     authorities);
  } else {
   throw new BadCredentialsException("Try again");
  }
  
 }
  
 private Collection getActivatedModulesAsRoles() {
  List activatedModules = new ArrayList();
  if(CollectionUtils.isNotEmpty(licenseInformationWrapper.getActivatedModules())) {
   for(String activatedModuleName: licenseInformationWrapper.getActivatedModules()) {
    activatedModules.add(new SimpleGrantedAuthority(ROLE_PREFIX + activatedModuleName));
   }
  }
  return activatedModules;
 }
  
 private Collection buildRolesFromUser(User user) {
  Collection authorities = new HashSet();
  
  for (Group group : user.getGroups()) {
   for (Role role : group.getRoles()) {
  
    authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));
   }
  }
  return authorities;
 }
  
 @Override
 public boolean supports(Class authentication) {
  return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
 }
  
 public UserService getUserService() {
  return userService;
 }
  
 public void setUserService(UserService userService) {
  this.userService = userService;
 }
  
 public LicenseInformationWrapper getLicenseInformationWrapper() {
  return licenseInformationWrapper;
 }
  
 public void setLicenseInformationWrapper(LicenseInformationWrapper licenseInformationWrapper) {
  this.licenseInformationWrapper = licenseInformationWrapper;
 }
  
}

Как видите, процесс аутентификации действительно прост. Мой пользовательский поставщик аутентификации реализует интерфейс Spring AuthenticationProvider .

И это делает работу так же, как мы обсуждали ранее. Он ищет имя пользователя и пароль в таблице пользователей в базе данных. Если такой пользователь найден, то объект аутентификации создается и возвращается. В противном случае, если такого пользователя не найдено, метод authenticate создает соответствующее исключение. И вот еще. Spring использует коллекцию объектов GrantedAuthority для представления ролей, которые предоставляются пользователю. По этой причине мы собираем такую ​​коллекцию и прикрепляем ее к объекту аутентификации. Каждая роль, связанная с пользователем в базе данных, должна быть добавлена ​​в коллекцию предоставленных прав доступа, чтобы Spring мог рассматривать это как роль. И каждая роль должна иметь префикс ROLE_ . У нас есть еще одна вещь, чтобы показать. Как этот фильтр вызывается с веб-страницы входа? Вот часть файла login.jsp:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<form id="loginForm" method="POST" action="j_spring_security_check">
  
  
  
 <table>
<tr>
   <td><b><fmt:message key="login.username.label" />:</b></td>
    <c:choose>
     <c:when test="${not empty param.j_username}" >
      <td><input type="text" name="j_username" id="username" value="${param.j_username }" class="loginInput" /></td>   
     </c:when>
     <c:otherwise>
      <td><input type="text" name="j_username" id="username" class="loginInput"/></td>
     </c:otherwise>
    </c:choose>
  </tr>
<tr>
   <td><b><fmt:message key="login.password.label" />:</b></td>
    <c:choose>
     <c:when test="${not empty param.j_password}" >
      <td><input type="password" name="j_password" id="password" value="${param.j_password }" class="loginInput" /></td>   
     </c:when>
            <c:otherwise>
      <td><input type="password" name="j_password" id="password" class="loginInput" /></td>
     </c:otherwise>
    </c:choose>
  </tr>
<tr>
   <td><b><fmt:message key="login.alphabet.label" /></b>:</td>
   <td><select id="alphabet" class="fullWidth" onchange="languageSelect();">
    <option value="lat">
          <fmt:message key="login.alphabet.lat" />
    </option>
    <option value="cir">
          <fmt:message key="login.alphabet.cyr" />
    </option>
   </select></td>
  </tr>
<tr>
   <td></td>
   <td><input type="submit" value="<fmt:message key="login.button.label" />" class="right"></td>
   </tr>
</table></form>

Стандартная настройка безопасности Spring по умолчанию требует, чтобы вы вызывали цепочку безопасности из формы входа в систему, вызывая j_spring_security_check . Фильтр имени пользователя и пароля будет перехватывать этот URL (купить по умолчанию), но вы можете настроить его на перехват любого другого URL. Ну, это все, что касается области безопасности «клиент на основе браузера». Если пользователь не вошел в систему и пытается получить доступ к ресурсу, защищенному этой областью (точкой входа), тогда область собирается перенаправить пользователя на страницу входа и попросить его войти в систему. Только если пользователь входит в систему, тогда защищенная ресурс будет доступен.

Второе царство безопасности

Теперь, наконец, давайте поговорим о второй области безопасности в приложении. Мы упоминали об этом только во вступительной части блога. Это приложение поддерживает вызовы службы REST. Нам пришлось выполнить требование по синхронизации определенных частей приложения с простым приложением для Android, работающим на мобильных устройствах. Мы решили, что самым простым способом будет совершать RESTfull звонки с мобильного телефона в веб-приложение. И, конечно, здесь нам также нужна безопасность. Мы не хотим, чтобы пользователи всегда могли подключаться к приложению. Список пользователей и их ролей ведется в базе данных. Например, пользователь может быть активным сегодня, но завтра администратор может решить, что этот пользователь больше не активен и не должен иметь возможность подключаться к приложению (также не должен иметь возможность войти в систему). Как следствие этого требования, безопасность должна быть реализована в сфере услуг REST.

Давайте подумаем об этом мире на секунду. Как эти REST звонки должны работать. Приложение Android отправляет запросы POST (запросы RESTfull) в веб-приложение для получения определенных данных (назначений врача и т. Д.). Приложение находит и возвращает запрошенные данные. Приложение Android обрабатывает полученные данные и отображает их для пользователя.

Теперь давайте добавим безопасность к этой концепции RESTfull и попробуем описать концепцию с безопасностью. Приложение Android отправляет запросы POST. Приложение Android отправляет заголовок, содержащий хэшированное имя пользователя и пароль, как часть каждого запроса (см. -> Базовая аутентификация ).

Предполагается, что область безопасности веб-приложения (точка входа) получит этот запрос, и если имя пользователя и пароль действительно представляют активного пользователя, то этот запрос может получить доступ к службе REST в веб-приложении, и он будет обработан. Если по какой-либо причине имя пользователя и пароль недействительны (или пользователь неактивен), тогда в точке входа безопасности должен произойти сбой запроса, что означает, что мы должны немедленно вернуть правильно отформатированный HTTP-ответ, который уведомит клиентское приложение, что пользователь с этим пользователем Имя и пароль не имеют доступа к службе REST в веб-приложении.

Как мы видим в этом случае, поведение ранее определенной точки входа не соответствует сервисам REST. Предыдущая точка входа, перенаправляет пользователя на страницу входа, если он не аутентифицирован. Это означает, что если пользователь не аутентифицирован, сервер фактически возвращает HTTP-ответ, содержащий HTML-код страницы входа в систему. Мы не можем справиться с подобным поведением в приложении для Android, так как оно не отображает веб-страницы HTML. Так что же он будет делать, когда получит HTML-код страницы входа?
Это главная причина, по которой нам на самом деле нужна вторая сфера безопасности (точка входа безопасности) в веб-приложении, которая будет работать иначе, чем наш механизм, который работает с клиентами браузера. Эта новая область безопасности будет возвращать правильно отформатированный HTTP-ответ клиентскому приложению только в том случае, если пользователь не может пройти аутентификацию (он установит определенный HTTP-статус и HTTP-сообщение в ответе).
Мы знаем, что в среде Java Server у нас есть тип безопасности, называемый Basic Authentication . Он основан на отправке хэшированного имени пользователя и пароля как части заголовков запроса (заголовки должны быть отформатированы определенным образом). Затем, если имя пользователя и пароль могут быть найдены в пуле данных пользователя, запрос разрешается передавать. В противном случае возвращается HTTP-ответ с соответствующим статусом и сообщением, информирующим клиента о том, что ему не разрешен доступ к определенному ресурсу. К счастью для нас, Spring поддерживает такой механизм аутентификации. Мы собираемся добавить еще одну точку входа и фильтр. Вот как это будет выглядеть:

01
02
03
04
05
06
07
08
09
10
11
12
13
       <http entry-point-ref="basicAuthEntryPoint" pattern="/ws/**" use-expressions="true">
 <intercept-url pattern="/ws/schedule/patients" access="hasAnyRole('ROLE_1','ROLE_100','ROLE_300','ROLE_1000')" />
 <custom-filter ref="basicAuthenticationFilter" after="BASIC_AUTH_FILTER" />
</http>
  
<beans:bean id="basicAuthEntryPoint" class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
 <beans:property name="realmName" value="REST Realm" />
</beans:bean>
  
<beans:bean id="basicAuthenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
 <beans:property name="authenticationManager" ref="authenticationManager"/>
 <beans:property name="authenticationEntryPoint" ref="basicAuthEntryPoint" />
</beans:bean>

По сути, мы добавили новую точку входа (область безопасности), которая перехватывает все запросы по пути URL / ws / ** . Это путь, по которому проходят все наши вызовы службы REST. Мы использовали Springs BasicAuthenticationFilter, который обеспечивает функциональность чтения заголовков запросов и вызова диспетчера аутентификации. Если имя пользователя и пароль найдены в базе данных (обрабатывается менеджером аутентификации), запрос будет разрешен для дальнейшего прохождения. Если имя пользователя и пароль не найдены в базе данных, точка входа установит статус 401 в ответе HTTP и немедленно вернет этот ответ клиенту. Это просто поведение, которое нам нужно для приложения Android.

И это все настройки безопасности, в которых нуждается наше приложение. Теперь осталось только включить фильтры безопасности Spring в файле web.xml. Я уже упоминал, что Spring Security работает, вызывая цепочки фильтров по запросу. Это означает, что существует некоторый «главный» фильтр, который является основой для всех последующих фильтров и сервисов. Этот «основной» фильтр включен и настроен в файле web.xml. Вот моя конфигурация:

01
02
03
04
05
06
07
08
09
10
11
12
       <filter>
 <filter-name>springSecurityFilterChain</filter-name>
 <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
 <filter-name>springSecurityFilterChain</filter-name>
 <url-pattern>/*</url-pattern>
 <dispatcher>ERROR</dispatcher>
 <dispatcher>REQUEST</dispatcher>
 <dispatcher>INCLUDE</dispatcher>
 <dispatcher>FORWARD</dispatcher>
</filter-mapping>

Как видите, основной фильтр безопасности Spring настроен на перехват всех запросов ко всем ресурсам приложения. Но какие ресурсы действительно защищены, а какие общедоступны, контролируется точками входа (через шаблоны URL в элементах http). Например, все ресурсы, расположенные в папке / css , считаются общедоступными и не требуют, чтобы пользователь проходил аутентификацию, чтобы иметь к ним доступ:

1
<http pattern="/css/**" security="none" />

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

1
2
3
<!-- more xml -->
<intercept-url pattern="/includes/content/administration.jsp" access="hasAnyRole('ROLE_100','ROLE_1000')" />
<!-- more xml -->

И еще две очень важные вещи, чтобы сказать!

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

После прочтения этого блога вы можете подумать, что достаточно добавить аутентификацию на основе форм или обычную аутентификацию, и ваше приложение будет в безопасности. Это, однако, не совсем верно. Любой, обладающий «техническими» знаниями по протоколу HTTP и работе в сети, может, вероятно, подумать о том, как перехватывать потоки данных HTTP внутри сети. В случае обычной проверки подлинности и проверки подлинности на основе форм такая информация, как имя пользователя и пароль, отправляется напрямую по протоколу HTTP. В случае базовой аутентификации они отправляются как заголовки HTTP-запроса. В случае аутентификации на основе форм они отправляются как параметры запроса. Таким образом, человек, который может перехватывать и читать эти HTTP-потоки, может легко прочитать ваши заголовки и запросить параметры. Позже этот же человек может вручную создавать запросы и прикреплять эти заголовки или параметры к запросу. Конечно, этот новый запрос теперь будет авторизован контейнером, поскольку он содержит ваши правильные данные аутентификации.

Итак, что мы можем сделать, чтобы избежать этих атак безопасности на наше приложение?
Реальный ответ будет таким: мы должны использовать протокол HTTPS везде, где у нас есть защищенные ресурсы в нашем приложении. Только используя протокол HTTPS и механизмы аутентификации Java-сервера, мы можем с большой уверенностью утверждать, что наше приложение действительно безопасно.

Справка: Spring Security — две области безопасности в одном приложении от нашего партнера по JCG Бранислава Видови? в блоге Geek 🙂 .