From e2c588714948d7fe08b3cc644107a2ea44703892 Mon Sep 17 00:00:00 2001 From: Dmitry Sviridov Date: Tue, 19 Sep 2023 18:16:02 +0300 Subject: [PATCH] =?UTF-8?q?no-task:=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20basic=20auth=20=D0=BD=D0=B0=20=D0=BA=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=BD=D1=83=D1=8E=20=D1=81=D1=85=D0=B5=D0=BC=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sandbox/JwtAuthenticationConfigurer.java | 85 ++++++++++-- .../sandbox/RequestLoginPasswordFilter.java | 130 ++++++++++++++++++ .../SandboxSpringSecurityJwtApplication.java | 19 ++- 3 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 src/main/java/pro/akosarev/sandbox/RequestLoginPasswordFilter.java diff --git a/src/main/java/pro/akosarev/sandbox/JwtAuthenticationConfigurer.java b/src/main/java/pro/akosarev/sandbox/JwtAuthenticationConfigurer.java index 4ddfc99..be29082 100644 --- a/src/main/java/pro/akosarev/sandbox/JwtAuthenticationConfigurer.java +++ b/src/main/java/pro/akosarev/sandbox/JwtAuthenticationConfigurer.java @@ -1,11 +1,14 @@ package pro.akosarev.sandbox; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.AuthenticationFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; @@ -21,14 +24,24 @@ public class JwtAuthenticationConfigurer private Function accessTokenStringSerializer = Object::toString; + private ObjectMapper objectMapper; + private Function accessTokenStringDeserializer; private Function refreshTokenStringDeserializer; private JdbcTemplate jdbcTemplate; + private UserDetailsService daoUserDetailsService; + + private String defaultUsernameJsonBodyParameter = "username"; + + private String defaultPasswordJsonBodyParameter = "password"; + + private String nativeAppHeader; + @Override - public void init(HttpSecurity builder) throws Exception { + public void init(HttpSecurity builder) { var csrfConfigurer = builder.getConfigurer(CsrfConfigurer.class); if (csrfConfigurer != null) { csrfConfigurer.ignoringRequestMatchers(new AntPathRequestMatcher("/jwt/tokens", "POST")); @@ -36,21 +49,44 @@ public void init(HttpSecurity builder) throws Exception { } @Override - public void configure(HttpSecurity builder) throws Exception { + public void configure(HttpSecurity builder) { + AuthenticationManager authenticationManager = + builder.getSharedObject(AuthenticationManager.class); + + var requestLoginPasswordFilter = new RequestLoginPasswordFilter(); + requestLoginPasswordFilter + .authenticationManager(authenticationManager) + .defaultUsernameJsonBodyParameter(defaultUsernameJsonBodyParameter) + .defaultPasswordJsonBodyParameter(defaultPasswordJsonBodyParameter) + .nativeAppHeader(nativeAppHeader) + .objectMapper(objectMapper); + var requestJwtTokensFilter = new RequestJwtTokensFilter(); requestJwtTokensFilter.setAccessTokenStringSerializer(this.accessTokenStringSerializer); requestJwtTokensFilter.setRefreshTokenStringSerializer(this.refreshTokenStringSerializer); - var jwtAuthenticationFilter = new AuthenticationFilter(builder.getSharedObject(AuthenticationManager.class), - new JwtAuthenticationConverter(this.accessTokenStringDeserializer, this.refreshTokenStringDeserializer)); - jwtAuthenticationFilter - .setSuccessHandler((request, response, authentication) -> CsrfFilter.skipRequest(request)); - jwtAuthenticationFilter - .setFailureHandler((request, response, exception) -> response.sendError(HttpServletResponse.SC_FORBIDDEN)); + var jwtAuthenticationFilter = new AuthenticationFilter( + builder.getSharedObject(AuthenticationManager.class), + new JwtAuthenticationConverter( + this.accessTokenStringDeserializer, + this.refreshTokenStringDeserializer + ) + ); - var authenticationProvider = new PreAuthenticatedAuthenticationProvider(); - authenticationProvider.setPreAuthenticatedUserDetailsService( + jwtAuthenticationFilter.setSuccessHandler( + (request, response, authentication) -> CsrfFilter.skipRequest(request) + ); + + jwtAuthenticationFilter.setFailureHandler( + (request, response, exception) -> response.sendError(HttpServletResponse.SC_FORBIDDEN) + ); + + var preAuthenticatedAuthenticationProvider = new PreAuthenticatedAuthenticationProvider(); + var daoAuthenticationProvider = new DaoAuthenticationProvider(); + + preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService( new TokenAuthenticationUserDetailsService(this.jdbcTemplate)); + daoAuthenticationProvider.setUserDetailsService(daoUserDetailsService); var refreshTokenFilter = new RefreshTokenFilter(); refreshTokenFilter.setAccessTokenStringSerializer(this.accessTokenStringSerializer); @@ -59,9 +95,11 @@ public void configure(HttpSecurity builder) throws Exception { builder.addFilterAfter(requestJwtTokensFilter, ExceptionTranslationFilter.class) .addFilterBefore(jwtAuthenticationFilter, CsrfFilter.class) + .addFilterBefore(requestLoginPasswordFilter, ExceptionTranslationFilter.class) .addFilterAfter(refreshTokenFilter, ExceptionTranslationFilter.class) .addFilterAfter(jwtLogoutFilter, ExceptionTranslationFilter.class) - .authenticationProvider(authenticationProvider); + .authenticationProvider(preAuthenticatedAuthenticationProvider) + .authenticationProvider(daoAuthenticationProvider); } public JwtAuthenticationConfigurer refreshTokenStringSerializer( @@ -92,4 +130,29 @@ public JwtAuthenticationConfigurer jdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; return this; } + + public JwtAuthenticationConfigurer daoUserDetailsService(UserDetailsService daoUserDetailsService) { + this.daoUserDetailsService = daoUserDetailsService; + return this; + } + + public JwtAuthenticationConfigurer objectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + public JwtAuthenticationConfigurer defaultUsernameJsonBodyParameter(String defaultUsernameJsonBodyParameter) { + this.defaultUsernameJsonBodyParameter = defaultUsernameJsonBodyParameter; + return this; + } + + public JwtAuthenticationConfigurer defaultPasswordJsonBodyParameter(String defaultPasswordJsonBodyParameter) { + this.defaultPasswordJsonBodyParameter = defaultPasswordJsonBodyParameter; + return this; + } + + public JwtAuthenticationConfigurer nativeAppHeader(String nativeAppHeader) { + this.nativeAppHeader = nativeAppHeader; + return this; + } } diff --git a/src/main/java/pro/akosarev/sandbox/RequestLoginPasswordFilter.java b/src/main/java/pro/akosarev/sandbox/RequestLoginPasswordFilter.java new file mode 100644 index 0000000..0cc29e8 --- /dev/null +++ b/src/main/java/pro/akosarev/sandbox/RequestLoginPasswordFilter.java @@ -0,0 +1,130 @@ +package pro.akosarev.sandbox; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.log.LogMessage; +import org.springframework.http.HttpMethod; +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.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class RequestLoginPasswordFilter extends OncePerRequestFilter { + + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy(); + private ObjectMapper objectMapper; + private String defaultUsernameJsonBodyParameter = "username"; + private String defaultPasswordJsonBodyParameter = "password"; + private String nativeAppHeader = "X-native"; + private RequestMatcher requestMatcher = new AntPathRequestMatcher("/jwt/tokens", HttpMethod.POST.name()); + private AuthenticationManager authenticationManager; + private SecurityContextRepository securityContextRepository = new RequestAttributeSecurityContextRepository(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + if (this.requestMatcher.matches(request)) { + try { + var isNative = checkNativeHeader(request); + if (!isNative) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + UsernamePasswordAuthenticationToken authRequest = convert(request); + if (authRequest == null) { + this.logger.trace("Did not process authentication request on transform to UsernamePasswordAuthenticationToken"); + chain.doFilter(request, response); + return; + } + + String username = authRequest.getName(); + this.logger.trace(LogMessage.format("Found username '%s'", username)); + + Authentication authResult = this.authenticationManager.authenticate(authRequest); + SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); + context.setAuthentication(authResult); + this.securityContextHolderStrategy.setContext(context); + if (this.logger.isDebugEnabled()) { + this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); + } + this.securityContextRepository.saveContext(context, request, response); + } catch (AuthenticationException var8) { + this.securityContextHolderStrategy.clearContext(); + this.logger.debug("Failed to process authentication request", var8); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + chain.doFilter(request, response); + } + chain.doFilter(request, response); + } + + private boolean checkNativeHeader(HttpServletRequest request) { + return request.getHeader(this.nativeAppHeader) != null; + } + + private UsernamePasswordAuthenticationToken convert(HttpServletRequest request) throws IOException { + try { + var jsonRequest = this.objectMapper.readTree(request.getReader()); + var username = jsonRequest.get(defaultUsernameJsonBodyParameter).asText(); + var password= jsonRequest.get(defaultPasswordJsonBodyParameter).asText(); + if (username != null && password != null) { + return new UsernamePasswordAuthenticationToken(username, password); + } + } catch (IOException e) { + logger.error(e); + return null; + } + return null; + } + + public RequestLoginPasswordFilter objectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "Should not be null"); + this.objectMapper = objectMapper; + return this; + } + + public RequestLoginPasswordFilter defaultUsernameJsonBodyParameter(String defaultUsernameJsonBodyParameter) { + Assert.notNull(defaultUsernameJsonBodyParameter, "Should not be null"); + this.defaultUsernameJsonBodyParameter = defaultUsernameJsonBodyParameter; + return this; + } + + public RequestLoginPasswordFilter defaultPasswordJsonBodyParameter(String defaultPasswordJsonBodyParameter) { + Assert.notNull(defaultPasswordJsonBodyParameter, "Should not be null"); + this.defaultPasswordJsonBodyParameter = defaultPasswordJsonBodyParameter; + return this; + } + + public RequestLoginPasswordFilter requestMatcher(RequestMatcher requestMatcher) { + Assert.notNull(requestMatcher, "Should not be null"); + this.requestMatcher = requestMatcher; + return this; + } + + public RequestLoginPasswordFilter authenticationManager(AuthenticationManager authenticationManager) { + Assert.notNull(authenticationManager, "Should not be null"); + this.authenticationManager = authenticationManager; + return this; + } + + public RequestLoginPasswordFilter nativeAppHeader(String nativeAppHeader) { + Assert.notNull(nativeAppHeader, "Should not be null"); + this.nativeAppHeader = nativeAppHeader; + return this; + } +} diff --git a/src/main/java/pro/akosarev/sandbox/SandboxSpringSecurityJwtApplication.java b/src/main/java/pro/akosarev/sandbox/SandboxSpringSecurityJwtApplication.java index 1764c55..43eccc6 100644 --- a/src/main/java/pro/akosarev/sandbox/SandboxSpringSecurityJwtApplication.java +++ b/src/main/java/pro/akosarev/sandbox/SandboxSpringSecurityJwtApplication.java @@ -1,17 +1,18 @@ package pro.akosarev.sandbox; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.crypto.DirectDecrypter; import com.nimbusds.jose.crypto.DirectEncrypter; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jose.jwk.OctetSequenceKey; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -32,7 +33,9 @@ public static void main(String[] args) { public JwtAuthenticationConfigurer jwtAuthenticationConfigurer( @Value("${jwt.access-token-key}") String accessTokenKey, @Value("${jwt.refresh-token-key}") String refreshTokenKey, - JdbcTemplate jdbcTemplate + @Qualifier("daoUserDetailsService") UserDetailsService userDetailsService, + JdbcTemplate jdbcTemplate, + ObjectMapper objectMapper ) throws ParseException, JOSEException { return new JwtAuthenticationConfigurer() .accessTokenStringSerializer(new AccessTokenJwsStringSerializer( @@ -47,16 +50,21 @@ public JwtAuthenticationConfigurer jwtAuthenticationConfigurer( .refreshTokenStringDeserializer(new RefreshTokenJweStringDeserializer( new DirectDecrypter(OctetSequenceKey.parse(refreshTokenKey)) )) + .defaultUsernameJsonBodyParameter("username") + .defaultPasswordJsonBodyParameter("password") + .nativeAppHeader("X-native-app") + .objectMapper(objectMapper) + .daoUserDetailsService(userDetailsService) .jdbcTemplate(jdbcTemplate); } @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http, - JwtAuthenticationConfigurer jwtAuthenticationConfigurer) throws Exception { + public SecurityFilterChain securityFilterChain( + HttpSecurity http, + JwtAuthenticationConfigurer jwtAuthenticationConfigurer) throws Exception { http.apply(jwtAuthenticationConfigurer); return http - .httpBasic(Customizer.withDefaults()) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorizeHttpRequests -> @@ -68,6 +76,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, } @Bean + @Qualifier("daoUserDetailsService") public UserDetailsService userDetailsService(JdbcTemplate jdbcTemplate) { return username -> jdbcTemplate.query("select * from t_user where c_username = ?", (rs, i) -> User.builder()