登陆成功返回 json
修改 configure(HttpSecurity http)
如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("passwd")
.successHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
writer.flush();
writer.close();
})
.permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
.and()
.csrf().disable();
}
主要添加了 successHandler
部分,指定登陆成功后返回的内容,我指定为 json 格式的登录认证信息。
此时使用 POST 方式请求登录接口 127.0.0.1:8080/login?username=bolitao&passwd=bolitao
,返回结果如下:
{
"password": null,
"username": "bolitao",
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
}
登陆失败返回 json
和上节类似,添加 failureHandler
即可应对失败的返回情况:
.failureHandler((req, resp, exception) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
String exceptionMessage = exception.getMessage();
if (exception instanceof LockedException) {
exceptionMessage = "账户被锁";
} else if (exception instanceof CredentialsExpiredException) {
exceptionMessage = "密码过期";
} else if (exception instanceof AccountExpiredException) {
exceptionMessage = "账户过期";
} else if (exception instanceof DisabledException) {
exceptionMessage = "账户已被禁用";
} else if (exception instanceof BadCredentialsException) {
exceptionMessage = "用户名或密码错误";
}
writer.write(new ObjectMapper().writeValueAsString(exceptionMessage));
writer.flush();
writer.close();
})
这里未作特殊处理,仅返回了具体错误信息。
Q:为什么用户名或密码错误不返回确定的错误呢?账户输错提示用户名错误、密码输错提示密码错误,这样不是更友好吗?
A:为了防止被穷举撞库。
在 Spring Security 源码中,其实确实是有密码错误的异常的,但默认不会返回,只会统一返回
BadCredentialsException
。具体见 Spring Security 源码的
AbstractUserDetailsAuthenticationProvider
类:@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); String username = determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } // ...
可以看到 Spring Security 通过
hideUserNotFoundExceptions
这个布尔值判断是否返回UsernameNotFoundException
(用户不存在异常)。并且hideUserNotFoundExceptions
的默认值为true
。
未登录提示
Spring Security 默认会跳转到 loginURL 让用户进行登陆,在前后端分离场景下返回未登录 json 消息才是更合适的方式。在 config 添加一小段即可实现:
.exceptionHandling().authenticationEntryPoint(
(req, resp, exception) -> {
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString("尚未登陆"));
writer.flush();
writer.close();
});
这里仅返回了一句话,在实际场景应该用统一返回类进行包装。实际效果:
注销成功提示
和上面的登陆成功返回 json 类似,定义一个 lambda 函数描述返回值:
.logoutSuccessHandler((req, resp, authentication)-> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString("注销成功"));
writer.flush();
writer.close();
})