《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》

摘要: 原创出处 http://niocoder.com/2018/01/14/Spring-Security源码分析五-Spring-Security短信登录/ 「龙飞」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

目前常见的社交软件、购物软件、支付软件、理财软件等,均需要用户进行登录才可享受软件提供的服务。目前主流的登录方式主要有 3 种:账号密码登录、短信验证码登录和第三方授权登录。我们已经实现了账号密码和第三方授权登录。本章我们将使用Spring Security实现短信验证码登录。

1. 概述

Spring Security源码分析一:Spring Security认证过程Spring Security源码分析二:Spring Security授权过程两章中。我们已经详细解读过Spring Security如何处理用户名和密码登录。(其实就是过滤器链)本章我们将仿照用户名密码来显示短信登录。

1.1 目录结构

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Security-sms.png

1.2 SmsCodeAuthenticationFilter

SmsCodeAuthenticationFilter对应用户名密码登录的UsernamePasswordAuthenticationFilter同样继承AbstractAuthenticationProcessingFilter

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

/**
* request中必须含有mobile参数
*/
private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;
/**
* post请求
*/
private boolean postOnly = true;

protected SmsCodeAuthenticationFilter() {
/**
* 处理的手机验证码登录请求处理url
*/
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_MOBILE, "POST"));
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//判断是是不是post请求
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(未认证)
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

//设置用户信息
setDetails(request, authRequest);
//返回Authentication实例
return this.getAuthenticationManager().authenticate(authRequest);
}

/**
* 获取手机号
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}

protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

public void setMobileParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.mobileParameter = usernameParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getMobileParameter() {
return mobileParameter;
}
}

  1. 认证请求的方法必须为POST
  2. 从request中获取手机号
  3. 封装成自己的Authenticaiton的实现类SmsCodeAuthenticationToken(未认证)
  4. 调用 AuthenticationManagerauthenticate 方法进行验证(即SmsCodeAuthenticationProvider

1.3 SmsCodeAuthenticationToken

SmsCodeAuthenticationToken对应用户名密码登录的UsernamePasswordAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 2383092775910246006L;

/**
* 手机号
*/
private final Object principal;

/**
* SmsCodeAuthenticationFilter中构建的未认证的Authentication
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}

/**
* SmsCodeAuthenticationProvider中构建已认证的Authentication
* @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
}

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

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

/**
* @param isAuthenticated
* @throws IllegalArgumentException
*/
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();
}
}

1.4 SmsCodeAuthenticationProvider

SmsCodeAuthenticationProvider对应用户名密码登录的DaoAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

private UserDetailsService userDetailsService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
//调用自定义的userDetailsService认证
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//如果user不为空重新构建SmsCodeAuthenticationToken(已认证)
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

authenticationResult.setDetails(authenticationToken.getDetails());

return authenticationResult;
}

/**
* 只有Authentication为SmsCodeAuthenticationToken使用此Provider认证
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}

public UserDetailsService getUserDetailsService() {
return userDetailsService;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}

1.4.1 SmsCodeAuthenticationSecurityConfig短信登录配置

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

@Autowired
private AuthenticationFailureHandler merryyouAuthenticationFailureHandler;

@Autowired
private UserDetailsService userDetailsService;

@Override
public void configure(HttpSecurity http) throws Exception {
//自定义SmsCodeAuthenticationFilter过滤器
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(merryyouAuthenticationFailureHandler);

//设置自定义SmsCodeAuthenticationProvider的认证器userDetailsService
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
//在UsernamePasswordAuthenticationFilter过滤前执行
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

1.5 MerryyouSecurityConfig 主配置文件

 @Override
protected void configure(HttpSecurity http) throws Exception {
// http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
http
.formLogin()//使用表单登录,不再使用默认httpBasic方式
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//如果请求的URL需要认证则跳转的URL
.loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//处理表单中自定义的登录URL
.and()
.apply(validateCodeSecurityConfig)//验证码拦截
.and()
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.apply(merryyouSpringSocialConfigurer)//社交登录
.and()
.rememberMe()
......

1.6 调试过程

短信登录拦截请求/authentication/mobile

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/spring-sms-01.png

自定义SmsCodeAuthenticationProvider http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/spring-sms-02.png

效果如下: http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/121.gif.gif

2. 代码下载

从我的 github 中下载,https://github.com/longfeizheng/logback

文章目录
  1. 1. 1. 概述
    1. 1.1. 1.1 目录结构
    2. 1.2. 1.2 SmsCodeAuthenticationFilter
    3. 1.3. 1.3 SmsCodeAuthenticationToken
    4. 1.4. 1.4 SmsCodeAuthenticationProvider
      1. 1.4.1. 1.4.1 SmsCodeAuthenticationSecurityConfig短信登录配置
    5. 1.5. 1.5 MerryyouSecurityConfig 主配置文件
    6. 1.6. 1.6 调试过程
  2. 2. 2. 代码下载