web-dev-qa-db-fra.com

Spring Security et authentification JSON

J'ai une application dans spring/spring-mvc qui utilise totalement les communications JSON ..__ Je dois maintenant authentifier mon application avec Spring Security 3 (qui utilise LdapAuthenticationProvider) via JSON.

Le formulaire de soumission Spring Seurity par défaut nécessite a POST comme ceci:

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

Mais je veux passer un objet JSON comme ceci:

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

Je lis beaucoup de posts comme this , cet autre ou celui-ci sans chance, dans tous les cas ajax se fait a POST comme ci-dessus.

Des idées?

27
fl4l
12
Kevin Bayes

Selon les suggestions de Kevin,
et après avoir lu ce billet: 1 , 2 , documentation 3 , et merci à this blog post,
J'ai écrit mon propre FORM_LOGIN_FILTER pour gérer directement JSON avant l'authentification.
Je colle mon code pour la communauté.

L’objectif est d’autoriser l’authentification à la fois par le navigateur classique POST et l’authentification basée sur JSON. De plus, dans l'authentification JSON, je souhaite éviter la redirection vers loginSuccesful.htm.

Dans le contexte:

<security:http use-expressions="true" auto-config="false" entry-point-ref="http403EntryPoint">      
    <security:intercept-url pattern="/logs/**" access="denyAll" />
    <!-- ... All other intercept URL -->

    <security:custom-filter ref="CustomUsernamePasswordAuthenticationFilter" position="FORM_LOGIN_FILTER "/>
    <security:logout
            invalidate-session="true"
            logout-success-url="/LogoutSuccessful.htm"
            delete-cookies="true"
    />
    <security:session-management>
        <security:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
    </security:session-management>
    <security:access-denied-handler error-page="/accessDenied.htm" />
</security:http>

<bean id="CustomUsernamePasswordAuthenticationFilter" class="path.to.CustomUsernamePasswordAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="customSuccessHandler"/>
    <property name="authenticationFailureHandler" ref="failureHandler"/>
    <property name="filterProcessesUrl" value="/j_spring_security_check"/>
    <property name="usernameParameter" value="j_username"/>
    <property name="passwordParameter" value="j_password"/>
</bean>

<bean id="customSuccessHandler" class="path.to.CustomAuthenticationSuccessHandler">
    <property name="defaultTargetUrl" value="/login.htm" />
    <property name="targetUrlParameter" value="/LoginSuccessful.htm" />
</bean>

<bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <property name="defaultFailureUrl" value="/login.htm" />
</bean>

<bean id="http403EntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" />

CustomUsernamePasswordAuthenticationFilter classe:

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);
        }
    }
}
23
fl4l
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;
    }
}
13
oe.elvik

Si vous souhaitez simplement un analyseur de corps de requête différent pour la demande de connexion, étendez simplement UsernamePasswordAuthenticationFilter et substituez la méthode attemptAuthentication . Par défaut, UsernamePasswordAuthenticationFilter analysera les données encodées dans l'URL et créera UsernamePasswordAuthenticationToken. Maintenant, il vous suffit de créer un analyseur syntaxique qui analysera tout ce que vous envoyez à l'application.

Voici un exemple qui analysera {"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;
    }

}

Dans le fragment, le corps de la demande est extrait en chaîne et mappé à l'objet AuthReq (l'annotation @Data est issue de lombok lib, elle générera des séparateurs et des getters) . Vous pouvez alors créer UsernamePasswordAuthenticationToken qui sera passé à la valeur par défaut AuthenticationProvider.

Vous pouvez maintenant étendre WebSecurityConfigurerAdapter et remplacer la méthode cnofigure pour remplacer l’ancien filtre.

@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();
}

Avec la méthode addFilterAt, vous remplacez la valeur par défaut UsernamePasswordAuthenticationFilter. N'oubliez pas d'utiliser l'annotation @EnableWebSecurity.

7
Mihael Mamula

Un autre moyen, selon this post, consiste à gérer manuellement l’authentification de sécurité du ressort directement dans le contrôleur.
De cette manière, il est très simple de gérer les entrées JSON et d’éviter la redirection de connexion:

@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;
}
5
fl4l

J'ai appliqué les réponses de fl4l et oe.elvik pour la connexion avec des informations d'identification JSON dans une application Spring Boot. Je travaille avec une configuration de bean basée sur des annotations.

Dans les réponses référencées, un filtre personnalisé est créé dans lequel le gestionnaire d'authentification est injecté. Pour ce faire, le gestionnaire d'authentification doit être présent en tant que bean Spring. Voici un lien sur la façon de le faire: https://stackoverflow.com/a/21639553/3950535 .

1
Martijn Dirkse

Voici la configuration Java pour les solutions ci-dessus:

@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();
}
0
Ali786

Regardez cet exemple: 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);
    }

}
0
Intopass