Как стать автором
Обновить

Разработка приложения с oauth2Login авторизацией пользователя: Java 17 + SpringBoot 3.4 + Keycloak 26

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров5.4K

Решала на проекте задачу по настройке флоу auth2 для Java приложения с использованием Keycloak в качестве сервера авторизации.
Вроде бы информации много на разных ресурсах и документация есть, но встречаюсь с такими нюансами: версии Keycloak-а меняются так, что быстро устаревают распространенные примеры, никто уже не использует их адаптер, на который также масса ссылок и примеров; меняются версии Spring и их примеры тоже быстро устаревают и прошлые варианты реализации всё равно надо по новому переписывать в новых версиях. Плюс для меня это был новый опыт работы с auth2, потому, конечно, множество источников пришлось перечитать в поисках информации, причем вычленить ещё актуальную для версий проекта из всего многообразия.
Потому решила написать небольшую инструкцию как в общем я реализовывала эту задачу, с какими сложностями столкнулась и как решала.

Версии на момент разработки:
Java Coretto 17,
SpringBoot 3.4.1,
Keycloak 26.0.7.


Репозиторий:https://github.com/ElenaSpb/keycloak-example

Немного из теории для понимания.
Существует два типа Spring Boot приложений-клиентов, с разными спринговыми стартерами:

  • oauth2Login - Клиент с сохранением состояния.
    Он основан на сессиях, поэтому ему требуется защита от CSRF.
    Основные варианты использования oauth2Login — это приложения, UI которых рендерится на стороне сервера и Spring Cloud Gateway, используемый в качестве бэкенда OAuth2 для фронтенда. Но из-за их состояния приходится использовать что-то вроде Spring Session или smart proxy при горизонтальном масштабировании для достижения высокой доступности и балансировки нагрузки.

  • oauth2ResourceServer - Клиент без сохранения состояния.
    Он основан на токенах Bearer, не имеет состояния, поэтому ему не нужна защита от CSRF.
    Приложения без сохранения состояния легко масштабируются. Для этого требуется, чтобы то, что отправляет (или маршрутизирует) запросы к REST API, могло извлекать токены с сервера авторизации, сохранять их в каком-либо состоянии (сеанс или что-то еще), обновлять их по истечении срока действия и авторизовать запросы с помощью токена доступа. Многие клиенты REST могут это делать (программные, такие как RestClient и WebClient от Spring, или те, у которых есть пользовательский интерфейс, такой как Postman), но браузеры не могут этого делать без помощи фреймворка, такого как Angular, React или Vue (и это теперь не рекомендуется по соображениям безопасности).

    Важно, что oauth2Login и oauth2ResourceServer служат разным целям. Spring Security предоставляет разные стартеры Spring Boot для каждого из них, поскольку они не должны находиться в одном и том же bean-компоненте Security(Web)FilterChain. Authorities mapping и тип аутентификации в security context различаются в
    oauth2Login (uses OAuth2AuthenticationToken ),
    oauth2ResourceServer with JWT decoder (uses JwtAuthenticationToken ),
    oauth2ResourceServer with introspection (uses BearerTokenAuthentication).


    1. Keycloak и его настройка для локальной разработки

    Keycloak - один из самых популярных сервисов идентификации, а также управления доступом с открытым исходным кодом. Это гибкое, эффективное решение для создания защищенных информационных систем.

В Keycloak существует понятие Realm. Это изолированное пространство, в котором хранятся пользователи и клиенты. При использовании таких областей, один экземпляр системы может независимо управлять набором приложений нескольких организаций.

Keycloak дает полноценную поддержку технологии  единого входа (Single Sign-On), обеспечивает безопасный, удобный способ входа пользователей в систему. С помощью SSO пользователи могут войти в систему один раз и автоматически получить доступ ко всем приложениям, интегрированным с сервисом, без повторной аутентификации.
Authorization Code Flow — используется с серверными приложениями (server-side applications). Один из наиболее распространенных типов разрешения на авторизацию, поскольку он хорошо подходит для серверных приложений, в которых исходный код приложения и данные клиента недоступны посторонним. Процесс в данном случае строится на перенаправлении (redirection). Приложение должно быть в состоянии взаимодействовать с пользовательским агентом (user-agent), таким как веб-браузер — получать коды авторизации API перенаправляемые через пользовательский агент.

Типовые сценарии авторизации с использование OAuth2 в Keycloak хорошо описаны здесь: https://habr.com/ru/companies/X5Tech/articles/486778/

В нашем приложении используем Authorization Code Flow.
Этот флоу используется с серверными приложениями (server-side applications). Один из наиболее распространенных типов разрешения на авторизацию, поскольку он хорошо подходит для серверных приложений, в которых исходный код приложения и данные клиента недоступны посторонним. Процесс в данном случае строится на перенаправлении (redirection). Приложение должно быть в состоянии взаимодействовать с пользовательским агентом (user-agent), таким как веб-браузер — получать коды авторизации API перенаправляемые через пользовательский агент.

Настройка Keycloak:

1.1 Cкачиваем последнюю версию, запускаем.
У меня он скачен в c:\distr\keycloak, перехожу там в папку \bin и запускаю сервер Keycloak командой kc.bat start-dev --http-port 8085 . На порту 8085 в dev профиле запустился.
1.2 При первом запуске он просит создать пользователя temporary admin user, админа сервера то есть, задав логин и пароль, я для примера создаю lenas / lenas.
Вот логи:

Логи сервера Keycloak при первом запуске и создании temporary admin user.
Логи сервера Keycloak при первом запуске и создании temporary admin user.

1.3 Создаю realm lenas-realm:


1.4 Создаю клиента lenas-client:

Создание клиента 1 шаг
Создание клиента 1 шаг
Создание клиента 2 шаг
Создание клиента 2 шаг
Создание клиента 3 шаг
Создание клиента 3 шаг

1.5 Создание пользователя, которым будем логиниться из приложения

Создание пользователя 1 шаг
Создание пользователя 1 шаг
Создание пользователя: задание пароля
Создание пользователя: задание пароля


2. Разработка Java приложения

Spring Security oauth2Login настраивает для нас из коробки authorization-code and refresh-token flows .
Он также настраивает авторизованный клиентский репозиторий для хранения токенов (по умолчанию в сессии) и authorized client manager для доступа к нему — обновление токенов по мере необходимости перед их возвратом.
Запросы к клиенту Spring с oauth2Login авторизуются с помощью сеансового cookie.
Вот почему защита от атак CSRF всегда должна быть включена в bean-компоненте Security(Web)FilterChain с oauth2Login.
Тип аутентификации в контексте безопасности после успешной авторизации запроса — OAuth2AuthenticationToken.
Если поставщик автоматически настроен с использованием OpenID, принципал OAuth2AuthenticationToken — это OidcUser, созданный из токена ID, в противном случае требуется дополнительный вызов конечной точки userinfo для установки OAuth2User в качестве принципала.
Spring Security authorities сопоставляются с помощью GrantedAuthoritiesMapper или пользовательского OAuth2UserService.

2.1 можно возпользоваться Spring Initializer https://start.spring.io/ для создания скелета:

2.2 Прописываем в application.properties нашу конфигурацию из п.1

spring.application.name=lenas-client 
server.port=8081  

## keycloak 
spring.security.oauth2.client.provider.lenas-realm.issuer-uri=http://localhost:8085/realms/lenas-realm  
spring.security.oauth2.client.registration.lenas-realm.provider=lenas-realm 
spring.security.oauth2.client.registration.lenas-realm.client-name=lenas-client-name 
spring.security.oauth2.client.registration.lenas-realm.client-id=lenas-client-id 
spring.security.oauth2.client.registration.lenas-realm.client-secret=veS8yqDACC5SDOTwfyX68pnJa81aI2ol 
spring.security.oauth2.client.registration.lenas-realm.scope=openid,offline_access,profile 
spring.security.oauth2.client.registration.lenas-realm.authorization-grant-type=authorization_code

https://github.com/ElenaSpb/keycloak-example/blob/main/src/main/resources/application.properties

client-secret берем отсюда:

2.3 Прописываем Spring Configuration
https://github.com/ElenaSpb/keycloak-example/blob/main/src/main/java/com/example/keycloak_app/SecurityConfiguration.java
(он пока взят из текущего проекта, на котором версия спринга ниже, пятая, потому многое там подчеркнуто как deprecated, надо на шестой версии как-то все по умному переписать, но пока руки не дошли, не нашла нормальных примеров).

2.4 Пишем простой Контроллер
https://github.com/ElenaSpb/keycloak-example/blob/main/src/main/java/com/example/keycloak_app/controller/IndexController.java

@RestController
public class IndexController {    
 
@GetMapping(path = "/")
    public HashMap index() {
        OAuth2User user = ((OAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        return new HashMap() {{
            put("hello", user.getAttribute("name"));
            put("your email is", user.getAttribute("email"));
        }};
    }    
 
@GetMapping(path = "/unauthenticated")
    public String unauthenticatedRequests() {
        return "this is unauthenticated endpoint";
    }    
 
@GetMapping(path = "/cats")
    public List<String> getCats() {
        return List.of("cat1", "cat2");
    }
}

2.5 Запускаем приложение и проверяем
2.5.1 http://localhost:8081/unauthenticated - доступен без авторизации, как и ожидалось.
2.5.2 http://localhost:8081 - автоматически переводит на страницу keycloak для логина, лигинимся пользоватлем, коотрый создали в KeyCloak:

Логинимся созданным пользователем
Логинимся созданным пользователем
Получаем ответ от сервера, после успешного логина
Получаем ответ от сервера, после успешного логина

Супер, всё взлетело, работает!
Что мы сделали для этого? - Добавили несколько строчек кода, всё остальное получаем из коробки Spring Security.

2.5.3 Проверяем открытую сессию пользователя в keycloak сервере:

Сессии пользователей в Keycloak
Сессии пользователей в Keycloak


2.6 Проверяем logout напоследок:

Logout из коробки
Logout из коробки



3. Добавляем роли в KeyCloak и проверяем их наличие в приложении для проверки доступа. ( https://github.com/ElenaSpb/keycloak-example/pull/2 )

При загрузке пользователя в контекст необходимо вычитать из AccessToken роли с KeyCloak и их смапить на допустим свои кастомные в приложении и положить их рядышком в пользователя, чтобы потом из любого места в приложении можно было понять права текущего залогиненого пользователя.

package com.example.keycloak_app.service;import com.example.keycloak_app.auth.LenasOidcUser;
import com.nimbusds.jwt.JWTParser;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;@Service
public class LenasUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {    
  
  private Map<String, Set<String>> rolePermissionMap =
            Map.of("role1", Set.of("permission1"),
                    "lenasRole", Set.of("lenasPermission1", "lenasPermission2"),
                    "lenasGroupRole", Set.of("lenasPermission1", "lenasPermission3")
            );    

  @Override
    public OidcUser loadUser(OidcUserRequest oidcUserRequest) throws OAuth2AuthenticationException {
        OidcIdToken oidcIdToken = oidcUserRequest.getIdToken();
        String email = oidcIdToken.getEmail();

        // create user in application repo if it is absent
        OAuth2AccessToken accessToken = oidcUserRequest.getAccessToken();
        List<String> roles = getRoles(accessToken);
        List<GrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        Set<String> permissions = getAllPermissions(roles);
        return new LenasOidcUser(oidcUserRequest.getIdToken(), authorities, permissions);
    }    

  private List<String> getRoles(OAuth2AccessToken token) {
        try {
            var realmAccess = (Map) JWTParser.parse(token.getTokenValue())
                    .getJWTClaimsSet()
                    .getClaim("realm_access");
            ArrayList<String> rolesNode = (ArrayList<String>) realmAccess.get("roles");
            return rolesNode.stream().map(Object::toString).collect(Collectors.toList());
        } catch (ParseException e) {
            throw new AuthenticationServiceException("Error while obtaining user keycloak roles.");
        }
    }    

  Set<String> getAllPermissions(List<String> roles) {
        // some business logic getting permissions for roles from DB
        return roles.stream()
                .filter(role -> rolePermissionMap.containsKey(role))
                .map(role -> rolePermissionMap.get(role))
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
    }
}



4. Решаем вопрос от frontend с проблемой cross-domain redirect при использовании AJAX. ( https://github.com/ElenaSpb/keycloak-example/pull/1 )
Проблема в том, что Spring под капотом использует несколько редиректов с изменением базы урла, что называется cross-domain redirect problem сейчас в нашем случае это

http://localhost:8081 ->
http://localhost:8081/oauth2/authorization/lenas-realm ->
http://localhost:8085/realms/lenas-realm/protocol/openid-connect/auth?response_type=code&client_id=lenas-client-id&scope=openid offline_access profile&state=bYkwfiPzHEfjIbnJo75TC7Ea0Una9oJqG-fdYLX_9Uw%3D&redirect_uri=http://localhost:8081/login/oauth2/code/lenas-realm&nonce=nH6OBl3zsjzV6-aFK5csdzr8KfWCsac81hGVe44-Fy8

Причем, если KeyCloak отдельно поднятый сервер, а это так обычно и бывает, например, вместо http://localhost:8085 будет http://keycloak.lenas.com, а смена домена невозможна в нашем случае / небезопасна в общем случае.

Решение: переписываем в этом логине респонс: меняем статус (потому как в тело респонса с 302 боди не вычитывается вообще) и в body складываем url, который берут на строне фронта уже далее и делают переход на этот url своими силами уже.

package com.example.keycloak_app.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import java.io.IOException;

public class LenasDefaultRedirectStrategy implements RedirectStrategy {    

    protected final Log logger = LogFactory.getLog(this.getClass());
    private boolean contextRelative;    
  
    @Override
    public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
        String redirectUrl = this.calculateRedirectUrl(request.getContextPath(), url);
        redirectUrl = response.encodeRedirectURL(redirectUrl);
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.format("Redirecting to %s", redirectUrl));
        }
        // for first AUTH redirect on {host}/Intra/oauth2/authorization/keycloak
        // to fix ajax cross-domain FE issue about impossible redirect by 302 http status
        if (redirectUrl.contains("/oauth2/authorization/lenas-realm")) {
            response.setStatus(401);
            response.getWriter().write(redirectUrl);
            response.flushBuffer();
        } else {
            // for all other redirections should by default
            response.sendRedirect(redirectUrl);
        }
    }    
  
  protected String calculateRedirectUrl(String contextPath, String url) {
        if (!UrlUtils.isAbsoluteUrl(url)) {
            return this.isContextRelative() ? url : contextPath + url;
        } else if (!this.isContextRelative()) {
            return url;
        } else {
            Assert.isTrue(url.contains(contextPath), "The fully qualified URL does not include context path.");
            url = url.substring(url.lastIndexOf("://") + 3);
            url = url.substring(url.indexOf(contextPath) + contextPath.length());
            if (url.length() > 1 && url.charAt(0) == '/') {
                url = url.substring(1);
            }
            return url;
        }
    }    
  
  public void setContextRelative(boolean useRelativeContext) {
        this.contextRelative = useRelativeContext;
    }    
  
  protected boolean isContextRelative() {
        return this.contextRelative;
    }
}


Этот класс переписан вместо спрингового DefaultRedirectStrategy (добавлена только одна ветка проверки, что у нас 1й редирект). Эта стратегия является приватным финальным полем класса LoginUrlAuthenticationEntryPoint, откуда и идет этот 1й редирект.
Оба эти класса причем не являются спринговыми бинами, чтобы их подменить просто.
Зато LoginUrlAuthenticationEntryPoint является полем, наконец уже, бина DelegatingAuthenticationEntryPoint.
Его и будем переписывать. Чтобы изменить это поле, пришлось написать свой BeanPostProcessor с установкой своей кастомной стратегии редиректа в поле поля бина:

package com.example.keycloak_app.auth;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.util.LinkedHashMap;

@Component
public class LenasChangeLoginRedirectStrategyBeanPostProcessor implements BeanPostProcessor {    

/**
      set private final redirect strategy to use custom 401 http status instead of 302
      to solve FE issue about impossible cross-domain redirect with 302 http status.
      due to https://github.com/spring-projects/spring-security/pull/11387
      set redirectStrategy in SpringSecurity can be done after Spring 5.8
      todo: delete this class after update Spring and setting IntraDefaultRedirectStrategy in SecurityConfig
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean.getClass() == DelegatingAuthenticationEntryPoint.class) {
            Field entryPoints = DelegatingAuthenticationEntryPoint.class.getDeclaredField("entryPoints");
            entryPoints.setAccessible(true);
            LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = null;
            map = (LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>) entryPoints.get(bean);
            var loginUrlAuthenticationEntryPoint = map.values().stream().findFirst().get();
            LenasDefaultRedirectStrategy lenasDefaultRedirectStrategy = new LenasDefaultRedirectStrategy();
            setRedirectStrategy(loginUrlAuthenticationEntryPoint, lenasDefaultRedirectStrategy);
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }    
  
  private void setRedirectStrategy(Object object, Object redirectStrategy) throws NoSuchFieldException, IllegalAccessException {
        Field field = object.getClass().getDeclaredField("redirectStrategy");
        field.setAccessible(true);
        field.set(object, redirectStrategy);
    }
}

Итог: теперь при первом редиректе получаем. Из response body берем url, мы для тестирования руками, на фронте эта обработка реализуется в обход ajax redirect, и по нему продолжаем процесс логина:

1й редирект дает вместо 302 теперь 401 статус с url-ом для перехода в теле запроса
1й редирект дает вместо 302 теперь 401 статус с url-ом для перехода в теле запроса

Далее переходим по этому url и продолжаем обычный процесс редиректа:

2й редирект уже на сам логин на сервере Keycloak
2й редирект уже на сам логин на сервере Keycloak


5. Тестирование

5.1 Простой unit тест к контроллеру написан здесь https://github.com/ElenaSpb/keycloak-example/pull/2/files#diff-20f4fc0abfaa4eb09a957a6b736668e3cf17903641c99469929849a8dea0b297

Исходя их концепции легкого, без поднятия спрингового контекста подхода, с подменой контекста исходя из текущих нужд:

@ExtendWith(MockitoExtension.class)
public class ProductControllerUnitTest {

    private MockMvc mvc;

    @Mock
    private ProductService productService;

    @InjectMocks
    private ProductController productController;

    @BeforeEach
    public void setup() {
        // We would need this line if we would not use the MockitoExtension
        // MockitoAnnotations.initMocks(this);
        // Here we can't use @AutoConfigureJsonTesters because there isn't a Spring context
        JacksonTester.initFields(this, new ObjectMapper());
        // MockMvc standalone approach
        mvc = MockMvcBuilders.standaloneSetup(productController).build();
    }

    @Test
    void getAllProducts() throws Exception {
        given(productService.getPublicProducts()).willReturn(List.of("product1", "product2", "product3"));

        MockHttpServletResponse response = mvc
                .perform(get("/products").contentType(MediaType.APPLICATION_JSON))
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo("[\"product1\",\"product2\",\"product3\"]");
    }

    @Test
    void getPrivateProductsForbidden() throws Exception {
        setUpContext("");
        MockHttpServletResponse response = mvc
                .perform(get("/private-products").contentType(MediaType.APPLICATION_JSON))
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.FORBIDDEN.value());
    }

    @Test
    void getPrivateProductsOk() throws Exception {
        setUpContext("lenasPermission1");

        given(productService.getPrivateProducts()).willReturn(List.of("pr1", "pr2", "pr3"));

        MockHttpServletResponse response = mvc
                .perform(get("/private-products").contentType(MediaType.APPLICATION_JSON))
                .andReturn()
                .getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
        assertThat(response.getContentAsString()).isEqualTo("[pr1, pr2, pr3]");
    }

    private void setUpContext(String permissions) {
        var permissionsSet = Set.of(permissions.replaceAll(" ", "").split(","));
        var lenasOidcUser = mock(LenasOidcUser.class);
        when(lenasOidcUser.getAllPermissions()).thenReturn(permissionsSet);

        var authentication = mock(OAuth2LoginAuthenticationToken.class);
        when(authentication.getPrincipal()).thenReturn(lenasOidcUser);

        SecurityContext securityContext = Mockito.mock(SecurityContext.class);
        Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
        SecurityContextHolder.setContext(securityContext);
    }
}



5.2
Для более удобного тестирования я написла свою аннатацию, чтобы сразу в ней можно было бы задавать permissions мокируемого пользователя, который проставляется к security-context, который также мокируется.

package com.example.keycloak_app.auth;import org.springframework.security.test.context.support.WithSecurityContext;import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockLenasUserSecurityContextFactory.class)
public @interface WithMockLenasUser {
    String name() default "Test User";
    String permissions() default "some";
}

Ну и класс фабрики для обработки этой аннотации:

package com.example.keycloak_app.auth;

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
import java.util.Set;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class WithMockLenasUserSecurityContextFactory implements WithSecurityContextFactory<WithMockLenasUser> {
    @Override
    public SecurityContext createSecurityContext(WithMockLenasUser customUser) {
      var permissions = Set.of(customUser.permissions().replaceAll(" ", "").split(","));        

      var lenasOidcUser = mock(LenasOidcUser.class);
      when(lenasOidcUser.getAllPermissions()).thenReturn(permissions);       
      
      var authentication = mock(OAuth2LoginAuthenticationToken.class);
      when(authentication.getPrincipal()).thenReturn(lenasOidcUser);
      when(authentication.isAuthenticated()).thenReturn(true);   
      
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      context.setAuthentication(authentication);
      return context;
    }
}


Далее эту аннотацию @WithMockLenasUser можно просто использовать в тестах контроллеров енд-пойнтов, доступ к которым возможен только залогиненым пользователям и в тестах сервисов, где используется проверка текущего пользоватлея в сессии и возможно его права.

Ура, Конец и всё работает!

Да, решения некоторые не такие идеальные, как хотелось бы, но данный вариант рабочий и это главное для 1й итерации.

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

Ну и проработать еще раз HttpSecurity, такое чувство, что с каждой новой версией Spring приходится переписывать эту часть. (((
Примеры, на которые ориентируемся быстро устаревают от версии к версии.
Как на новом Spring Security 6.4 переписать правильно эту часть пока не нашла..

Если Вы сталкивались с подобными кейсами и придумали/нашли решение получше, пожалуйста, напишите в комментарии, буду благодарна.

Теги:
Хабы:
Рейтинг0
Комментарии11

Публикации

Работа

Java разработчик
197 вакансий

Ближайшие события