Spring Security e autenticazione JSON

Ho un’applicazione in spring / spring-mvc che utilizza totalmente le comunicazioni JSON. Ora ho bisogno di autenticare la mia applicazione con spring security 3 (che usa LdapAuthenticationProvider) tramite JSON.

Il modulo di invio di segretezza di default richiede un POST come questo:

POST /myapp/j_spring_security_check HTTP/1.1 Accept-Encoding: gzip,deflate Content-Type: application/x-www-form-urlencoded Content-Length: 32 Host: 127.0.0.1:8080 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.1.1 (java 1.5) j_username=myUsername&j_password=myPass 

Ma voglio passare un object JSON come questo:

 {"j_username":"myUsername","j_password":"myPass"} 

Ho letto molti post come questo , quest’altro o questo senza fortuna, in tutti i casi di ajax si fa un POST come sopra.

Qualche idea?

Secondo i suggerimenti di Kevin,
e dopo aver letto questo post: 1 , 2 , documentazione 3 , e grazie a questo post sul blog,
Ho scritto il mio FORM_LOGIN_FILTER per gestire direttamente JSON prima dell’autenticazione.
Incollo il mio codice per la comunità.

L’objective è quello di garantire sia l’autenticazione POST classica del browser con l’autenticazione basata su JSON. Anche nell’autenticazione JSON voglio evitare il reindirizzamento per loginSuccesful.htm

Nel contesto:

                           

CustomUsernamePasswordAuthenticationFilter class:

 public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter{ private String jsonUsername; private String jsonPassword; @Override protected String obtainPassword(HttpServletRequest request) { String password = null; if ("application/json".equals(request.getHeader("Content-Type"))) { password = this.jsonPassword; }else{ password = super.obtainPassword(request); } return password; } @Override protected String obtainUsername(HttpServletRequest request){ String username = null; if ("application/json".equals(request.getHeader("Content-Type"))) { username = this.jsonUsername; }else{ username = super.obtainUsername(request); } return username; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){ if ("application/json".equals(request.getHeader("Content-Type"))) { try { /* * HttpServletRequest can be read only once */ StringBuffer sb = new StringBuffer(); String line = null; BufferedReader reader = request.getReader(); while ((line = reader.readLine()) != null){ sb.append(line); } //json transformation ObjectMapper mapper = new ObjectMapper(); LoginRequest loginRequest = mapper.readValue(sb.toString(), LoginRequest.class); this.jsonUsername = loginRequest.getUsername(); this.jsonPassword = loginRequest.getPassword(); } catch (Exception e) { e.printStackTrace(); } } return super.attemptAuthentication(request, response); } } 

Classe CustomAuthenticationSuccessHandler:

 public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication auth )throws IOException, ServletException { if ("application/json".equals(request.getHeader("Content-Type"))) { /* * USED if you want to AVOID redirect to LoginSuccessful.htm in JSON authentication */ response.getWriter().print("{\"responseCode\":\"SUCCESS\"}"); response.getWriter().flush(); } else { super.onAuthenticationSuccess(request, response, auth); } } } 
 public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){ if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } LoginRequest loginRequest = this.getLoginRequest(request); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } private LoginRequest getLoginRequest(HttpServletRequest request) { BufferedReader reader = null; LoginRequest loginRequest = null; try { reader = request.getReader(); Gson gson = new Gson(); loginRequest = gson.fromJson(reader, LoginRequest.class); } catch (IOException ex) { Logger.getLogger(AuthenticationFilter.class.getName()).log(Level.SEVERE, null, ex); } finally { try { reader.close(); } catch (IOException ex) { Logger.getLogger(AuthenticationFilter.class.getName()).log(Level.SEVERE, null, ex); } } if (loginRequest == null) { loginRequest = new LoginRequest(); } return loginRequest; } } 

Se si desidera un parser di body di richiesta diverso per la richiesta di accesso, estendere attemptAuthentication metodo UsernamePasswordAuthenticationFilter e scavalcare il metodo attemptAuthentication . Per impostazione predefinita, UsernamePasswordAuthenticationFilter analizzerà i dati url codificati e creerà UsernamePasswordAuthenticationToken da esso. Ora hai solo bisogno di fare un parser che analizzerà tutto ciò che invii all’applicazione.

Ecco un esempio che analizzerà {"username": "someusername", "password": "somepassword"}

 public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { BufferedReader reader = request.getReader(); StringBuffer sb = new StringBuffer(); String line = null; while ((line = reader.readLine()) != null) { sb.append(line); } String parsedReq = sb.toString(); if (parsedReq != null) { ObjectMapper mapper = new ObjectMapper(); AuthReq authReq = mapper.readValue(parsedReq, AuthReq.class); return new UsernamePasswordAuthenticationToken(authReq.getUsername(), authReq.getPassword()); } } catch (Exception e) { System.out.println(e.getMessage()); throw new InternalAuthenticationServiceException("Failed to parse authentication request body"); } return null; } @Data public static class AuthReq { String username; String password; } } 

Nel corpo della richiesta snippet viene estratto in stringa e associato all’object AuthReq ( @Data annotazione @Data è di lib lombok, genererà i seter e i getter). Quindi puoi rendere UsernamePasswordAuthenticationToken che verrà passato a Default AuthenticationProvider .

Ora è ansible estendere WebSecurityConfigurerAdapter e sovrascrivere il metodo cnofigure per sostituire il vecchio filtro.

 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login", "/logout").permitAll() .anyRequest().authenticated() .and().addFilterAt(new CustomUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .formLogin().loginProcessingUrl("/login") .and() .csrf().disable(); } 

Con il metodo addFilterAt si sostituisce il nome UsernamePasswordAuthenticationFilter predefinito addFilterAt . Non dimenticare di utilizzare @EnableWebSecurity annotazione @EnableWebSecurity .

Un altro modo, secondo questo post, è di gestire manualmente l’autenticazione di sicurezza a molla direttamente nel Controller.
In questo modo è molto semplice gestire l’input JSON ed evitare il reindirizzamento dell’accesso:

 @Autowired AuthenticationManager authenticationManager; @ResponseBody @RequestMapping(value="/login.json", method = RequestMethod.POST) public JsonResponse mosLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest request) { JsonResponse response = null; try { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); token.setDetails(new WebAuthenticationDetails(request)); Authentication auth = authenticationManager.authenticate(token); SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(auth); if(auth.isAuthenticated()){ HttpSession session = request.getSession(true); session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext); LoginResponse loginResponse = new LoginResponse(); loginResponse.setResponseCode(ResponseCodeType.SUCCESS); response = loginResponse; }else{ SecurityContextHolder.getContext().setAuthentication(null); ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setResponseCode(ResponseCodeType.ERROR); response = errorResponse; } } catch (Exception e) { ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setResponseCode(ResponseCodeType.ERROR); response = errorResponse; } return response; } 

Guarda questo esempio: https://github.com/fuhaiwei/springboot_security_restful_api

 @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private CustomLoginHandler customLoginHandler; @Autowired private CustomLogoutHandler customLogoutHandler; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/api/admin/**").hasRole("ADMIN") .antMatchers("/api/basic/**").hasRole("BASIC") .antMatchers("/api/session").permitAll() .antMatchers(HttpMethod.GET).permitAll() .antMatchers("/api/**").hasRole("BASIC"); http.formLogin(); http.logout() .logoutUrl("/api/session/logout") .addLogoutHandler(customLogoutHandler) .logoutSuccessHandler(customLogoutHandler); http.exceptionHandling() .accessDeniedHandler(customAccessDeniedHandler) .authenticationEntryPoint(customAccessDeniedHandler); http.csrf() .ignoringAntMatchers("/api/session/**"); http.addFilterBefore(new AcceptHeaderLocaleFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class); } private CustomAuthenticationFilter customAuthenticationFilter() throws Exception { CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setAuthenticationSuccessHandler(customLoginHandler); filter.setAuthenticationFailureHandler(customLoginHandler); filter.setAuthenticationManager(authenticationManager()); filter.setFilterProcessesUrl("/api/session/login"); return filter; } private static void responseText(HttpServletResponse response, String content) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); byte[] bytes = content.getBytes(StandardCharsets.UTF_8); response.setContentLength(bytes.length); response.getOutputStream().write(bytes); response.flushBuffer(); } @Component public static class CustomAccessDeniedHandler extends BaseController implements AuthenticationEntryPoint, AccessDeniedHandler { // NoLogged Access Denied @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { responseText(response, errorMessage(authException.getMessage())); } // Logged Access Denied @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { responseText(response, errorMessage(accessDeniedException.getMessage())); } } @Component public static class CustomLoginHandler extends BaseController implements AuthenticationSuccessHandler, AuthenticationFailureHandler { // Login Success @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { LOGGER.info("User login successfully, name={}", authentication.getName()); responseText(response, objectResult(SessionController.getJSON(authentication))); } // Login Failure @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { responseText(response, errorMessage(exception.getMessage())); } } @Component public static class CustomLogoutHandler extends BaseController implements LogoutHandler, LogoutSuccessHandler { // Before Logout @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { } // After Logout @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { responseText(response, objectResult(SessionController.getJSON(null))); } } private static class AcceptHeaderLocaleFilter implements Filter { private AcceptHeaderLocaleResolver localeResolver; private AcceptHeaderLocaleFilter() { localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(Locale.US); } @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Locale locale = localeResolver.resolveLocale((HttpServletRequest) request); LocaleContextHolder.setLocale(locale); chain.doFilter(request, response); } @Override public void destroy() { } } } public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { UsernamePasswordAuthenticationToken authRequest; try (InputStream is = request.getInputStream()) { DocumentContext context = JsonPath.parse(is); String username = context.read("$.username", String.class); String password = context.read("$.password", String.class); authRequest = new UsernamePasswordAuthenticationToken(username, password); } catch (IOException e) { e.printStackTrace(); authRequest = new UsernamePasswordAuthenticationToken("", ""); } setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } 

Ecco la configurazione java per le soluzioni di cui sopra:

 @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .addFilterBefore(authenticationFilter(),UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll(); } @Bean public AuthenticationFilter authenticationFilter() throws Exception{ AuthenticationFilter authenticationFilter = new AuthenticationFilter(); authenticationFilter.setUsernameParameter("username"); authenticationFilter.setPasswordParameter("password"); authenticationFilter.setAuthenticationManager(authenticationManager()); authenticationFilter.setFilterProcessesUrl("/login"); authenticationFilter.setAuthenticationSuccessHandler(successHandler()); return authenticationFilter; } @Bean public SuccessHandler successHandler(){ return new SuccessHandler(); }