복붙노트

[SPRING] Spring 용 webflux 사용자 정의 인증

SPRING

Spring 용 webflux 사용자 정의 인증

Angular 5 응용 프로그램 용 API를 만들고 있습니다. JWT를 인증에 사용하고 싶습니다. 스프링 보안이 제공하는 기능을 사용하여 역할로 쉽게 작업 할 수 있습니다.

기본 인증을 사용하지 않도록 관리했습니다. 그러나 http.authorizeExchange ()를 사용할 때 anyExchange (). authenticated (); 나는 여전히 로그인 프롬프트를 얻는다. 프롬프트 대신 403을주고 싶습니다. 따라서 토큰에 대한 인증 헤더를 검사하는 "것"(필터인가?)에 의해 로그인 프롬프트를 무시합니다.

JWT 토큰을 리턴하는 컨트롤러에서 수행하려는 로그인. 하지만 사용자 인증 정보를 확인하는 데 사용해야하는 보안 bean은 무엇입니까? 나 자신의 서비스와 저장소를 구축 할 수 있지만 가능한 한 많이 봄 보안이 제공하는 기능을 사용하고 싶습니다.

이 질문의 짧은 버전은 다음과 같습니다. 스프링 보안의 인증을 사용자 정의하려면 어떻게해야합니까? 어떤 콩을 만들어야합니까? 구성을 어디에 두어야합니까? (이제 SecurityWebFilterChain 빈)

스프링 보안을 사용하여 webflux에서 인증에 관한 유일한 문서는 다음과 같습니다. https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux

해결법

  1. ==============================

    1.많은 검색과 시도 후에 나는 해결책을 찾았다 고 생각합니다 :

    많은 검색과 시도 후에 나는 해결책을 찾았다 고 생각합니다 :

    모든 구성을 포함하는 SecurityWebFilterChain 빈이 필요합니다. 이 내 꺼야:

    @Configuration
    public class SecurityConfiguration {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private SecurityContextRepository securityContextRepository;
    
        @Bean
        public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
            // Disable default security.
            http.httpBasic().disable();
            http.formLogin().disable();
            http.csrf().disable();
            http.logout().disable();
    
            // Add custom security.
            http.authenticationManager(this.authenticationManager);
            http.securityContextRepository(this.securityContextRepository);
    
            // Disable authentication for `/auth/**` routes.
            http.authorizeExchange().pathMatchers("/auth/**").permitAll();
            http.authorizeExchange().anyExchange().authenticated();
    
            return http.build();
        }
    }
    

    내 맞춤 인증을 할 수 있도록 httpBasic, formLogin, csrf 및 logout을 비활성화했습니다.

    AuthenticationManager와 SecurityContextRepository를 설정하여 사용자가 요청에 대해 인증 / 승인되었는지 확인하기 위해 기본 스프링 보안 구성을 재정의했습니다.

    인증 관리자 :

    @Component
    public class AuthenticationManager implements ReactiveAuthenticationManager {
    
        @Override
        public Mono<Authentication> authenticate(Authentication authentication) {
            // JwtAuthenticationToken is my custom token.
            if (authentication instanceof JwtAuthenticationToken) {
                authentication.setAuthenticated(true);
            }
            return Mono.just(authentication);
        }
    }
    

    나는 인증 관리자가 어디에 있는지 확실하지 않지만 최종 인증을하려고 생각하고 있으므로 authentication.setAuthenticated (true); 모든 것이 옳은 때.

    SecurityContextRepository :

    @Component
    public class SecurityContextRepository implements ServerSecurityContextRepository {
    
        @Override
        public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
            // Don't know yet where this is for.
            return null;
        }
    
        @Override
        public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
            // JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
            Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ? 
                new JwtAuthenticationToken(...) :
                new GuestAuthenticationToken();
            return new SecurityContextImpl(authentication);
        }
    }
    

    로드 할 때 사용자가 인증되면 serverWebExchange의 헤더를 기반으로 확인합니다. 나는 https://github.com/jwtk/jjwt를 사용한다. 사용자가 인증을 받았다면 다른 종류의 인증 토큰을 반환합니다.

  2. ==============================

    2.이전 프로젝트에서는이 구성을 사용했습니다.

    이전 프로젝트에서는이 구성을 사용했습니다.

    @Configuration
    @EnableWebSecurity
    @Import(WebMvcConfig.class)
    @PropertySource(value = { "classpath:config.properties" }, encoding = "UTF-8", ignoreResourceNotFound = false)
    public class WebSecWebSecurityCfg extends WebSecurityConfigurerAdapter
    {
        private UserDetailsService userDetailsService;
        @Autowired
        @Qualifier("objectMapper")
        private ObjectMapper mapper;
        @Autowired
        @Qualifier("passwordEncoder")
        private PasswordEncoder passwordEncoder;
        @Autowired
        private Environment env;
    
        public WebSecWebSecurityCfg(UserDetailsService userDetailsService)
        {
            this.userDetailsService = userDetailsService;
        }
    
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception
        {                                                             
            JWTAuthorizationFilter authFilter = new JWTAuthorizationFilter
                                                                        (   authenticationManager(),//Auth mgr  
                                                                            env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
                                                                            env.getProperty("config.jwt.header.string"), //nome header
                                                                            env.getProperty("config.jwt.token.prefix") //Prefisso token
                                                                        );
            JWTAuthenticationFilter authenticationFilter = new JWTAuthenticationFilter
                                                                        (
                                                                            authenticationManager(), //Authentication Manager
                                                                            env.getProperty("config.secret.symmetric.key"), //Chiave simmetrica
                                                                            Long.valueOf(env.getProperty("config.jwt.token.duration")),//Durata del token in millisecondi
                                                                            env.getProperty("config.jwt.header.string"), //nome header
                                                                            env.getProperty("config.jwt.token.prefix"), //Prefisso token
                                                                            mapper
                                                                        );
            http        
            .cors()
            .and()
            .csrf()
            .disable()
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .addFilter(authenticationFilter)
            .addFilter(authFilter)
            // Disabilitiamo la creazione di sessione in spring
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception
        {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
        }
    
        @Bean
        CorsConfigurationSource corsConfigurationSource()
        {
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
            return source;
        }
    }
    

    JWT 인증 필터의 위치 :

    public class JWTAuthorizationFilter extends BasicAuthenticationFilter
    {
        private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class.getName());
        private String secretKey;
        private String headerString;
        private String tokenPrefix; 
    
        public JWTAuthorizationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, String secretKey, String headerString, String tokenPrefix)
        {
            super(authenticationManager, authenticationEntryPoint);
            this.secretKey = secretKey;
            this.headerString = headerString;
            this.tokenPrefix = tokenPrefix;
        }
        public JWTAuthorizationFilter(AuthenticationManager authenticationManager, String secretKey, String headerString, String tokenPrefix)
        {
            super(authenticationManager);
            this.secretKey = secretKey;
            this.headerString = headerString;
            this.tokenPrefix = tokenPrefix;
        }
        @Override
        protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException
        {
            AuthenticationErrorEnum customErrorCode = null;
            StringBuilder builder = new StringBuilder();
            if( failed.getCause() instanceof MissingJwtTokenException )
            {
                customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_MANCANTE;
            }
            else if( failed.getCause() instanceof ExpiredJwtException )
            {
                customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_SCADUTO;
            }
            else if( failed.getCause() instanceof MalformedJwtException )
            {
                customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NON_CORRETTO;
            }
            else if( failed.getCause() instanceof MissingUserSubjectException )
            {
                customErrorCode = AuthenticationErrorEnum.TOKEN_JWT_NESSUN_UTENTE_TROVATO;
            }
            else if( ( failed.getCause() instanceof GenericJwtAuthorizationException ) || ( failed.getCause() instanceof Exception ) )
            {
                customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
            }
            builder.append("Errore duranre l'autorizzazione. ");
            builder.append(failed.getMessage());
            JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
            String errore = ( new ObjectMapper() ).writeValueAsString(apiError);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
            request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
        }
    

    그리고 JWTAuthenticationFilter는

    public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter
    {
        private AuthenticationManager authenticationManager;
        private String secretKey;
        private long tokenDurationMillis;
        private String headerString;
        private String tokenPrefix;
        private ObjectMapper mapper;
    
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException
        {
            AuthenticationErrorEnum customErrorCode = null;
            StringBuilder builder = new StringBuilder();
            if( failed instanceof BadCredentialsException )
            {
                customErrorCode = AuthenticationErrorEnum.CREDENZIALI_SERVIZIO_ERRATE;
            }
    
            else
            {
                //Teoricamente nella fase di autenticazione all'errore generico non dovrebbe mai arrivare
                customErrorCode = AuthenticationErrorEnum.ERRORE_GENERICO;
            }       
            builder.append("Errore durante l'autenticazione del servizio. ");
            builder.append(failed.getMessage());
            JwtAuthApiError apiError = new JwtAuthApiError(HttpStatus.UNAUTHORIZED, failed.getMessage(), Arrays.asList(builder.toString()), customErrorCode);
            String errore = mapper.writeValueAsString(apiError);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.sendError(HttpStatus.UNAUTHORIZED.value(), errore);
            request.setAttribute(IRsConstants.API_ERROR_REQUEST_ATTR_NAME, apiError);
        }
    
        public JWTAuthenticationFilter(AuthenticationManager authenticationManager, String secretKey, long tokenDurationMillis, String headerString, String tokenPrefix, ObjectMapper mapper)
        {
            super();
            this.authenticationManager = authenticationManager;
            this.secretKey = secretKey;
            this.tokenDurationMillis = tokenDurationMillis;
            this.headerString = headerString;
            this.tokenPrefix = tokenPrefix;
            this.mapper = mapper;
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException
        {
            try
            {
                ServiceLoginDto creds = new ObjectMapper().readValue(req.getInputStream(), ServiceLoginDto.class);
    
                return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getCodiceServizio(), creds.getPasswordServizio(), new ArrayList<>()));
            }
            catch (IOException e)
            {
                throw new RuntimeException(e);
            }
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException
        {
            DateTime dt = new DateTime();
            Date expirationTime = dt.plus(getTokenDurationMillis()).toDate();
            String token = Jwts
                            .builder()
                            .setSubject(((User) auth.getPrincipal()).getUsername())
                            .setExpiration(expirationTime)
                            .signWith(SignatureAlgorithm.HS512, getSecretKey().getBytes())
                            .compact();
            res.addHeader(getHeaderString(), getTokenPrefix() + token);
            res.addHeader("jwtExpirationDate", expirationTime.toString());
            res.addHeader("jwtTokenDuration", String.valueOf(TimeUnit.MILLISECONDS.toMinutes(getTokenDurationMillis()))+" minuti");
        }
        public String getSecretKey()
        {
            return secretKey;
        }
    
        public void setSecretKey(String secretKey)
        {
            this.secretKey = secretKey;
        }
    
        public long getTokenDurationMillis()
        {
            return tokenDurationMillis;
        }
    
        public void setTokenDurationMillis(long tokenDurationMillis)
        {
            this.tokenDurationMillis = tokenDurationMillis;
        }
    
        public String getHeaderString()
        {
            return headerString;
        }
    
        public void setHeaderString(String headerString)
        {
            this.headerString = headerString;
        }
    
        public String getTokenPrefix()
        {
            return tokenPrefix;
        }
    
        public void setTokenPrefix(String tokenPrefix)
        {
            this.tokenPrefix = tokenPrefix;
        }
    }
    

    사용자 세부 정보는 고전적인 userservicedetail입니다.

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService
    {
        @Autowired
        private IServizioService service;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
        {
            Service svc;
            try
            {
                svc = service.findBySvcCode(username);
            }
            catch (DbException e)
            {
                throw new UsernameNotFoundException("Errore durante il processo di autenticazione; "+e.getMessage(), e);
            }
            if (svc == null)
            {
                throw new UsernameNotFoundException("Nessun servizio trovato per il codice servizio "+username);
            }
            else if( !svc.getAbilitato().booleanValue() )
            {
                throw new UsernameNotFoundException("Servizio "+username+" non abilitato");
            }
            return new User(svc.getCodiceServizio(), svc.getPasswordServizio(), Collections.emptyList());
        }
    }
    

    Spring webflux를 사용하지 않았다는 점에 유의하십시오.

    유용하게 사용되기를 바랍니다.

    안젤로

  3. ==============================

    3.Spring Webflux 애플리케이션에서 인증을 커스터마이징하고 api에 대한 액세스를 보호하기 위해 예제를 사용하여 많은 도움을 받았습니다. 필자의 경우 사용자 역할을 설정하기 위해 헤더를 읽는 것만으로 스프링 보안이 사용자 인증을 확인하여 내 메소드에 대한 액세스를 보호하기를 원합니다. 사용자 지정 http.securityContextRepository (this.securityContextRepository)를 사용하여 키를 제공했습니다. SecurityConfiguration (사용자 정의 인증 관리자 필요 없음).

    Spring Webflux 애플리케이션에서 인증을 커스터마이징하고 api에 대한 액세스를 보호하기 위해 예제를 사용하여 많은 도움을 받았습니다. 필자의 경우 사용자 역할을 설정하기 위해 헤더를 읽는 것만으로 스프링 보안이 사용자 인증을 확인하여 내 메소드에 대한 액세스를 보호하기를 원합니다. 사용자 지정 http.securityContextRepository (this.securityContextRepository)를 사용하여 키를 제공했습니다. SecurityConfiguration (사용자 정의 인증 관리자 필요 없음).

    이 SecurityContextRepository 덕분에 사용자 정의 인증 (아래 단순화)을 빌드하고 설정할 수있었습니다.

    @Override
    public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
        String role = serverWebExchange.getRequest().getHeaders().getFirst("my-header");
        Authentication authentication =
           new AnonymousAuthenticationToken("authenticated-user", someUser,  AuthorityUtils.createAuthorityList(role) );
    
        return Mono.just(new SecurityContextImpl(authentication));
    }
    

    따라서 이러한 역할을 사용하여 메서드를 보호 할 수 있습니다.

    @Component
    public class MyService {
        @PreAuthorize("hasRole('ADMIN')")
        public Mono<String> checkAdmin() {
            // my secure method
       }
    }
    
  4. ==============================

    4.동일한 문제 (Webflux + 사용자 정의 인증 + JWT)가있는 사용자의 경우, AuthenticationWebFilter, 사용자 정의 ServerAuthenticationConverter 및 ReactiveAuthenticationManager를 사용하여 해결 된 코드가 있으면 나중에 도움이 될 수 있습니다. 최신 버전 (spring-boot 2.1.1.RELEASE)으로 테스트되었습니다.

    동일한 문제 (Webflux + 사용자 정의 인증 + JWT)가있는 사용자의 경우, AuthenticationWebFilter, 사용자 정의 ServerAuthenticationConverter 및 ReactiveAuthenticationManager를 사용하여 해결 된 코드가 있으면 나중에 도움이 될 수 있습니다. 최신 버전 (spring-boot 2.1.1.RELEASE)으로 테스트되었습니다.

    @EnableWebFluxSecurity
    @EnableReactiveMethodSecurity
    public class SpringSecurityConfiguration {
        @Bean
        public SecurityWebFilterChain configure(ServerHttpSecurity http) {
        return http
            .csrf()
                .disable()
                .headers()
                .frameOptions().disable()
                .cache().disable()
            .and()
                .authorizeExchange()
                .pathMatchers(AUTH_WHITELIST).permitAll()
                .anyExchange().authenticated()
            .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                .build();
        }
    
        private AuthenticationWebFilter authenticationWebFilter() {
            AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
            authenticationWebFilter.setServerAuthenticationConverter(new JwtAuthenticationConverter(tokenProvider));
            NegatedServerWebExchangeMatcher negateWhiteList = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST));
            authenticationWebFilter.setRequiresAuthenticationMatcher(negateWhiteList);
            authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
            authenticationWebFilter.setAuthenticationFailureHandler(responseError());
            return authenticationWebFilter;
        }
    }
    
    
    public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
        private final TokenProvider tokenProvider;
    
        public JwtAuthenticationConverter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
        }
    
        private Mono<String> resolveToken(ServerWebExchange exchange) {
        log.debug("servletPath: {}", exchange.getRequest().getPath());
        return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
                .filter(t -> t.startsWith("Bearer "))
                .map(t -> t.substring(7));
        }
    
        @Override
        public Mono<Authentication> convert(ServerWebExchange exchange) {
        return resolveToken(exchange)
                .filter(tokenProvider::validateToken)
                .map(tokenProvider::getAuthentication);
        }
    
    }
    
    
    public class CustomReactiveAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager {
        public CustomReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
        super(userDetailsService);
        }
    
        @Override
        public Mono<Authentication> authenticate(Authentication authentication) {
        if (authentication.isAuthenticated()) {
            return Mono.just(authentication);
        }
        return super.authenticate(authentication);
        }
    }
    

    추신 : https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java에서 찾을 수있는 TokenProvider 클래스

  5. from https://stackoverflow.com/questions/47354171/spring-webflux-custom-authentication-for-api by cc-by-sa and MIT license