可以前往第一篇博客查看目录结构 --> 这里
一、在core模块下创建authentication.mobile包
二、参考UsernamePasswordAuthenticationToken自定义一个SmsCodeAuthenticationToken
package com.zeke.core.authentication.mobile;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
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 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 getCredentials() {
return null;
}
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 void eraseCredentials() {
super.eraseCredentials();
}
}
三、参考UsernamePasswordAuthenticationFilter自定义一个SmsCodeAuthenticationFilter
package com.zeke.core.authentication.mobile;
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;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String ZEKE_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = ZEKE_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "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);
}
/**
* 获取手机号
*/
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 mobileParameter the parameter name. Defaults to "username".
*/
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username 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;
}
}
四、创建SmsCodeAuthenticationProvider完成短信验证登录逻辑
package com.zeke.core.authentication.mobile;
import org.springframework.beans.factory.annotation.Autowired;
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 userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//传进来的authentication就是我们自定义的SmsCodeAuthenticationToken,强转
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
//Principal就是手机号码,根据手机号码获取用户信息
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//生成已认证Token
SmsCodeAuthenticationToken smsCodeAuthenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
//吧authenticationToken中未认证的Details放到smsCodeAuthenticationResult的Details中认证
smsCodeAuthenticationResult.setDetails(authenticationToken.getDetails());
return smsCodeAuthenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
五、把SmsCodeAuthenticationFilter、SmsCodeAuthenticationProvider、SmsCodeAuthenticationToken连接起来,配置到Security里面去
package com.zeke.core.authentication.mobile;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.stereotype.Component;
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler zekeAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler zekeAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
//new一个filter
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
//filter调用authenticationManager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(zekeAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(zekeAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
六、在Browser模块下的BrowserSecurityConfig中把SmsCodeAuthenticationSecurityConfig 'apply'进入configure方法中
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
.and()
.apply(smsCodeAuthenticationSecurityConfig);
七、把core模块下的ValidateCodeFilter(图形验证码的过滤器)复制一份,改名为SmsCodeFilter用作短信验证码的过滤器(这个地方实际生产需要重构,有重复代码,本人懒~)
package com.imooc.security.core.validate.sms;
import com.imooc.security.core.properties.SecurityProperties;
import com.imooc.security.core.validate.ValidateCode;
import com.imooc.security.core.validate.ValidateCodeException;
import com.imooc.security.core.validate.image.ImageCode;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import static com.imooc.security.core.validate.ValidateCodeProcessor.SESSION_KEY_PREFIX;
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 存放所有需要拦截的URL
*/
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(),",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
urls.add("/authentication/mobile");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (pathMatcher.match(url,request.getRequestURI())){
action = true;
}
}
if (action){
try {
validate(new ServletWebRequest(request));
}
catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
/**
* 校验提交验证码的合法性
* @param request
* @throws ServletRequestBindingException
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
String type = SESSION_KEY_PREFIX + "SMS";
ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request,type);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if (StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()){
sessionStrategy.removeAttribute(request,type);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request,type);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
八、在browser模块下BrowserSecurityConfig的configure中添加下述代码,把第七条的filter配置进去
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
九、启动工程,访问localhost/zeke-login.html,输入错误验证码或正确验证码测试,两种登录方式