Simple Spring (полный фарш)
В прошлом сервере-ресурсов нам пришлось самим проверять ролевую модель и управлять доступом пользователя. Но можно переложить эту задачу на спринг-секьюрити.
Сразу хочу ответить на вопрос: зачем городить огород, писать самостоятельную авторизацию и ограниченно использовать секьюрити, когда можно просто использовать спринг-секьюрити?
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')»)для проверки доступа к апи