-
Notifications
You must be signed in to change notification settings - Fork 56
Spring security
Spring Security - это самостоятельная библиотека построенная на основе Spring для обеспечения принципов безопасности.
Spring Security позволяет авторизовать пользователей, а также обеспечивать контроль доступа.
Spring Security реализует как программный, так и декларативный (с использованием аннотаций и Spring Expression Language) подходы к настройке и проверке прав доступа.
Spring Security может быть использована как в составе Spring для web, так и самостоятельно без Spring или, например, в SWING приложениях.
Краеугольным камнем Spring Security являются такие понятия как SecurityContext
и SecurityContextHolder
. SecurityContextHolder
- это класс с помощью, которого можно получить текущий контекст безопасности (SecurityContext
). Текущий контекст безопасноcти хранит данные об аутентификации (Authentication
) доверителя (Principal
).
SecurityContextHolder
по умолчанию использует ThreadLocal, что делает возможным его получение из любого места приложения, в случае работы в многопоточной среде есть соответствующие настройки для передачи SecurityContext
между потоками.
Получение данных об аутентификации и доверителе (Principal
) будет выглядеть так:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Object user = authentication.getPrincipal();
В переменной user
могут быть сохранены 2 типа значений:
- Объект реализующий
UserDetails
- Строка с представлением пользователя
Получение имени пользователя может выглядеть так:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails) {
username = ((UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
Основными методами в SecurityContextHolder
являются методы получение и установки контекста безопасности.
SecurityContext getContext(); // получить контекст безопасности
void setContext(SecurityContext context); // установить контекст безопасности
Интерфейс, который должны реализовывать классы, которые представляют маркет для запроса проверки безопасности или авторизованного доверителя (Principal
).
Класс, который реализует интерфейс Authentication
, возвращается в ответ на попытку авторизации:
// Authentication authToken;
// AuthenticationManager am;
Authentication auth = am.authenticate(authToken);
Таким образом, в объект реализующий Authentication
c заполненными полями username
и credentials
передаётся объекту AuthenticationManager
, который проверяет возможность аутентификации и устанавливает флаг isAuthenticated()
данного объекта.
Другими словами Authentication
представляет данные пользователя для аутентификации или аутентифицированного пользователя.
Основными методами являются
Collection<? extends GrantedAuthority> getAuthorities(); // Получение списка полномочий
Object getCredentials(); // Получение пароля
Object getDetails(); // Получение дополнительных данных по пользователю
Object getPrincipal(); // Получение объекта или идентификатора авторизованного пользователя
boolean isAuthenticated(); // Получить статус аутентификации
Интерфейс представляет собой описание полномочий, который выданы объекту аутентификации.
String getAuthority();
Полномочия представлены как строки, если возможно, иначе этот данный метод возвращает null
.
Базовой реализацией GrantedAuthority
является класс SimpleGrantedAuthority
, который представляет полномочие как строку.
Полномочие - широкое понятие, оно может представлять как право, так и, например, роль. В таком случае данное полномочие будет представлено как:
GrantedAuthority roleAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
При проверке прав доступа с полномочиями работает AccessDecisionManager
, который на основании полномочий выбирает предоставлять доступ или нет.
AuthenticationManager
- интерфейс, который должны реализовывать классы, которые производят аутентификацию и заполнение по переданному объекту Authentication
(включая список полномочий, если аутентификация прошла успешно).
Единственный метод, который должен реализовывать класс реализующий этот интерфейс - это метод аутентификации.
Authentication authenticate(Authentication authentication) throws AuthenticationException;
В состав Spring security входит реализация данного интерфейса - ProviderManager
, который реализует аутентификацию как делегирование вызова authenticate()
провайдерам аутентификации, которые реализуют интерфейс AuthenticationProvider
.
Интерфесй AuthenticationProvider
содержит 2 метода:
-
Authentication authenticate(Authentication authentication) throws AuthenticationException
- собственно аутентификация -
boolean supports(Class<?> authentication);
- проверка возможно применения данного провайдера аутентификации к данному объекту аутентификации
ProviderManager
таким образом проходит по списку провайдеров и вызывает метод supports
. Если провайдер поддерживает данный объект аутентификации, то вызывается метод authenticate()
. Провайдеры проверяются по порядку до первой успешной аутентификации. Данное свойство позволяет реализовывать гибкие правила аутентификации, например, сотрудники компании аутентифицируются через LDAP, а остальные пользователи аутентифицируются через доступ к БД.
В состав Spring Security входит несколько провайдеров аутентификации, которые можно использовать рассмотрим наиболее интересные и важные:
-
DaoAuthenticationProvider
- провайдер аутентификации через DAO -
TestingAuthenticationProvider
- тестовый провайдер аутентификации (возвращает неизменный объект аутентификации для Unit тестирования) -
LdapAuthenticationProvider
- провайдер аутентификации через LDAP
Рассмотрим DaoAuthenticationProvider
и LdapAuthenticationProvider
.
DaoAuthenticationProvider
- это провайдер, который использует DAO (точнее UserDetailsService
) для аутентификации пользователей.
Данный класс использует дополнительные классы для аутентификации:
-
void setPasswordEncoder(Object passwordEncoder);
- метод устанаваливает шифровальщик паролей для шифрования и проверки паролей -
void setSaltSource(SaltSource saltSource);
- устанавливает поставщик "соли" для декодирования паролей -
void setUserDetailsService(UserDetailsService userDetailsService);
- устанавливаетUserDetailsService
для получения данных пользователя
DaoAuthenticationProvider
- использует UserDetailsService
для получения пользователя по имени пользователя и сравнивает пароли используя PasswordEncoder
(если установлен SaltSource
, то он используется для добавления к паролю "соли").
Spring Security поставляется с несколькими реализациями UserDetailsService
.
-
InMemoryUserDetailsManager
- хранилище данных пользователей в памяти -
JdbcUserDetailsManager
- хранилище данных пользователей в БД -
LdapUserDetailsManager
- хранилище данных пользователей в LDAP
Класс реализующий интерфейс UserDetails
представляет данные пользователя и список полномочий. UserDetails
расширенные свойства для данных пользователя.
Методы интерфейса UserDetails
:
-
Collection<? extends GrantedAuthority> getAuthorities();
- получение списка полномочий -
String getPassword();
- получение пароля, если это возможно (реализацииUserDetails
обычно "затирают" пароль после аутентификации) -
String getUsername();
- получение имени пользователя -
boolean isAccountNonExpired();
- является ли аккаунт просроченным -
boolean isAccountNonLocked();
- является ли аккаунт заблокированным -
boolean isCredentialsNonExpired();
- не истекло ли время действия пароля -
boolean isEnabled();
- является ли пользователь активным
Кроме интерфейса UserDetails
существуют интерфейсы, которые расширяют данный интерфейс специализированными полями, например, LdapUserDetails
.
Так же существуют и реализации обоих интерфейсов, например, User
(для UserDetails
) или InetOrgPerson
(для LdapUserDetails
).
Интерфейс UserDetailsService
реализует сервис получения данных пользователя по имени пользователя.
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
Spring security поставляется с интерфейсом UserDetailsManager
, который расширяет UserDetailsService
дополнительными методами:
-
void createUser(UserDetails user);
- создание пользователя -
void updateUser(UserDetails user);
- обновление данных пользователя -
void deleteUser(String username);
- удаление пользователя -
void changePassword(String oldPassword, String newPassword);
- изменение пароля пользователя -
boolean userExists(String username);
- проверка существования пользователя
Менеджер пользователей, который хранит данные пользователей в памяти. Данный менеджер удобно использовать для unit-тестирования.
Данный менеджер позволяет получать пользователей из БД используя JDBC из таблиц:
-
users
- таблица пользователей -
authorities
- таблица полномочий -
groups
- таблица групп -
group_members
- таблица соответствия между группами и пользователями -
group_authorities
- таблица групповых полномочий
Данный менеджер получает данные пользователя из LDAP и заполняет соответствующие поля.
Для авторизации через LDAP Spring security предоставляет соответствующий адаптер LdapAuthenticationProvider
. Данный адаптер производит аутентификацию пользователя с использованием объекта, реализующего интерфейс LdapAuthenticator
и LdapAuthoritiesPopulator
.
LdapAuthenticator
необходим для выбора типа аутентификации в LDAP: биндом или проверкой пароля.
Соответствующие классы реализуют данный интерфейс:
-
BindAuthenticator
для проверки биндом -
PasswordComparisonAuthenticator
проверкой пароля
Из соображений безопасности более предпочтительным является BindAuthenticator
, так как в этом случае передача пароля(или его хэша) не производится.
Интерфейс LdapAuthoritiesPopulator
используется для заполнения полномочий пользователя.
В поставку Spring security входят следующие реализации данного интерфейса:
-
DefaultLdapAuthoritiesPopulator
- заполнение полномочий пользователя по списку групп из LDAP (ou=groups
) NullLdapAuthoritiesPopulator
-
UserDetailsServiceLdapAuthoritiesPopulator
- делегирует получение полномочийUserDetailsService
Рассмотрим наиболее важные классы подробнее:
Данный класс производит аутентификацию пользователя путём авторизации на LDAP сервере.
В качестве аргумента принимает объект, реализующий интерфейс BaseLdapPathContextSource
, который отвечает за предоставление базового адреса LDAP.
Реализацией интерфейса BaseLdapPathContextSource
может являться класс LdapContextSource
, который хранит:
- хост и порт LDAP сервера
- имя и пароль пользователя для аутентификации на LDAP сервере
TODO!
AccessDecisionManager
- осуществляет конечную проверку полномочий доступа.
Содержит следующие методы:
-
boolean supports(Class<?> clazz);
- проверка возможности поддержки класса -
boolean supports(ConfigAttribute attribute);
- проверка поддержки атрибута -
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
- проверяет доступ пользователя к объекту
Менеджер решений принимает решение о получении доступа опрашивая объекты типа Voter
. Voter
'ы голосуют за предоставление доступа.
Возможные варианты решений voter'ов:
-
ACCESS_GRANTED
- предоставить доступ -
ACCESS_ABSTAIN
- воздержаться от решения -
ACCESS_DENIED
- доступ запрещён
В стандартную поставку входят несколько менеджеров решений:
-
UnanimousBased
- все голосователи должны предоставить доступ или воздержаться -
AffirmativeBased
- хотя бы один предоставляет доступ -
ConsensusBased
- доступ предоставляется по решению большинства проголосовавших
TODO!
Подготовительный код:
package com.a1systems.security_test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.http.UserDetailsServiceFactoryBean;
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 org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* Hello world!
*
*/
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken("user1", "user1");
SampleAuthenticationManager am = new SampleAuthenticationManager();
Authentication auth = am.authenticate(authToken);
logger.debug("auth {}", auth);
SecurityContextHolder.getContext().setAuthentication(auth);
SecurityContext context = SecurityContextHolder.getContext();
logger.debug("Context {}", context);
Authentication authentication = context.getAuthentication();
logger.debug("authentication {}", authentication);
Object user = authentication.getPrincipal();
logger.debug("principal {}", user);
List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
voters.add(new RoleVoter());
voters.add(new MyVoter());
AffirmativeBased ab = new AffirmativeBased(voters);
User u1 = new User();
u1.setLogin("user1");
u1.setPassword("pswd1");
User u2 = new User();
u2.setLogin("user2");
u2.setPassword("pswd2");
Book b1 = new Book();
b1.setAuthor(u1);
b1.setTitle("Title1");
Book b2 = new Book();
logger.debug("principal {}", user);
List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
voters.add(new RoleVoter());
voters.add(new MyVoter());
AffirmativeBased ab = new AffirmativeBased(voters);
User u1 = new User();
u1.setLogin("user1");
u1.setPassword("pswd1");
User u2 = new User();
u2.setLogin("user2");
u2.setPassword("pswd2");
Book b1 = new Book();
b1.setAuthor(u1);
b1.setTitle("Title1");
Book b2 = new Book();
b2.setAuthor(u2);
b2.setTitle("Title2");
List<ConfigAttribute> list = new ArrayList<ConfigAttribute>();
list.add(new ConfigAttribute() {
@Override
public String getAttribute() {
return "view_book";
}
});
try {
ab.decide(authentication, b1, list);
} catch (AccessDeniedException e) {
logger.debug("Acces denied on b1");
}
try {
ab.decide(authentication, b2, list);
} catch (AccessDeniedException e) {
logger.debug("Acces denied on b2");
}
}
private static class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
AUTHORITIES.add(new ViewBookAuthority());
}
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
private static class ViewBookAuthority implements GrantedAuthority {
@Override
public String getAuthority() {
return "view_book";
}
}
private static class MyVoter implements AccessDecisionVoter {
@Override
public boolean supports(ConfigAttribute attribute)
{
package com.a1systems.security_test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.http.UserDetailsServiceFactoryBean;
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 org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
/**
* Hello world!
*
*/
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken("user1", "user1");
List<AuthenticationProvider> plist = new ArrayList<AuthenticationProvider>();
List<UserDetails> users = new ArrayList<UserDetails>();
users.add(new UserDetailsImpl("user1","user1"));
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(users);
daoProvider.setUserDetailsService(inMemoryUserDetailsManager);
plist.add(daoProvider);
ProviderManager am = new ProviderManager(plist);
Authentication auth = am.authenticate(authToken);
logger.debug("auth {}", auth);
SecurityContextHolder.getContext().setAuthentication(auth);
SecurityContext context = SecurityContextHolder.getContext();
logger.debug("Context {}", context);
Authentication authentication = context.getAuthentication();
logger.debug("authentication {}", authentication);
Object user = authentication.getPrincipal();
logger.debug("principal {}", user);
logger.debug("details {}", authentication.getDetails());
logger.debug("credentials {}", authentication.getCredentials());
List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
voters.add(new RoleVoter());
voters.add(new MyVoter());
AffirmativeBased ab = new AffirmativeBased(voters);
User u1 = new User();
u1.setLogin("user1");
u1.setPassword("pswd1");
User u2 = new User();
u2.setLogin("user2");
u2.setPassword("pswd2");
Book b1 = new Book();
b1.setAuthor(u1);
b1.setTitle("Title1");
Book b2 = new Book();
b2.setAuthor(u2);
b2.setTitle("Title2");
List<ConfigAttribute> list = new ArrayList<ConfigAttribute>();
list.add(new ConfigAttribute() {
@Override
public String getAttribute() {
return "view_book";
}
});
try {
ab.decide(authentication, b1, list);
logger.debug("Acces granted on b1");
} catch (AccessDeniedException e) {
logger.debug("Acces denied on b1");
}
try {
ab.decide(authentication, b2, list);
logger.debug("Acces granted on b1");
} catch (AccessDeniedException e) {
logger.debug("Acces denied on b2");
}
}
private static class UserDetailsImpl implements UserDetails {
private String username;
private String password;
private UserDetailsImpl(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
authorityList.add(new ViewBookAuthority());
return authorityList;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
private static class ViewBookAuthority implements GrantedAuthority {
@Override
public String getAuthority() {
return "view_book";
}
}
private static class MyVoter implements AccessDecisionVoter {
@Override
public boolean supports(ConfigAttribute attribute) {
return "view_book".equals(attribute.getAttribute());
}
@Override
public boolean supports(Class clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection attributes) {
Logger logger = LoggerFactory.getLogger(MyVoter.class);
logger.debug("{} {}", object.getClass(), object);
Object principal = authentication.getPrincipal();
logger.debug("{}", principal);
Book b = (Book)object;
return b.getAuthor().getLogin() == ((org.springframework.security.core.userdetails.User)principal).getUsername() ? ACCESS_GRANTED : ACCESS_DENIED;
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
">
<!-- Настройка AuthenticationManaher'а -->
<security:authentication-manager id="authManager">
<!-- Настройка Ldap Authentication Provider'а -->
<security:authentication-provider ref="ldapAuthProvider" />
</security:authentication-manager>
<!-- -->
<bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<!-- -->
<constructor-arg ref="ldapBindAuthenticator" />
</bean>
<bean id="ldapDirectoryServer" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://ldapserver.domain:389/" />
<property name="userDn" value="CN=Manager,OU=Company,DC=Company,DC=com" />
<property name="password" value="some_pa$$w0rd" />
</bean>
<bean id="ldapBindAuthenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
<constructor-arg ref="ldapDirectoryServer" />
<property name="userSearch" ref="ldapSearchBean"/>
</bean>
<bean id="ldapSearchBean" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg value="ou=users,dc=company,dc=com" />
<constructor-arg value="(sAMAccountName={0})" />
<constructor-arg ref="ldapDirectoryServer" />
</bean>
</beans>
package com.a1systems.security_test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("spring/context.xml");
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken("b.gates", "123456");
AuthenticationManager am = (AuthenticationManager)ctx.getBean("authManager");
Authentication auth = am.authenticate(authToken);
logger.debug("{}", auth);
FilterBasedLdapUserSearch search = (FilterBasedLdapUserSearch)ctx.getBean("ldapSearchBean");
DirContextOperations searchForUser = search.searchForUser("j.smith");
logger.debug("{}", searchForUser);
}
}