2013-10-21 140 views
22

我在spring/spring-mvc中有一个完全使用JSON通信的应用程序。 现在我需要通过JSON对Spring应用程序进行身份验证(使用LdapAuthenticationProvider)。Spring Security和JSON身份验证

默认春天seurity提交表单需要这样的POST:

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 

但我想传递一个JSON对象是这样的:

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

我看了很多帖子像thisthis otherthis one没有运气,在所有的ajax案例中都是像上面那样做POST。

任何想法?

回答

16

与凯文的建议根据,
和阅读这个帖子后:12,文档3,并感谢this博客帖子,
我写了自己的FORM_LOGIN_FILTER直接管理e验证前的JSON。
我粘贴我的社区代码。

目标是授予传统浏览器表单POST身份验证与基于JSON的身份验证。此外,在JSON验证我想避免重定向到loginSuccesful.htm

在背景:

<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类:

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

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); 
     } 
    } 
} 
+0

共享的解决方案十分感谢。 –

+11

这不是安全的。过滤器是一个bean。您不应将用户名和密码存储为我建议保存为请求属性的成员,也不要将其覆盖为取得用户名和取得密码。我的解决方案 –

+1

是的,我试过这个解决方案,并有这个确切的问题。使用请求属性而不是成员变量解决了问题。 – Keeth

5

另一种方式,根据this p直接在Controller中手动管理弹簧安全认证。
以这种方式是非常简单的管理JSON输入,避免登录重定向:

@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; 
} 
+1

我宁愿过滤器,上面基本上只是一个控制器的请求,我觉得它不应该混合,过滤器给安全和控制器之间的一个很好的分离。个人想法。 –

+0

我有同样的观点,我更喜欢过滤器,因为在进入控制器之前管理的“安全层”中。从安全角度来看,您认为这种解决方案比过滤解决方案更安全吗?有没有潜在的安全展览? – fl4l

+0

不过滤器没有注册路由的问题,并且spring不能正确处理其他类型的错误或功能,如OPTIONS和HEAD请求,或者显示所有可用路由。 – xenoterracide

10
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; 
    } 
} 
+0

确定这是一个有效的解决方案。我的课程的目标是尊重原始的validationAuthentication流程检索并设置用户名和密码,然后在方法结束时调用super.attemptAuthentication(...)。谢谢 – fl4l

+3

是的,但是通过将用户名和密码保存为bean的成员,解决方案不是防踩踏的。试图在同一时间进行身份验证的两个用户可能会混用用户名和密码。一种解决方案也是使用setAttribute在请求对象中保存用户名和密码 –

5

如果你想只是不同的请求主体解析器登录请求只是延长UsernamePasswordAuthenticationFilter和覆盖attemptAuthentication方法。 默认情况下,UsernamePasswordAuthenticationFilter将解析url编码数据并从中创建UsernamePasswordAuthenticationToken。现在,您只需要制作解析器即可解析发送给应用程序的任何内容。

下面是示例,将解析{"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; 
    } 

} 

在片断请求体提取到串并映射到对象AuthReq@Data注释是从龙目lib中,它会产生seters和getter)。 比你可以让UsernamePasswordAuthenticationToken,将被传递给默认AuthenticationProvider

现在您可以扩展WebSecurityConfigurerAdapter并覆盖cnofigure方法来替换旧的过滤器。

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

随着addFilterAt方法替换默认UsernamePasswordAuthenticationFilter。不要忘记使用@EnableWebSecurity注释。

0

请看下面的例子: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); 
    } 

} 
相关问题