Сервер авторизации для микросервисов на Spring Boot

—

от автора

В этой статье рассмотрим, как реализовать аутентификацию с помощью централизованного сервера авторизации (Authorization Server) и API-шлюза (API Gateway).

Что такое JWT-токен и зачем его использовать?

JSON Web Token (JWT) — это стандарт передачи информации с полезной нагрузкой в формате JSON в виде некоторого количества утверждений (claim)  с опциональной подписью и/или шифрованием.

JWT-токены содержат сведения для аутентификации и могут использоваться в нескольких сервисах, инстансах для реализации stateless-аутентификации (без сохранения состояния). При использовании JWT-токенов нет необходимости содержать отдельные ресурсы для пользовательских сессий или хранить токены/сессии в отдельной базе данных/кэше.

Архитектура решения

Отметим основные моменты:

  • Для создания и проверки JWT-токенов используется централизованный сервер авторизации (Authorization Server).

  • API Gateway представляет собой единую точку входа в приложение, которая перенаправляет запросы к соответствующим микросервисам.

  • К маршрутам (routes) добавляется Gateway-фильтр, проверяющий JWT-токены в запросах к защищаемым ресурсам. Для валидации токена и получения имени пользователя с его полномочиями выполняется обращение к серверу авторизации (Authorization Server). Далее полученная информация передается другим сервисам в заголовке запроса.

  • Для Service Discovery (обнаружения сервисов) будем использовать Eureka Discovery Client.

Аутентификация

Аутентификация происходит следующим образом:

  • Пользователь логинится (создает токен аутентификации), вызывая конечную точку /login (POST) с передачей имени пользователя и пароля. В ответ в заголовке он получает Bearer-токен.

  • Токен передается в заголовке в параметре Authorization в формате Bearer access_token.

  • Для запросов к защищенным ресурсам вызывается кастомный Gateway Filter (AuthenticationPrefilter). В фильтре выполняется обращение к конечной точке /api/v1/validateToken сервиса аутентификации (Authentication Service), который валидирует токен и, в случае успешной проверки, отправляет в ответ имя пользователя и его полномочия (authorities).

  • Если токен валидный, то перед переадресацией на ресурс, запрошенный пользователем, к заголовку запроса добавляются имя пользователя и полномочия.

  • В остальных микросервисах (например, user-service) фильтр авторизации, наследуемый от OncePerRequestFilter, создает объект Authentication, используя класс UsernamePasswordAuthenticationToken (с username и SimpleGrantedAuthority из заголовка, с паролем null).

  • Если у пользователя есть полномочия/доступ к ресурсу, то запрос разрешается. В противном случае клиенту возвращается ответ 401 Unathorized / 403 Forbidden.

Пишем сервисы

Eureka Server

  • Создайте приложение Spring Boot, через Spring Initializr с зависимостью spring-cloud-starter-netflix-eureka-server. Также добавьте spring-cloud-dependencies в dependencyManagement.

  • Теперь для запуска Eureka Server достаточно добавить аннотацию @EnableEurekaServer к основному классу приложения.

  • В property-файл добавьте следующие настройки Eureka Server:

spring.application.name=naming-server server.port=8761  eureka.client.register-with-eureka=false eureka.client.fetch-registry=false eureka.instance.prefer-ip-address=true
  • Eureka Server будет доступен по адресу http://localhost:8761/. На главной странице можно увидеть список зарегистрированных сервисов.

Authorization Service (сервис авторизации)

  • Создайте приложения Spring Boot со следующими зависимостями: spring-boot-starter-security, spring-boot-starter-web, spring-cloud-starter-sleuth, spring-cloud-starter-config, spring-cloud-starter-netflix-eureka-client, spring-boot-starter-data-jpa, spring-boot-starter-data-mongodb, spring-boot-starter-data-redis и lombok.

  • Зависимость spring-boot-starter-security необходима для авторизации и аутентификации, spring-boot-starter-data-mongodb и spring-boot-starter-data-jpa — для доступа к учетным данным в MongoDB. Для создания и проверки JWT-токенов будем использовать io.jsonwebtoken:jjwt.

  • Для аутентификации с использованием учетных данных в базе данных напишем свою реализацию UserDetailsService из Spring Security. Для получения учетных данных пользователя из базы данных и создания экземпляра UserDetails необходимо реализовать метод loadUserByUsername().

@Service public class ApplicationUserDetailsService implements UserDetailsService {      @Autowired     private UsersService usersService;       @Override     public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {         return new ApplicationUsers(usersService.getByUsrName(s).orElseThrow(() -> new UsernameNotFoundException("Username Not Found")));     } }
  • Своя реализация UserDetails нам нужна для маппинга объектов, хранящихся в базе данных, на объекты, требуемые Spring Security.

  • Создаем класс конфигурации — наследник WebSecurityConfigurerAdapter.

package com.infotrends.in.authenticationserver.security.config;  import com.infotrends.in.authenticationserver.security.filters.JWTAuthenticationFilter; import com.infotrends.in.authenticationserver.security.filters.JWTVerifierFilter; import com.infotrends.in.authenticationserver.security.services.ApplicationUserDetailsService; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder;  @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {       @Autowired     private PasswordEncoder encoder;      @Autowired     private ApplicationUserDetailsService applicationUserDetailsService;      @Autowired     private TokensRedisService redisService;      @Override     protected void configure(HttpSecurity http) throws Exception {         http.csrf().disable()                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                 .and()                 .addFilter(new JWTAuthenticationFilter(authenticationManager(), redisService))                 .addFilterAfter(new JWTVerifierFilter(redisService), JWTAuthenticationFilter.class)                 .authorizeRequests()                 .antMatchers("/api/v1/validateConnection/whitelisted").permitAll()                 .anyRequest()                 .authenticated()                 .and().httpBasic();     }      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.authenticationProvider(authenticationProvider());     }      @Bean     public DaoAuthenticationProvider authenticationProvider() {         DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();         authenticationProvider.setPasswordEncoder(encoder);         authenticationProvider.setUserDetailsService(applicationUserDetailsService);          return authenticationProvider;     } }
  • Здесь мы создаем бин DaoAuthenticationProvider с кодировщиком паролей и нашей реализацией UserDetailsService.

  • Которые, в свою очередь, используются для переопределения configure(AuthenticationManagerBuilder auth), настраивающего AuthenticationManagerBuilder для использования созданного нами Authentication Provider.

  • Далее для использования JWT/Bearer-токенов вместо аутентификации по логину и паролю, надо настроить два фильтра: один для генерации Bearer-токена, а другой — для его проверки.

  • Фильтр генерации JWT создаем как подкласс UsernamePasswordAuthenticationFilter.  Для проверки учетных данных переопределяем метод attemptAuthentication(). Для создания JWT-токена при успешной аутентификации — метод и successAuthentication().

package com.infotrends.in.authenticationserver.security.filters;  import com.fasterxml.jackson.databind.ObjectMapper; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.authenticationserver.model.ConnValidationResponse; import com.infotrends.in.authenticationserver.model.JwtAuthenticationModel; import com.infotrends.in.authenticationserver.model.redis.TokensEntity; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import com.infotrends.in.authenticationserver.utils.Utilities; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Date;  @Slf4j @RequiredArgsConstructor public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {      private final AuthenticationManager authenticationManager;     private ObjectMapper mapper=new ObjectMapper();      private final TokensRedisService tokensRedisService;      @Override     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {         try {             JwtAuthenticationModel authModel = mapper.readValue(request.getInputStream(), JwtAuthenticationModel.class);             Authentication authentication = new UsernamePasswordAuthenticationToken(authModel.getUsername(), authModel.getPassword());             return authenticationManager.authenticate(authentication);          } catch (IOException e) {             throw new RuntimeException(e);         }     }      @Override     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {         String token = Jwts.builder()                 .setSubject(authResult.getName())                 .claim("authorities", authResult.getAuthorities())                 .claim("principal", authResult.getPrincipal())                 .setIssuedAt(new Date())                 .setIssuer(SecurityConstants.ISSUER)                 .setExpiration(Date.from(LocalDateTime.now().plusMinutes(30).toInstant(ZoneOffset.UTC)))                 .signWith(SignatureAlgorithm.HS256, SecurityConstants.KEY)                 .compact();          log.info(token);         TokensEntity tokensEntity = TokensEntity.builder().id(Utilities.generateUuid()).authenticationToken(token)                         .username(authResult.getName())                         .createdBy("SYSTEM").createdOn(LocalDateTime.now())                         .modifiedBy("SYSTEM").modifiedOn(LocalDateTime.now())                         .build();         tokensEntity = tokensRedisService.save(tokensEntity);         response.addHeader(SecurityConstants.HEADER, String.format("Bearer %s", tokensEntity.getId())); //        response.addHeader("Expiration", String.valueOf(30*60));          ConnValidationResponse respModel = ConnValidationResponse.builder().isAuthenticated(true).build();         response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);         response.getOutputStream().write(mapper.writeValueAsBytes(respModel));     } }
  • Фильтр проверки JWT наследуем от OncePerRequestFilter и настраиваем его вызов после фильтра, генерирующего JWT, с помощью addFilterAfter() в классе конфигурации WebSecurityConfig.

package com.infotrends.in.authenticationserver.security.filters;  import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.authenticationserver.model.redis.TokensEntity; import com.infotrends.in.authenticationserver.services.redis.TokensRedisService; import com.infotrends.in.authenticationserver.utils.Utilities; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import org.apache.tomcat.util.http.parser.Authorization; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; 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.web.filter.OncePerRequestFilter;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors;  @RequiredArgsConstructor public class JWTVerifierFilter extends OncePerRequestFilter {      private final TokensRedisService tokensRedisService;      @Override     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {         String bearerToken = httpServletRequest.getHeader(SecurityConstants.HEADER);         if(!(Utilities.validString(bearerToken) && bearerToken.startsWith(SecurityConstants.PREFIX))) {             filterChain.doFilter(httpServletRequest, httpServletResponse);             return;         }          String authToken = bearerToken.replace(SecurityConstants.PREFIX, "");          Optional<TokensEntity> tokensEntity = tokensRedisService.findById(authToken);          if(!tokensEntity.isPresent()) {             filterChain.doFilter(httpServletRequest, httpServletResponse);             return;         }          String token = tokensEntity.get().getAuthenticationToken();         Jws<Claims> authClaim = Jwts.parser().setSigningKey(SecurityConstants.KEY)                 .requireIssuer(SecurityConstants.ISSUER)                 .parseClaimsJws(token);          String username = authClaim.getBody().getSubject();          List<Map<String, String>> authorities = (List<Map<String, String>>) authClaim.getBody().get("authorities");         List<GrantedAuthority> grantedAuthorities = authorities.stream().map(map -> new SimpleGrantedAuthority(map.get("authority")))                 .collect(Collectors.toList());         Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, grantedAuthorities);         SecurityContextHolder.getContext().setAuthentication(authentication);          httpServletRequest.setAttribute("username", username);         httpServletRequest.setAttribute("authorities", grantedAuthorities);          filterChain.doFilter(httpServletRequest, httpServletResponse);      } }
package com.infotrends.in.authenticationserver.resources;  import com.sun.security.auth.UserPrincipal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.GrantedAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;  import javax.servlet.http.HttpServletRequest; import java.util.List;  @RestController @RequestMapping("/api/v1/validateToken") public class ConnectionValidatorResource {      @GetMapping(value = "", produces = {MediaType.APPLICATION_JSON_VALUE})     public ResponseEntity<ConnValidationResponse> validateGet(HttpServletRequest request) {         String username = (String) request.getAttribute("username");         List<GrantedAuthority> grantedAuthorities = (List<GrantedAuthority>) request.getAttribute("authorities");         return ResponseEntity.ok(ConnValidationResponse.builder().status("OK").methodType(HttpMethod.GET.name())                         .username(username).authorities(grantedAuthorities)                 .isAuthenticated(true).build());     }          @Getter     @Builder     @ToString     public class ConnValidationResponse {         private String status;         private boolean isAuthenticated;         private String methodType;         private String username;         private List<GrantedAuthority> authorities;     }      }

Дополнительная функциональность

Как правило, содержимое JWT-токена нельзя изменить после его создания. Однако токен можно легко расшифровать и прочитать.

Но мы можем вместо отправки пользователю JWT-токена, содержащего все данные, вернуть только случайный UUID, сгенерированный для данного запроса аутентификации, а необходимые данные сохранить в кэше Redis.

Таким образом, клиентскому приложению будет возвращен только сгенерированный UUID, а JWT-токен будет использоваться другими сервисами для авторизации/аутентификации.

package com.infotrends.in.authenticationserver.model.redis;  import lombok.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisHash;  import java.time.LocalDateTime;  @RedisHash(value = "Tokens", timeToLive = 86400) @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class TokensEntity {       private String id;      private String username;     private String authenticationToken;     private String modifiedBy;     private LocalDateTime modifiedOn;     private String createdBy;     private LocalDateTime createdOn; }

API Gateway

  • Создайте приложение Spring Boot со следующими зависимостями, необходимыми для API Gateway с Eureka Client: spring-cloud-starter-gateway, spring-cloud-starter-config и spring-cloud-starter-netflix-eureka-client.

  • Файл настроек Cloud Config Server и Eureka Server:

debug: true logging:   level:     org.springframework.cloud.gateway: DEBUG     reactor.netty.http.client: DEBUG server:   port: '8765' spring:   cloud:     config:       profile: dev     gateway:       discovery.locator.enabled: true   config:     import: optional:configserver:http://clouduser:configserver705!@localhost:8888   application:     name: api-gateway   jackson:     date-format: yyyy-MM-dd HH:mm:ss management:   endpoints:     web:       exposure:         include: '*' eureka:   client:     serviceUrl:       defaultZone: http://eurekauser:eureka124!@localhost:8761/eureka   instance:     prefer-ip-address: 'true'
  • Далее добавляем к классу ApiGatewayApplication аннотацию @EnableFeignClients для написания запросов к Eureka Server.

  • И настраиваем Gateway Filter, который проверяет Bearer-токен в запросах, используя конечную точку /validateToken на сервере авторизации. Для этого наследуемся от класса AbstractGatewayFilterFactory, предоставленного Spring-API Gateway, и переопределяем метод apply(Config config), который возвращает реализацию GatewayFilter.

package com.infotrends.in.InfoTrendsIn.ApiGateway.filters;  import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.infotrends.in.InfoTrendsIn.ApiGateway.model.Authorities; import com.infotrends.in.InfoTrendsIn.ApiGateway.model.ConnValidationResponse; import com.infotrends.in.InfoTrendsIn.ApiGateway.utils.Utilities; import com.infotrends.in.InfoTrendsIn.exceptions.model.ExceptionResponseModel; import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;  import java.util.Date; import java.util.List; import java.util.function.Predicate;  @Component @Slf4j public class AuthenticationPrefilter extends AbstractGatewayFilterFactory<AuthenticationPrefilter.Config> {      @Autowired     @Qualifier("excludedUrls")     List<String> excludedUrls;     private final WebClient.Builder webClientBuilder;      public AuthenticationPrefilter(WebClient.Builder webClientBuilder) {         super(Config.class);         this.webClientBuilder=webClientBuilder;     }      @Autowired     private ObjectMapper objectMapper;      @Override     public GatewayFilter apply(Config config) {         return (exchange, chain) -> {             ServerHttpRequest request = exchange.getRequest();             log.info("**************************************************************************");             log.info("URL is - " + request.getURI().getPath());             String bearerToken = request.getHeaders().getFirst(SecurityConstants.HEADER);             log.info("Bearer Token: "+ bearerToken);              if(isSecured.test(request)) {                 return webClientBuilder.build().get()                         .uri("lb://authentication-service/api/v1/validateToken")                         .header(SecurityConstants.HEADER, bearerToken)                         .retrieve().bodyToMono(ConnValidationResponse.class)                         .map(response -> {                             exchange.getRequest().mutate().header("username", response.getUsername());                             exchange.getRequest().mutate().header("authorities", response.getAuthorities().stream().map(Authorities::getAuthority).reduce("", (a, b) -> a + "," + b));                              return exchange;                         }).flatMap(chain::filter).onErrorResume(error -> {                             log.info("Error Happened");                             HttpStatus errorCode = null;                             String errorMsg = "";                             if (error instanceof WebClientResponseException) {                                 WebClientResponseException webCLientException = (WebClientResponseException) error;                                 errorCode = webCLientException.getStatusCode();                                 errorMsg = webCLientException.getStatusText();                              } else {                                 errorCode = HttpStatus.BAD_GATEWAY;                                 errorMsg = HttpStatus.BAD_GATEWAY.getReasonPhrase();                             } //                            AuthorizationFilter.AUTH_FAILED_CODE                             return onError(exchange, String.valueOf(errorCode.value()) ,errorMsg, "JWT Authentication Failed", errorCode);                         });             }              return chain.filter(exchange);         };     }      public Predicate<ServerHttpRequest> isSecured = request -> excludedUrls.stream().noneMatch(uri -> request.getURI().getPath().contains(uri));     private Mono<Void> onError(ServerWebExchange exchange, String errCode, String err, String errDetails, HttpStatus httpStatus) {         DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory(); //        ObjectMapper objMapper = new ObjectMapper();         ServerHttpResponse response = exchange.getResponse();         response.setStatusCode(httpStatus);         try {             response.getHeaders().add("Content-Type", "application/json");             ExceptionResponseModel data = new ExceptionResponseModel(errCode, err, errDetails, null, new Date());             byte[] byteData = objectMapper.writeValueAsBytes(data);             return response.writeWith(Mono.just(byteData).map(t -> dataBufferFactory.wrap(t)));          } catch (JsonProcessingException e) {             e.printStackTrace();          }         return response.setComplete();     }      @NoArgsConstructor     public static class Config {       } }

Маршруты

В отдельной конфигурации настраиваем маршруты (routes) на использование созданного выше GatewayFilter.

package com.infotrends.in.InfoTrendsIn.ApiGateway.config;  import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.infotrends.in.InfoTrendsIn.ApiGateway.filters.AuthenticationPrefilter; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;  import java.text.SimpleDateFormat; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;  @Configuration public class RouteConfiguration {      @Bean     public RouteLocator routes(             RouteLocatorBuilder builder,             AuthenticationPrefilter authFilter) {         return builder.routes()                 .route("auth-service-route", r -> r.path("/authentication-service/**")                         .filters(f ->                                 f.rewritePath("/authentication-service(?<segment>/?.*)", "$\\{segment}")                                         .filter(authFilter.apply(                                                 new AuthenticationPrefilter.Config())))                         .uri("lb://authentication-service"))                 .route("user-service-route", r -> r.path("/user-service/**")                         .filters(f ->                                 f.rewritePath("/user-service(?<segment>/?.*)", "$\\{segment}")                                         .filter(authFilter.apply(                                                 new AuthenticationPrefilter.Config())))                         .uri("lb://user-service"))                 .build();     }  }

User-Service

  • Создайте проект User-Service с помощью Spring Initializr. В этом сервисе будут храниться пользователи. Это будет пример бэкенда.

  • Создайте класс конфигурации, наследуя класс WebSecurityConfigurerAdapter, и переопределите метод void configure(HttpSecurity http). Здесь мы настроим запуск нашего фильтра проверки токена (JWTVerifierFilter) перед UsernamePasswordAuthenticationFilter.

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter{      @Autowired     private PasswordEncoder encoder;      @Value("${security.users.username}")     private String username;      @Value("${security.users.password}")     private String password;      @Autowired     private AppUserDetailsService appUserDetailsService;       @Override     protected void configure(HttpSecurity http)       throws Exception {         http.csrf().disable()                 .headers().frameOptions().disable()                 .and()                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                 .and()                 .addFilterBefore(new JWTVerifierFilter(), UsernamePasswordAuthenticationFilter.class)                 .authorizeRequests()                 .antMatchers(HttpMethod.GET, "/api/v1/users").permitAll()                 .anyRequest()                 .authenticated()                 .and().httpBasic();              } }
  • JWTVerifierFilter проверяет наличие в заголовке запроса данных об имени пользователя и полномочиях, и создает объект Authentication, используя класс UsernamePasswordAuthenticationToken.

  • Далее, используя SecurityContextHolder, сохраняет authentication в контекст безопасности Spring Security.

package com.infotrends.in.InfoTrendsIn.config.security.filters;  import com.infotrends.in.InfoTrendsIn.security.SecurityConstants; import com.infotrends.in.InfoTrendsIn.utils.Utilities; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter;  import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.*; import java.util.stream.Collectors;  public class JWTVerifierFilter extends OncePerRequestFilter {       @Override     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {         String authHeader = httpServletRequest.getHeader("Authorization");         if(!Utilities.validString(authHeader) || !authHeader.startsWith("Bearer ")) {             filterChain.doFilter(httpServletRequest, httpServletResponse);             return;         }          logHeaders(httpServletRequest);         String username=httpServletRequest.getHeader("username");         List<Map<String, String>> authorities = new ArrayList<>();         String authoritiesStr = httpServletRequest.getHeader("authorities");         Set<SimpleGrantedAuthority> simpleGrantedAuthorities = new HashSet<>();         if(Utilities.validString(authoritiesStr)) {             simpleGrantedAuthorities=Arrays.stream(authoritiesStr.split(",")).distinct()                     .filter(Utilities::validString).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());;         }         Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities);         SecurityContextHolder.getContext().setAuthentication(authentication);          filterChain.doFilter(httpServletRequest, httpServletResponse);      }      private void logHeaders(HttpServletRequest httpServletRequest) {         Enumeration<String> headerNames = httpServletRequest.getHeaderNames();         while(headerNames.hasMoreElements()) {             String header=headerNames.nextElement();             logger.info(String.format("Header: %s --- Value: %s", header, httpServletRequest.getHeader(header)));          }     } }

Таким образом, запрос к ресурсу / конечной точке разрешается, если запрашивающий их пользователь имеет соответствующий доступ/полномочия (authority). Ниже приведен пример для GET-запроса.

@PreAuthorize("hasAnyAuthority('USER_READ', 'USER')") @GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity<EntityModel<UsersResponseModel>> getUserById(@PathVariable("id") String id) {     UsersResponseModel respModel = new UsersResponseModel();     Optional<Users> user = usersSvc.findById(id);     if(!user.isPresent()) {     throw new UserExceptions.UserNotFoudException(ErrorsMappings.USER_NOT_FOUND_MESSAGE);     }     respModel.setUser(user.get());     respModel.setCode(HttpStatus.OK.value());      EntityModel<UsersResponseModel> entity = EntityModel.of(respModel);     entity = usersProcess.generateHateoas(entity, this, "view-user", user.get().getId());     return new ResponseEntity(entity, HttpStatus.OK); }

Используемые зависимости:

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>     <groupId>org.projectlombok</groupId>     <artifactId>lombok</artifactId>     <optional>true</optional> </dependency> <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt</artifactId>     <version>0.9.1</version> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>

Для использования Config Server и Eureka Client также добавим следующие зависимости в раздел dependencyManagement.

<properties>    <spring-cloud.version>2020.0.3</spring-cloud.version> </properties> <dependencyManagement>    <dependencies>       <dependency>          <groupId>org.springframework.cloud</groupId>          <artifactId>spring-cloud-dependencies</artifactId>          <version>${spring-cloud.version}</version>          <type>pom</type>          <scope>import</scope>       </dependency>    </dependencies> </dependencyManagement>

Использование JWT-токена

Проверка запросов к микросервисам

  • API для аутентификации пользователя и генерации JWT-токена:

  • API validateToken, используемый для валидации токена, отправляемого в запросе, и получения сведений об авторизации. (Этот API позже будет заблокирован от внешнего доступа).

  • Запрос к защищенному ресурсу в User-Service при вызове через API Gateway с валидным токеном авторизации:

  • Запрос к защищенному ресурсу в User-Service при вызове через API Gateway без валидного токена:

  • Пример JWT-токена, созданный сервисом авторизации:

Полный исходный код проекта вы можете найти по адресу https://github.com/Vicky-cmd/Authentication-Service.git


Сегодня в 20:00 состоится открытое занятие «Структура программы на Java. Примитивные типы». На этом открытом уроке вы сможете познакомиться с основными этапами создания простейшей программы на Java, понять принципы работы компилятора и виртуальной машины, а также разобраться с class-файлами. На уроке мы изучим примитивные типы данных, константы и enum. Регистрация доступна по ссылке для всех желающих.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/681448/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *