写在前面

通过前面《详解登录流程》一文的学习,我们已经对用户登录流程、认证过程和保存用户信息等内容有了一个较为清晰的认识。同时我们也对用户登录流程和认证过程分别用了更为详细的内容去进行学习,那么本篇就花点时间来学习关于保存用户信息等那些事。

Authentication对象

首先阅读《详解登录流程》一文,之后再来阅读本部分内容会容易很多。

前面我们曾多次提到过Authentication这个接口,它用来保存用户的登录信息,这里再次贴上该接口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();

Object getCredentials();

Object getDetails();

Object getPrincipal();

boolean isAuthenticated();

void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

可以看到该接口继承了Principal接口,查看一下该接口的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Principal {
public boolean equals(Object another);

public String toString();

public int hashCode();

public String getName();

public default boolean implies(Subject subject) {
if (subject == null)
return false;
return subject.getPrincipals().contains(this);
}
}

这个接口中定义的都是一些较为基本的方法,因此Authentication接口其实也是进一步对Principal接口进行了封装。

回到Authentication接口中来,可以发现它是一个接口,其中有6个方法:
(1)getAuthorities()方法用来获取用户的权限;
(2)getCredentials()方法用来获取用户的凭证,通常这里的凭证就是用户密码;
(3)getDetails()方法用来获取用户携带的详细信息;
(4)getPrincipal()方法用来获取当前用户,注意它可能是用户名,也可能是用户对象本身;
(5)isAuthenticated()方法用来判断当前用户是否认证成功;
(6)setAuthenticated()方法用来设置当前用户是否认证。

看到(3)中的getDetails()方法了么?它用来获取用户携带的详细信息,那么问题来了,用户到底携带了哪些详细信息呢?查看一下源码中对于该方法的解释:

1
Stores additional details about the authentication request. These might be an IP address, certificate serial number etc.

可以看到该方法主要用于存储一些与用户身份认证相关的额外信息,如IP地址、证书序列号等。

源码分析

通过前面的学习,我们知道用户登录必定经过UsernamePasswordAuthenticationFilter过滤器,且在该类的attemptAuthentication方法中会获取登录用户名和密码,查看该方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

前面就是获取请求中的用户名和密码,之后调用setDetails方法,将request和UsernamePasswordAuthenticationToken对象作为参数传入进去,但是细心的你可能发现这个UsernamePasswordAuthenticationToken对象是没有details这个属性的,它没有这个属性,那就查看一下它的父类AbstractAuthenticationToken,可以发现它的父类是有的:

1
2
3
4
5
6
7
8
9
private Object details;

public Object getDetails() {
return this.details;
}

public void setDetails(Object details) {
this.details = details;
}

现在问题来了,这个details对象中保存的是什么呢?查看一下UsernamePasswordAuthenticationFilter类中setDetails方法的源码,如下所示:

1
2
3
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

可以发现此处调用了authenticationDetailsSourcebuildDetails()方法,查看一下源码:

1
2
3
public interface AuthenticationDetailsSource<C, T> {
T buildDetails(C var1);
}

可以发现这是一个接口,查看一下该接口的实现类:

里面有三个实现类,与登录相关的是WebAuthenticationDetailsSource类,查看一下该类的源码:

1
2
3
4
5
6
7
8
public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
public WebAuthenticationDetailsSource() {
}

public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new WebAuthenticationDetails(context);
}
}

可以看到该类实现了上述接口,里面有两个方法,一个是无参的构造方法,另一个则是buildDetails()方法,该方法用于返回一个WebAuthenticationDetails对象,查看一下这个WebAuthenticationDetails类的源码:

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
26
27
28
29
30
31
32
33
34
35
public class WebAuthenticationDetails implements Serializable {
private static final long serialVersionUID = 520L;
private final String remoteAddress;
private final String sessionId;

public WebAuthenticationDetails(HttpServletRequest request) {
this.remoteAddress = request.getRemoteAddr();
HttpSession session = request.getSession(false);
this.sessionId = session != null ? session.getId() : null;
}

private WebAuthenticationDetails(String remoteAddress, String sessionId) {
this.remoteAddress = remoteAddress;
this.sessionId = sessionId;
}

public boolean equals(Object obj) {
//省略逻辑
}

public String getRemoteAddress() {
return this.remoteAddress;
}

public String getSessionId() {
return this.sessionId;
}

public int hashCode() {
//省略逻辑
}

public String toString() {
//省略逻辑
}

可以看到该类定义了三个属性,其中remoteAddress表示请求的IP地址,sessionId则是sessionId,serialVersionUID则是序列化id,其他的则是属性的getter方法、有参的构造方法以及重写的equals、hashCode和toString方法。

注意里面还有一个传入HttpServletRequest对象的构造方法,可以看到它其实是从传入的HttpServletRequest对象中获取请求IP和session信息。

通过上面的分析,我们知道当用户登录时,那么SpringSecurity默认通过WebAuthenticationDetailsSource buildDetails方法来构建一个WebAuthenticationDetails对象,之后将其设置到UsernamePasswordAuthenticationToken对象的details属性中。

因此开发者可以在用户成功登录后,通过如下方式来获取用户IP、Session信息。这里依旧在上一篇的基础上进行操作。新建一个service包,并在里面新建一个InformationService类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
@Service
public class InformationService {
public String info(){
Authentication authentication =SecurityContextHolder.getContext().getAuthentication();
WebAuthenticationDetails details = (WebAuthenticationDetails)authentication.getDetails();
String remoteAddress = details.getRemoteAddress();
String sessionId = details.getSessionId();
return String.format("remoteAddress is:"+remoteAddress+",sessionId is:"+sessionId);
}
}

这段代码就是使用SecurityContextHolder.getContext().getAuthentication()方法来获取一个Authentication对象,之后调用getDetails()方法来获取WebAuthenticationDetails对象,最后通过这个WebAuthenticationDetails对象中的etRemoteAddress()getSessionId()方法来获取用户IP、Session信息。

最后在controller包内新建一个InformationController类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
@RestController
public class InformationController {
@Autowired
private InformationService informationService;

@GetMapping("/info")
public String info(){
return informationService.info();
}
}

之后启动项目,用户登录成功后,访问/info接口即可看到页面输出所示信息:

这里的用户IP为0:0:0:0:0:0:0:1,其实就是127.0.0.1

自定义WebAuthenticationDetails对象

通过前面的学习,我们知道WebAuthenticationDetails类中仅仅只包含了请求IP和SessionId,假如我们想保存更多关于HTTP请求的信息,此时就可以通过自定义WebAuthenticationDetails类来实现。

请注意,开发者仅仅只定义WebAuthenticationDetails类是不够的,还记得那个setDetails()方法么,再次查看它的源码:

1
2
3
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

可以看到它调用的是authenticationDetailsSource.buildDetails()方法,而AuthenticationDetailsSource是一个接口,它有3个实现类,系统默认调用的是WebAuthenticationDetailsSource这个类,因此还需要自定义WebAuthenticationDetailsSource类才行。

接下来结合之前《自定义认证逻辑》一文来学习如何自定义WebAuthenticationDetails对象。

在《自定义认证逻辑》一文中,我们是在MyAuthenticationProvider类进行验证码的逻辑判断,查看一下当时的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//获取当前请求
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取当前响应
//HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();

//获取用户通过表单输入的验证码字符串
String formCaptcha =request.getParameter("code");
//获取生成的验证码字符串(从session中获取)
String genCaptcha = (String) request.getSession().getAttribute("verify_code");

if(formCaptcha ==null || genCaptcha ==null || !formCaptcha.equals(genCaptcha)){
throw new AuthenticationServiceException("验证码错误!");
}
super.additionalAuthenticationChecks(userDetails, authentication);
}
}

其实这个验证码的逻辑也可以放在自定义的WebAuthenticationDetails类中进行,之后只需用户在进行登录认证时调用这个自定义的WebAuthenticationDetailsSource类,这样自定义的WebAuthenticationDetails类也会被调用。

第一步,新建一个details包,并在里面新建一个MyWebAuthenticationDetails类,注意这个类需要继承WebAuthenticationDetails类,并实现其中有参的构造方法,里面的代码如下所示:

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
26
27
28
29
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
//自定义一个属性,用于判断验证码是否通过
private boolean isPassed = false;

//自定义请求方法属性
private String method;

public MyWebAuthenticationDetails(HttpServletRequest request) {
super(request);
//获取用户通过表单输入的验证码字符串
String formCaptcha =request.getParameter("code");
//获取生成的验证码字符串(从session中获取)
String genCaptcha = (String) request.getSession().getAttribute("verify_code");
if(formCaptcha !=null && genCaptcha !=null && formCaptcha.equals(genCaptcha)){
isPassed =true;
}

//给此处的自定义请求方法属性赋值
this.method =request.getMethod();
}

public boolean isPassed(){
return this.isPassed;
}

public String getMethod(){
return this.method;
}
}

可以看到我们自定义了一个MyWebAuthenticationDetails类,并继承了WebAuthenticationDetails类,且需要提供一个包含HttpServletRequest的参数,因此开发者可以直接利用该对象来进行验证码的判断,并将判断结果存入isPassed变量。

如果开发者想扩展属性,那么只需在这个自定义的MyWebAuthenticationDetails类中定义更多的属性,之后再从HttpServletRequest对象中取出对应的属性,并将值赋值给它,这样在后续用户成功登陆后就可以获取这些属性。

上面我们就自定义了一个method属性,那么后期就将其输出在页面上。

第二步,在details包内新建一个MyWebAuthenticationDetailsSource类,注意这个类需要继承WebAuthenticationDetailsSource类,并实现其中的buildDetails方法,之后让这个方法返回之前我们自定义的WebAuthenticationDetails对象,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MyWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
public MyWebAuthenticationDetailsSource() {
super();
}

@Override
public MyWebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new MyWebAuthenticationDetails(context);
}
}

第三步,修改MyAuthenticationProvider类中的代码。前面我们已经将验证码的判断逻辑放在了自定义的MyWebAuthenticationDetails类中,因此此处MyAuthenticationProvider中的additionalAuthenticationChecks()方法只需根据它返回的isPassed值来判断验证码是否通过验证:

1
2
3
4
5
6
7
8
9
10
11
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

//获取登录额外信息
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if(!((MyWebAuthenticationDetails)authentication.getDetails()).isPassed()){
throw new AuthenticationServiceException("验证码错误!");
}
super.additionalAuthenticationChecks(userDetails, authentication);
}
}

第四步,使用自定义的MyWebAuthenticationDetailsSource来代替系统默认的WebAuthenticationDetailsSource。其实非常简单,开发者只需在SecurityConfig类的configure(HttpSecurity http)方法中新增如下代码:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private MyWebAuthenticationDetailsSource myWebAuthenticationDetailsSource;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
....
.formLogin()
.authenticationDetailsSource(myWebAuthenticationDetailsSource)
...
}

这里将用户自定义的MyWebAuthenticationDetailsSource对象注入到SecurityConfig类中,之后在formLogin中配置authenticationDetailsSource,并将自定义的MyWebAuthenticationDetailsSource对象传入进去。

第五步,修改InformationService类中的代码,在里面新增一个customizeInfo方法,里面的代码如下:

1
2
3
4
5
6
7
8
public String customizeInfo(){
Authentication authentication =SecurityContextHolder.getContext().getAuthentication();
MyWebAuthenticationDetails details = (MyWebAuthenticationDetails)authentication.getDetails();
String remoteAddress = details.getRemoteAddress();
String sessionId = details.getSessionId();
String method = details.getMethod();
return String.format("remoteAddress is:"+remoteAddress+",sessionId is:"+sessionId+",method is:"+method);
}

第六步,修改InformationController类中的代码,在里面新增一个customizeInfo方法,里面的代码如下:

1
2
3
4
@GetMapping("/customizeInfo")
public String customizeInfo(){
return informationService.customizeInfo();
}

第七步,启动项目,开始进行测试。启动项目,用户登录成功后,访问/customizeInfo接口即可看到页面输出所示信息:

这样本篇关于获取登录额外信息的学习就到此为止,后续学习其他内容。