Simple Spring (полный фарш)

a68c0a5bbfb8d950ab7ddcf5d0d3f5d9.png

В прошлом сервере-ресурсов нам пришлось самим проверять ролевую модель и управлять доступом пользователя. Но можно переложить эту задачу на спринг-секьюрити.

Сразу хочу ответить на вопрос: зачем городить огород, писать самостоятельную авторизацию и ограниченно использовать секьюрити, когда можно просто использовать спринг-секьюрити?
1. Сприг-секьюрити слишком подвержен изменениям. Постоянно выходят новые версии, старые методы становятся неподдерживаемыми, возникают новые классы и так далее. С моей точки зрения это говорит о том, что данный проект ещё несколько сыроват.
2. Использование сприг-секьюрити накладывает дополнительные архитектурные ограничения. Иногда эти ограничения идут в разрез с планируемой архитектурой.
3. Наконец что если я хочу написать действительно «микро»-сервис, то есть вообще без использования спринга?
В любом случае, я считаю, что используя сторонние инструменты надо как минимум понимать как они работают, а как максимум уметь самому реализовать что-то подобное (сердце того же спринга с его контекстом и фабрикой бинов вполне можно написать самому за пару недель).
На мой взгляд использование спринг-секьюрити полностью оправдано для защиты одиночного приложения, но может быть сомнительно для нескольких взаимодействующих приложений.

Итак. попросив удачи у Ершова приступим

Добавим к зависимостям в build.gradle

application.properties

spring.application.name=WebSecurityApplication1
server.port=8081

#config
spring.profiles.active=authSecret, swagger, WebSecurityApplication, actuator, adminClient
spring.cloud.config.fail-fast=true
spring.cloud.config.uri=http://localhost:8888
spring.config.import=optional:configserver:http://localhost:8888
spring.cloud.config.import-check.enabled=true

Настроим конфигурацию спринг-секьюрити


@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)  //чтобы можно было исп аннотацию Secured на методах, подробнее см https://www.baeldung.com/spring-security-method-security
@RequiredArgsConstructor
public class SecurityConfiguration {
    private final AuthorizationFilter authorizationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                // Своего рода отключение CORS (разрешение запросов со всех доменов)
                .cors(cors -> cors.configurationSource(request -> {
                    var corsConfiguration = new CorsConfiguration();
                    corsConfiguration.setAllowedOriginPatterns(List.of("*"));
                    corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                    corsConfiguration.setAllowedHeaders(List.of("*"));
                    corsConfiguration.setAllowCredentials(true);
                    return corsConfiguration;
                }))
                // Настройка доступа к конечным точкам
                .authorizeHttpRequests(request -> request
                        // Можно указать конкретный путь, * - 1 уровень вложенности, ** - любое количество уровней вложенности
                        /*permitAll - Эндпоинт доступен всем пользователям, и авторизованным и нет
                          authenticated - Только авторизованные пользователи
                          hasRole - Пользователь должен иметь конкретную роль, и, соответственно быть авторизованным
                          hasAnyRole - Должен иметь одну из перечисленных ролей (не представлено в коде)*/
                        .requestMatchers("/actuator/**").permitAll()
                        .requestMatchers("/swagger-ui/**", "/swagger-resources/*", "/api-docs/**").permitAll()
                        .requestMatchers("/app/v1/**").hasRole("WEB")
                        .anyRequest().authenticated())
                .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
                //.authenticationProvider(authenticationProvider())
                .addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

Видим, что здесь мы вручную применяем наш фильтр AuthorizationFilter который изменится соответствующим образом


/**Фильтр проверяющий ацесс-токен для авторизации который надо получать у auth-service если в настройках включена авторизация*/
@Component
@RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {

    private final TokenService tokenService;

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
       /* не нужно проверять так как доступ к разрешённым ресурсам разерешён в конфигурации.
       В данном фильтре проверяем строго токен для доступа к ресурсам требующим авторизации
        if (
                   request.getRequestURI().contains("/public/")
                || request.getRequestURI().contains("/actuator")
                || request.getRequestURI().contains("/swagger-ui")
                || request.getRequestURI().contains("/api-docs")
        ) {
            filterChain.doFilter(request, response);
            return;
        }*/

        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (authHeader==null || authHeader.isEmpty() || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        if (authHeader == null || authHeader.isBlank())
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        else {
            DecodedJWT decodedJWT = checkAuthorization(authHeader);
            if (decodedJWT==null)
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            else {
                //Авторизация прошла успешно
                //Создадим сприговский контекст безопасности
                //получать как SecurityContextHolder.getContext().getAuthentication()
                //Имя авторизованного пользователя как SecurityContextHolder.getContext().getAuthentication().getName()
                List roles = Arrays.stream(decodedJWT.getClaim("roles").asString().split(",")).toList().stream().map(String::trim).map(SimpleGrantedAuthority::new).toList();

                SecurityContext context = SecurityContextHolder.createEmptyContext();
                UsernamePasswordAuthenticationToken authToken =new UsernamePasswordAuthenticationToken(decodedJWT.getSubject(), null, roles);
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                context.setAuthentication(authToken);
                SecurityContextHolder.setContext(context);

                //продолжим обработку цепочки фильтров
                filterChain.doFilter(request, response);
            }
        }
    }

    private DecodedJWT checkAuthorization(String auth) {
        if (!auth.startsWith("Bearer "))
            return null;

        String token = auth.substring(7);
        return tokenService.checkToken(token);
    }
}

Видим что проверка на запросы для которых не надо проверять авторизацию ушла в SecurityConfiguration, а информацию о сеансе пользователя и его ролях мы теперь сохраняем в SecurityContext

Соответственно теперь, имея спринговский контекст безопасности мы можем использовать вместо кастомной проверки ролей спринговские аннотации @Secured ({«ROLE_HELLO»})для проверки доступа к методам сервиса и @PreAuthorize («hasRole ('ADMIN')»)для проверки доступа к апи

© Habrahabr.ru