修改 configure(HttpSecurity http)
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@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
,返回结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{
"password": null,
"username": "bolitao",
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
}
|
和上节类似,添加 failureHandler
即可应对失败的返回情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.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
类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@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 添加一小段即可实现:
1
2
3
4
5
6
7
8
9
|
.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 函数描述返回值:
1
2
3
4
5
6
7
|
.logoutSuccessHandler((req, resp, authentication)-> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new ObjectMapper().writeValueAsString("注销成功"));
writer.flush();
writer.close();
})
|