目的:在验证码的重构完成之后,已经实现了发送验证码的功能,接下来要做的就是将短信验证码登录的逻辑添加就如程序里。使用户可以以账号、密码或手机号加验证码的方式登录。
下图是短信验证码的实现逻辑:
当用户以用户名+密码的形式登录时,经过UsernamepasswordAuthenticationFilter,将用户信息封装为一个token。然后AuthenticationManager是会根据用户的登录类型,从众多的Provider中选择一个用于处理用户的登录。
要想实现短信验证码登录逻辑,需要自己实现的有SmsAuthenticationFilter和SmsAuthenticationToken以及SmsAuthenticationProvider。
要想完成这些类的实现,也很简单,模范已有的类实现就可以,以下是具体的实现代码。(短信验证码在app中也会用到,放在core模块里。)
SmsCodeAuthenticationFilter仿照UsernamepasswordAuthenticationFilter来编写,去掉有关password属性的部分。
package com.imooc.security.core.authenticate.mobile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/springSecurity/SmsCodeLogin", "POST"));
}
// ~ Methods
// ========================================================================================================
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 mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
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 mobile that will be presented in the
* <code>Authentication</code> request token to the
* <code>AuthenticationManager</code>
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
/**
* 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, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the
* login request.
*
* @param usernameParameter the parameter name. Defaults to "username".
*/
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
/**
* 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 getMobileParameter() {
return mobileParameter;
}
}
可以自己看看UsernamepasswordAuthenticationFilter的源码,比较二者的不同部分。
SmsCodeAuthenticationToken仿照UsernamepasswordAuthenticationToken来编写。
package com.imooc.security.core.authenticate.mobile;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* 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 SmsCodeAuthenticationToken(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 credentials
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
public Object getPrincipal() {
return this.principal;
}
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 Object getCredentials() {
// TODO Auto-generated method stub
return null;
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
SmsCodeAuthenticationProvider实现AuthenticationProvider接口,并实现接口中的方法。
package com.imooc.security.core.authenticate.mobile;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsServiceer;
public UserDetailsService getUserDetailsServiceer() {
return userDetailsServiceer;
}
public void setUserDetailsServiceer(UserDetailsService userDetailsServiceer) {
this.userDetailsServiceer = userDetailsServiceer;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//处理短信验证码验证
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
UserDetails user = userDetailsServiceer.loadUserByUsername((String) authenticationToken.getPrincipal());
if(user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
authenticate方法处理验证的逻辑。supports方法则是表明该provider支持哪种认证方式。将UserDetailsService注入进来,处理用户的登录判定。(短信验证码的验证是在这之外完成的,一定要清楚,这里处理的只是手机号的验证!)
这些类定义完成之后,对其进行配置,再将其加入到SpringSecurity的过滤器链中。
定义一个配置类,名字叫做SmsCodeAuthenticationSecurityConfig
package com.imooc.security.core.validate.sms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.imooc.security.core.authenticate.mobile.SmsCodeAuthenticationFilter;
import com.imooc.security.core.authenticate.mobile.SmsCodeAuthenticationProvider;
@Configuration
public class SmsCodeAuthenticationSecurityConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsServiceer;
@Override
public void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsServiceer(userDetailsServiceer);
http
.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
这个配置类的作用是,将上面所定义的类整合起来,串联起来。
最后一步就是在BrowserSecurityConfig中,将这个配置类引入,使其在SpringSecurity中生效起作用。将其注入进来之后,只需在最后加入下图代码即可。
至此,关于短信验证码的开发配置完成。