述
上文中实现了验证码发送接口的开发,以及重构, 本文再来看一下如何实现短信验证码的登录
先来回顾一下之前的用户名密码的登录认证流程
然后我们要做一个短信认证的流程,就仿照这个流程来就ok了
基本思路
跟之前的用户名密码的登录流程是一样的
- 提供一个校验验证码的filter
- 跟他的
UsernamePasswordAuthenticationFilter
一样我们写一个SmsAuthenticationFilter
这里会创建一个SmsAuthenticationToken
- 交给
AuthenticationManager
去找认证器,所以我们这里需要提供一个SmsAuthenticationProvider
实现自己的认证 - 最后调用
UserDetailsService
返回用户的认证对象
基本流程就是这样,然后图中橘色部分的是需要我们新建自己去写的
代码实现
首先第一个校验验证码的Filter,这个先跳过,最后再写
SmsAuthenticationFilter
这个和 UsernamePasswordAuthenticationFilter
的作用其实是一样的,就是组装一个未认证的token对象, 这个token对象也需要我们自己去创建
新建两个类, SmsAuthenticationFilter
和 SmsAuthenticationToken
首先把这个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
62public 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
99public 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,我们这个类能不能处理
总结
认证流程:
ValidateCodeFilter
去验证验证码SmsAuthenticationFilter
这个filter去创建一个未经过认证的token- 上面未认证的token对象会通过
AuthenticationManager
找到支持处理这个token的类 SmsAuthenticationProvider
去调用UserDetailsService
去查询用户的信息- 最后返回一个已认证的token
这里只要搞清楚认证的流程, 就仿照去实现一套就好了