-
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);
}
}