Spring Security-4-自定义登录成功失败处理

上文中,我们实现了一个自定义的可配置的登录页,效果就是首先一个请求过来,如果需要认证的话,就跳转到认证的界面,然后认证完成之后,再跳转到最开始访问的页面

在实际的环境中,我们登录成功之后可能并不是去跳转页面, 在前后端分离的环境中一般都是返回一个json格式的用户信息,然后登录失败也是返回具体的原因给前端

下面就来看一下如何实现自定义的登录成功/失败后返回json数据

自定义登录成功处理

登录成功处理只需要去实现 MyAuthenticationSuccessHandler 接口, 这个接口很简单,里面就一个方法,看一下源码
image

这里的参数有一个 Authentication 对象,我们之前看认证处理源码的时候,可以看到认证完成之后就会返回这个对象,然后在我们自定义的实现里面,就把这个对象转成json返回去

新建一个类 MyAuthenticationSuccessHandler 实现这个接口,重写里面的方法,具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户登录成功...");

response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));

}

}

这里就是直接把 Authentication 这个对象转成json字符串然后返回去了,然后这个类加了 @Component("myAuthenticationSuccessHandler") 这个注解,交给spring去管理,然后起了个名字,下面的配置要用到

事件写好之后还需要把这个handler加到 security 的配置中去,才能让他生效,修改 BrowserSecurityConfig 部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/from")

.successHandler(myAuthenticationSuccessHandler)
.and()
// ... 省略其他代码
}

先把我们刚刚创建的 MyAuthenticationSuccessHandler 注入进去,然后通过 successHandler() 方法去设置

测试

配置完成之后,启动项目,随便访问一个接口,然后登录,返回值如下:

image

可以看到返回的就是一串json字符串,格式化一下,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"authorities": [{"authority": "admin"}],
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "CAD61C0C6DB0DE0C5FDF4893061C1247"
},
"authenticated": true,
"principal": {
"password": null,
"username": "zhangsan",
"authorities": [{"authority": "admin"}],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"credentials": null,
"name": "zhangsan"
}

这里就包括了用户的权限,用户名,以及我们认证返回的那个 UserDetail 的信息, 这个基本都能看得懂 就不做过多解释了

自定义登录失败处理

登录失败和成功是类似的,只不过实现的接口不一样, 这里要实现的是 AuthenticationFailureHandler 这个接口,看一下源码

image

里面也是只有一个方法,唯一的区别是这里的参数是 AuthenticationException ,这个就是认证失败的异常,然后我们在自定义的实现里面,把这个对象也转成json返回给前端

新建类 MyAuthenticationFailureHandler ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("认证失败...");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}

}

然后,还是在 BrowserSecurityConfig 中配置一下,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
// formLogin 表示表单认证
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/from")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()

// ... 省略其他配置
}

和上面成功的配置基本是一样的

测试

完成之后,做一下测试,启动项目,随便访问一个接口,然后输入一个错误的密码,返回如下:
image

返回值简化一下,如下:

1
2
3
4
5
6
7
{
"cause": null,
"stackTrace":[]
"localizedMessage": "坏的凭证",
"message": "坏的凭证",
"suppressed": []
}

stackTrace 里面是一些java的错误堆栈信息,这个不看,然后其他的就是登陆失败的原因返回去了

到这儿,自定义的登录成功/失败的处理都完成了

升级改造

通过上面的配置,我们在任何时候都是返回的json数据给前端, 那可能实际使用中就是想直接跳转请求,而不是返回json数据

我们这里可以做成一个可配置的,就跟之前的登录页面一样,由调用服务的一方去决定是返回json还是去跳转页面, 下面看一下如何实现

配置类改造

还是和登录页一样,在上文中的浏览器配置类里面加一个配置,如下:

1
2
3
4
/**
* 登录返回类型
*/
private LoginResponseType loginType = LoginResponseType.JSON;

这里用到了一个我们自己定义的枚举, LoginResponseType, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum LoginResponseType implements Serializable {

/**
* 跳转请求
*/
REDIRECT,

/**
* json返回
*/
JSON,

;

}

里面之后两个,一个跳转一个json返回,然后配置类中给的默认的是返回json的

handler改造

配置类搞好之后,再来改造之前的两个handler, 先看登录成功的这个 MyAuthenticationFailureHandler , 我们之前实现的 AuthenticationFailureHandler. 现在 改成继承 SavedRequestAwareAuthenticationSuccessHandler 这个是 Spring Security 默认的成功处理 ,然后我们重写里面的方法,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

@Autowired
private ObjectMapper objectMapper;

@Autowired
private SecurityProperties securityProperties;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户登录成功...");

if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginResponseType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
} else {
super.onAuthenticationSuccess(request, response, authentication);
}

}

}

这里就是加了个判断,如果配置是json的话,就返回json,如果不是,那就调用父类的处理方式,就是 Spring Security 默认的成功处理

再看下失败的处理, 在 MyAuthenticationFailureHandler 中我们是实现的 AuthenticationFailureHandler 这个接口,然后现在改成继承 SimpleUrlAuthenticationFailureHandler , 然后里面方法和上面一样,也是加个判断就ok了, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Component("myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

@Autowired
private ObjectMapper objectMapper;

@Autowired
private SecurityProperties securityProperties;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

log.info("认证失败...");

if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginResponseType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
} else {
super.onAuthenticationFailure(request, response, exception);
}

}

}

同样的,如果配置的不是返回json就调用父类的处理方式

上面的内容处理完成之后,就可以在demo项目中的 application.yml 中去配置了,如下:

1
2
3
4
core:
security:
browser:
loginResponseType: REDIRECT

测试

配置都完成之后,来测试看下效果,application.yml 中,先不做任何配置, 直接用默认的就是json的,然后启动项目,效果如下:
image

然后是认证失败的,如下:
image

最后把配置加上,重启,改成Security 默认的处理方式,来看下效果,如下:
image

总结

本文主要实现了自定义的登录成功/失败处理

分别通过实现 AuthenticationSuccessHandlerAuthenticationFailureHandler 这两个接口实现

Spring Security 默认的实现是 SavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandler 可以通过继承这两个类来重写默认的处理方式

代码已经上传github,传送门