Spring Security 和 JSON 身份验证
Posted
技术标签:
【中文标题】Spring Security 和 JSON 身份验证【英文标题】:Spring Security and JSON Authentication 【发布时间】: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"
我读了很多帖子,比如this、this other 或this one,但运气不好,在所有 ajax 情况下都是像上面那样做一个 POST。
有什么想法吗?
【问题讨论】:
【参考方案1】:您可以编写自己的安全过滤器来解析您的 JSON。
http://docs.spring.io/spring-security/site/docs/3.0.x/reference/core-web-filters.html
您可以使用 BasicAuthenticationFilter 作为参考:
http://docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.html
【讨论】:
【参考方案2】:根据凯文的建议, 并在阅读此帖子后:1、2、文档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】:我应用来自 fl4l 和 oe.elvik 的答案在 Spring Boot 应用程序中使用 JSON 凭据登录。我正在使用基于注释的 bean 配置。
在引用的答案中,创建了一个自定义过滤器,其中注入了身份验证管理器。为此,身份验证管理器必须作为 Spring Bean 存在。这是一个关于如何做到这一点的链接:https://***.com/a/21639553/3950535。
【讨论】:
以上是关于Spring Security 和 JSON 身份验证的主要内容,如果未能解决你的问题,请参考以下文章
Spring Security:仅为经过身份验证的用户执行 jQuery 代码
Spring Security WebFlux - 带有身份验证的主体
在 Spring Security 中处理基本身份验证的未经授权的错误消息