Сеанс в Spring Security с аутентификацией LDAP для SPA (одностраничное приложение)JAVA

Программисты JAVA общаются здесь
Ответить Пред. темаСлед. тема
Anonymous
 Сеанс в Spring Security с аутентификацией LDAP для SPA (одностраничное приложение)

Сообщение Anonymous »

В проекте, над которым я работаю, реализованы собственные механизмы аутентификации, но, поскольку они были перенесены из другого проекта, в них имеется много ненужных функций. Вот почему я решил попробовать заменить их решениями Spring в надежде уменьшить размер кодовой базы. Это был адский путь, и я застрял.
Архитектура такова: есть интерфейс React + серверное приложение Spring Boot. Файлы внешнего интерфейса помещаются в папку ресурсов Java, а затем обслуживаются Spring Tomcat. В то же время серверная часть предоставляет ряд конечных точек для использования внешним интерфейсом, включая вход в систему, выход из системы, настройку и все виды конечных точек данных. Фронтенд вызывает конечную точку входа с именем пользователя + паролем в теле, чтобы установить файл cookie сеанса, который затем позволит ему вызывать другие конечные точки без предоставления учетных данных каждый раз.
Я нашел это руководство: https:// Spring.io/guides/gs/authenticating-ldap, где они используют комбинацию авторизацииHttpRequests + formLogin. Это хорошее начало, но необходимо выполнить множество настроек, поскольку мой проект является SPA.
Мне удалось заставить работать аутентификацию LDAP, но, к сожалению, я не могу принудительно любая из аутентифицированных точек работает. Все они возвращают статус 403 Forbidden — я погрузился в внутренности Spring гораздо глубже, чем когда-либо хотел, чтобы обнаружить, что сеанс не переносится между запросами. Симптомы следующие:
  • файл cookie сеанса, JSESSIONID, не установлен, а ApplicationSessionCookieConfig#createSessionCookie никогда не вызывается,
    аутентификация — это AnonumousAuthenticationToken везде — например, AuthorizationFilter#getAuthentication возвращает вышеупомянутый токен, и, таким образом, доступ отказано,
  • стратегией держателя контекста безопасности является InheritableThreadLocalSecurityContextHolderStrategy – я не знаю, хорошо это или нет
  • Код: Выделить всё

    HttpSessionSecurityContextRepository#loadDeferredContext
    вызывает request.getSession(false), который возвращает значение null — сеанс не существует
  • в конце, поскольку нет аутентификации< /code> в SecurityContextHolder, срабатывает AnonymousAuthenticationFilter.
Я предоставляю свою конфигурацию безопасности ниже. Я оставил много прокомментированного кода, чтобы дать вам представление о том, что я пробовал:

Код: Выделить всё

package com.visiona.suka.gui.adapter.in.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
import java.util.List;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.header.HeaderWriterFilter;

import static com.visiona.suka.gui.adapter.in.web.CspFilter.NONCE_PLACEHOLDER;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

static final String LOGIN_ENDPOINT_PATH = "/v1/login";
static final String LOGOUT_ENDPOINT_PATH = "/v1/logout";
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(SecurityConfiguration.class);
private static final String DEFAULT_CSP_PATTERN =
"default-src 'self'; style-src 'self' 'nonce-" + NONCE_PLACEHOLDER + "'";
private static final String CONFIGURATION_PATH = "/v1/configuration";
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${adapter.in.web.cspPattern:"  + DEFAULT_CSP_PATTERN + "}")
public String cspPattern;
@Value("${adapter.in.web.csrfProtection.enabled:true}")
private boolean isCsrfProtectionEnabled;
@Value("${server.ssl.enabled:false}")
private boolean isHttpsEnabled;

private static AuthenticationManager createAndSetAuthenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
http.setSharedObject(AuthenticationManager.class, authenticationManager);
http.authenticationManager(authenticationManager);
return authenticationManager;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Http403ForbiddenEntryPoint entryPoint = new Http403ForbiddenEntryPoint();
AuthenticationManager authenticationManager = createAndSetAuthenticationManager(http);

configureCsrf(http);

http.headers((headers) ->
headers.xssProtection(HeadersConfigurer.XXssConfig::disable) // Most modern browsers don't support it
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny) // Block iframes
.httpStrictTransportSecurity(httpStrictTrans ->
httpStrictTrans.includeSubDomains(true)
.maxAgeInSeconds(31_536_000)
)
).httpBasic(basic -> basic
.authenticationEntryPoint(entryPoint)
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public  O postProcess(O authenticationFilter) {
authenticationFilter.setAuthenticationConverter(new JsonBasicAuthenticationConverter());
return authenticationFilter;
}
})
//            .securityContextRepository()
//        ).sessionManagement(session -> session
//                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
//                .addSessionAuthenticationStrategy(new CsrfAuthenticationStrategy())
//                    .addSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy())

//        ).securityContext((securityContext) -> securityContext
//                .requireExplicitSave(true)
//this is default anyway:
//            .securityContextRepository(new DelegatingSecurityContextRepository(
//                new RequestAttributeSecurityContextRepository(),
//                new HttpSessionSecurityContextRepository()
//            ))
).authorizeHttpRequests(requests -> requests
//note: ordering of matchers matters - more specific must come first
.requestMatchers(antMatcher(HttpMethod.POST, LOGIN_ENDPOINT_PATH)).permitAll()
.requestMatchers(antMatcher(HttpMethod.POST, LOGOUT_ENDPOINT_PATH)).permitAll()
.requestMatchers(antMatcher(HttpMethod.GET, CONFIGURATION_PATH)).permitAll()
.requestMatchers("/v1/**/*").authenticated()
.requestMatchers(antMatcher(HttpMethod.GET, "/**")).permitAll()
.anyRequest().authenticated()
).exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(entryPoint)
//        );
).formLogin(customizer -> customizer
.loginProcessingUrl("/v1/login")
.successHandler((request, response, authentication) -> {
response.getOutputStream().println(objectMapper.writeValueAsString(authentication.getAuthorities()));
})
.failureHandler((request, response, authentication) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
})
.permitAll()
).logout(customizer ->  customizer
.logoutUrl(LOGOUT_ENDPOINT_PATH)
.logoutSuccessHandler((request, response, authentication) ->
response.setStatus(HttpStatus.NO_CONTENT.value())
)
).rememberMe(Customizer.withDefaults());

//legit replacement of filters doesn't work, they are overriden at a later stage
//        http.addFilterBefore(new JsonUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter
//        .class);
//        http.addFilter(new JsonUsernamePasswordAuthenticationFilter());

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

DefaultSecurityFilterChain securityFilterChain = http.build();
JsonUsernamePasswordAuthenticationFilter authenticationFilter =
new JsonUsernamePasswordAuthenticationFilter(authenticationManager);
replaceAuthenticationFilter(securityFilterChain, authenticationFilter);
return securityFilterChain;
}

/**
* UsernamePasswordAuthenticationFilter doesn't manage to read credentials from the request.
*/
private static void replaceAuthenticationFilter(DefaultSecurityFilterChain builtChain, JsonUsernamePasswordAuthenticationFilter replacement) {
List filters = builtChain.getFilters();
for (int i = 0; i < filters.size(); i++) {
Filter filter = filters.get(i);
if (filter instanceof UsernamePasswordAuthenticationFilter) {
filters.set(i, replacement);
return;
}
}
}

/**
* This is called before securityFilterChain, so the configuration of security chain already has LDAP manager
* configuration.
*/
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
String url = "(censored)";
String base = "(censored)";
String user = "(censored)";
String password = "(censored)";
LdapContextSource contextSource = new LdapContextSource();
contextSource.setUrl(url);
contextSource.setBase(base);
contextSource.setUserDn(user);
contextSource.setPassword(password);
contextSource.afterPropertiesSet();

DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator(
contextSource,
"");
populator.setGroupSearchFilter("(member={0})");
populator.setGroupRoleAttribute("cn");
populator.setSearchSubtree(true);
populator.setRolePrefix("");

auth
.ldapAuthentication()
.userSearchFilter("distinguishedName={0}")
.ldapAuthoritiesPopulator(populator)
.contextSource(contextSource);

auth
.ldapAuthentication()
.userSearchFilter("cn={0}")
.ldapAuthoritiesPopulator(populator)
.contextSource(contextSource);

auth
.ldapAuthentication()
.userSearchFilter("sAMAccountName={0}")
.ldapAuthoritiesPopulator(populator)
.contextSource(contextSource);

}

private Customizer getCsrfConfigurerCustomizer() {
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setCookieCustomizer(responseCookieBuilder -> responseCookieBuilder
.secure(isHttpsEnabled)
.sameSite("Lax")
.httpOnly(false)
);
return csrf ->  csrf.csrfTokenRepository(cookieCsrfTokenRepository)
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler());
}

private void configureCsrf(HttpSecurity http) throws Exception {
if (!isCsrfProtectionEnabled) {
http.csrf(AbstractHttpConfigurer::disable);
LOG.warn("Protection against CSRF (Cross-Site Request Forgery also known as XSRF) is disabled");
return;
}

http.csrf(getCsrfConfigurerCustomizer())
.addFilterAfter(new EagerCsrfCookieFilter(), BasicAuthenticationFilter.class);
}

}

Поддержка классов, в которых я извлекаю учетные данные из тела JSON, поскольку ни httpBasic (который прокомментирован во фрагменте выше), ни formLogin не могут сделать это «из коробки»:

Код: Выделить всё

package com.visiona.suka.gui.adapter.in.web;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static com.visiona.suka.gui.adapter.in.web.SecurityConfiguration.LOGIN_ENDPOINT_PATH;

public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private final ObjectMapper objectMapper = new ObjectMapper();

public JsonUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(LOGIN_ENDPOINT_PATH,
"POST"));
this.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.getOutputStream().println(objectMapper.writeValueAsString(authentication.getAuthorities()));
});
this.setAuthenticationFailureHandler((request, response, authentication) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
});
}

@Override
protected String obtainUsername(HttpServletRequest request) {
try {
return obtainFieldFromBody(request, this.getUsernameParameter());
} catch (IOException e) {
return null;
}
}

@Override
protected String obtainPassword(HttpServletRequest request) {
try {
return obtainFieldFromBody(request, this.getPasswordParameter());
} catch (IOException e) {
return null;
}
}

private String obtainFieldFromBody(HttpServletRequest request, String fieldName) throws IOException {
TypeReference typeRef
= new TypeReference() {};
HashMap  parsedBody = objectMapper.readValue(request.getReader(), typeRef);
return parsedBody.get(fieldName);
}
}

Код: Выделить всё

package com.visiona.suka.gui.adapter.in.web;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;

public class JsonBasicAuthenticationConverter extends BasicAuthenticationConverter {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {

String username = obtainUsername(request);
String password = obtainPassword(request);

UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken
.unauthenticated(username, password);
result.setDetails(this.getAuthenticationDetailsSource().buildDetails(request));
return result;
}

protected String obtainUsername(HttpServletRequest request) {
try {
return obtainFieldFromBody(request, "username");
} catch (IOException e) {
return null;
}
}

protected String obtainPassword(HttpServletRequest request) {
try {
return obtainFieldFromBody(request, "password");
} catch (IOException e) {
return null;
}
}

private String obtainFieldFromBody(HttpServletRequest request, String fieldName) throws IOException {
TypeReference typeRef
= new TypeReference() {};
HashMap parsedBody = objectMapper.readValue(request.getReader(), typeRef);
return parsedBody.get(fieldName);
}
}
Как видите — бардак. Любые указатели будут высоко оценены. Несмотря на то, что я Full-Stack разработчик, я в основном сосредоточен на интерфейсе, и хотя в последнее время я много узнал о Spring, я все еще очень далек от эксперта - возможно, я верю в какое-то простое заблуждение? Также возможно, что это нежелательное поведение косвенно вызвано некоторыми другими частями моего приложения — я отключил наиболее вероятных виновников, то есть защиту CspFilter и CSRF, но проблема сохраняется — может быть, я мог бы попробовать что-то еще? Еще раз, какие-нибудь подсказки, пожалуйста.


Подробнее здесь: https://stackoverflow.com/questions/793 ... e-page-app
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «JAVA»