【问题标题】:Spring Security and JSON AuthenticationSpring Security 和 JSON 身份验证
【发布时间】:2013-10-30 06:55:01
【问题描述】:

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

默认的 spring 安全提交表单需要这样的 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。

有什么想法吗?

【问题讨论】:

    标签: json spring spring-mvc spring-security


    【解决方案1】:
    【解决方案2】:

    根据凯文的建议,
    并在阅读此帖子后:12、文档3,并感谢this 博客帖子,
    我写了自己的 FORM_LOGIN_FILTER 来在认证前直接管理 JSON。
    我将我的代码粘贴到社区。​​p>

    目标是同时授予经典浏览器表单 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);
            }
        }
    }
    

    【讨论】:

    • 这不是安全的。过滤器是一个 bean。您不应该将用户名和密码存储为成员,我建议将其保存为请求属性或不覆盖获取用户名和获取密码。看看我的解决方案
    • 是的,我尝试了这个解决方案并遇到了这个确切的问题。使用请求属性而不是成员变量解决了这个问题。
    • 你的方向是正确的,但这是一个糟糕的实现。正如@oe.elvik 提到的,过滤器中不能有实例变量!只需覆盖尝试身份验证并在那里读取用户名/密码。无需使用获取密码/获取用户名。也不要根据要求存储密码..或其他任何地方!
    【解决方案3】:

    另一种方法,根据this的帖子,是直接在Controller中手动管理spring安全认证。
    以这种方式管理 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;
    }
    

    【讨论】:

    • 我更喜欢过滤器,上面基本上只是控制器中的一个请求,我觉得它不应该混合,过滤器很好地将安全性和控制器分开。个人意见。
    • 我也有同样的看法,我更喜欢过滤器,因为它位于进入控制器之前管理的“安全层”中。从安全的角度来看,您认为这种方案的安全性不如过滤方案吗?是否存在潜在的安全风险?
    • 不要过滤器存在不是注册路由的问题,spring 无法正确处理其他类型的错误或特性,如 OPTIONS 和 HEAD 请求,或显示所有可用的路线。
    【解决方案4】:
    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;
        }
    }
    

    【讨论】:

    • 好的,这是一个有效的解决方案。我班级的目标是尊重原始的尝试身份验证流程,检索和设置用户名和密码,然后在方法结束时调用 super.attemptAuthentication(...)。谢谢
    • 是的,但是通过将用户名和密码保存为 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;
        }
    
    }
    

    在sn-p中,请求体被提取为字符串并映射到对象AuthReq@Data注解来自lombok 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注解。

    【讨论】:

    • 也许春天变了。从 attemptAuthentication 返回 UsernamePasswordAuthenticationToken 似乎不正确。
    【解决方案6】:

    看这个例子: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);
        }
    
    }
    

    【讨论】:

      【解决方案7】:

      这是上述解决方案的java配置:

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

      【讨论】:

        【解决方案8】:

        我应用来自 fl4loe.elvik 的答案在 Spring Boot 应用程序中使用 JSON 凭据登录。我正在使用基于注释的 bean 配置。

        在引用的答案中,创建了一个自定义过滤器,其中注入了身份验证管理器。为此,身份验证管理器必须作为 Spring Bean 存在。这是一个关于如何做到这一点的链接:https://stackoverflow.com/a/21639553/3950535

        【讨论】:

          猜你喜欢
          • 2014-04-07
          • 2012-02-18
          • 2014-03-18
          • 2022-01-15
          • 2014-02-26
          • 2011-10-17
          • 2013-01-15
          • 2019-08-25
          • 2019-04-08
          相关资源
          最近更新 更多