Spring Security-11-实现短信登录流程

上文中实现了验证码发送接口的开发,以及重构, 本文再来看一下如何实现短信验证码的登录

先来回顾一下之前的用户名密码的登录认证流程
image

然后我们要做一个短信认证的流程,就仿照这个流程来就ok了

基本思路

image

跟之前的用户名密码的登录流程是一样的

  1. 提供一个校验验证码的filter
  2. 跟他的 UsernamePasswordAuthenticationFilter 一样我们写一个 SmsAuthenticationFilter 这里会创建一个 SmsAuthenticationToken
  3. 交给 AuthenticationManager 去找认证器,所以我们这里需要提供一个 SmsAuthenticationProvider 实现自己的认证
  4. 最后调用 UserDetailsService 返回用户的认证对象

基本流程就是这样,然后图中橘色部分的是需要我们新建自己去写的

代码实现

首先第一个校验验证码的Filter,这个先跳过,最后再写

SmsAuthenticationFilter

这个和 UsernamePasswordAuthenticationFilter 的作用其实是一样的,就是组装一个未认证的token对象, 这个token对象也需要我们自己去创建

新建两个类, SmsAuthenticationFilterSmsAuthenticationToken

首先把这个token先建起来, 名称 SmsAuthenticationToken , 仿照 UsernamePasswordAuthenticationToken 直接把他的代码都复制过来然后改改

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


private final Object principal;


/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}

/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}


@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return this.principal;
}

@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

UsernamePasswordAuthenticationToken 的区别就是去掉了 credentials 这个属性,这个是存密码的 我们这里没用就去掉了

同理 SmsAuthenticationFilter 中仿照 UsernamePasswordAuthenticationFilter 的代码, 直接全复制过来改改就可以了,如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

/**
* 表单中传过来的参数名称
*/
public static final String SPRING_SECURITY_FORM_PHONE_NO_KEY = "phoneNo";

private String phoneNoParameter = SPRING_SECURITY_FORM_PHONE_NO_KEY;

/**
* 是否只支持POST请求
*/
private boolean postOnly = true;


public SmsAuthenticationFilter() {
// 设置表单提交的路径
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}


@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String phoneNo = obtainPhoneNo(request);

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

phoneNo = phoneNo.trim();

SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phoneNo);

// 设置一些请求的详细信息
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
*
* @param request so that request attributes can be retrieved
*
* @return the username that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
protected String obtainPhoneNo(HttpServletRequest request) {
return request.getParameter(phoneNoParameter);
}

/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*
* @param phoneNoParameter the parameter name. Defaults to "phoneNo".
*/
public void setPhoneNoParameter(String phoneNoParameter) {
Assert.hasText(phoneNoParameter, "phoneNo parameter must not be empty or null");
this.phoneNoParameter = phoneNoParameter;
}

/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getPhoneNoParameter() {
return phoneNoParameter;
}

}

就是去掉了 UsernamePasswordAuthenticationFilter 中的用户名和密码,改成手机号了

SmsAuthenticationProvider

这里是具体的认证,新建类 SmsAuthenticationProvider ,代码如下:

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
@Data
public class SmsAuthenticationProvider implements AuthenticationProvider {

private UserDetailsService userDetailsService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;

// 根据手机号去取密码
UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

if (userDetails == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}

// 如果找到用户信息了,就给一个新的认证过的token
SmsAuthenticationToken SmsAuthenticationSuccessToken = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
// 请求的详细信息从旧的哪里拿出来放进去
SmsAuthenticationSuccessToken.setDetails(authenticationToken.getDetails());
return SmsAuthenticationSuccessToken;
}

/**
* 判断传进来的token (authentication对象) 是否支持处理
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}

这里就是根据未认证的token里面的信息(这里是手机号), 通过 UserDetailsService 去查询用户的信息,如果没有查到就抛出一个异常,如果找到了,就新建一个已经认证的过的token,然后返回回去

supports() 方法,就是判断传进来的这个token,我们这个类能不能处理

总结

认证流程:

  1. ValidateCodeFilter 去验证验证码
  2. SmsAuthenticationFilter 这个filter去创建一个未经过认证的token
  3. 上面未认证的token对象会通过 AuthenticationManager 找到支持处理这个token的类
  4. SmsAuthenticationProvider 去调用 UserDetailsService 去查询用户的信息
  5. 最后返回一个已认证的token

这里只要搞清楚认证的流程, 就仿照去实现一套就好了

本文代码传送门