在开发短信验证码接口

在之前图片验证码的基础上开发短信验证码

验证码实体SmsCode

  1. 短信验证码和图片验证码就差一个图片属性,直接把ImageCode拿来改成SmsCode并去掉 private BufferedImage image; 属性就行

  2. 因此可以使ImageCode继承SmsCode

  3. 最后把SmsCode名字改为ValidateCode比较好

  4. 验证码生成器ValidateCodeGenerator的返回值从ImageCode改为ValidateCode

手机验证码发送接口

每个人的短信验证码发送服务商都不同,应该让他们自己实现

SmsCodeSender

public interface SmsCodeSender {

    void send(String mobile,String code);

}

SmsCodeSender默认实现DefaultSmsCodeSender

public class DefaultSmsCodeSender implements SmsCodeSender {
    @Override
    public void send(String mobile, String code) {

        System.out.println("向手机发送短信验证码"+mobile+"---"+code);

    }
}

默认实现应该是让使用者覆盖掉的 同图片验证码生成器接口一样
配置如下
ValidateCodeBeanConfig

@Configuration
public class ValidateCodeBeanConfig {
	
	@Autowired
	private SecurityProperties securityProperties;
	
	@Bean
	@ConditionalOnMissingBean(name = "imageCodeGenerator")
	public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字
		ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
		codeGenerator.setSecurityProperties(securityProperties);
		return codeGenerator;
	}

	@Bean
//	@ConditionalOnMissingBean(name = "smsCodeSender")
	@ConditionalOnMissingBean(SmsCodeSender.class)//当容器中找到了SmsCodeSender的实现就不会再用此实现bean
	public SmsCodeSender smsCodeSender() { //方法的名字就是spring容器中bean的名字
		return new DefaultSmsCodeSender();
	}

}

手机验证码生成接口

类似于图像验证码生成接口

ValidateCodeController


    @Autowired
    private ValidateCodeGenerator smsCodeGenerator;//注入手机验证码创建接口

    @Autowired
    private SmsCodeSender smsCodeSender; //注入手机验证码发送器

    @RequestMapping("/code/sms")
    private void createSms(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
        //1根据请求中的随机数生成图片
        ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
        //2将随机数放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode);
        //3、这块应该由短信服务商将我们的短信发送出去,我们需要封装一个短信验证码发送的接口
        String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
        smsCodeSender.send(mobile,smsCode.getCode());

    }

SmsCodeGenerator

手机验证码的长度和过期时间做成可配置的属性,具体方法和图片验证码的创建相似,这里就不介绍了

@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

	@Autowired
	private SecurityProperties securityProperties;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.
	 * springframework.web.context.request.ServletWebRequest)
	 */
	@Override
	public ValidateCode generate(ServletWebRequest request) {
		String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
		return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
	}

	public SecurityProperties getSecurityProperties() {
		return securityProperties;
	}

	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}
	
	

}

不拦截短信验证码路径

BrowserSecurityConfig

···························
  			.and()
                .authorizeRequests() //对请求授权
//                .antMatchers("/signIn.html","/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证
                .antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/*").permitAll() //加一个匹配器 对匹配的路径不进行身份认证
              
              ···························

登录页面

在这里插入图片描述

<h3>短信登录</h3>
<form action="/authentication/mobile" method="post">
    <table>
        <tr>
            <td>手机号:</td>
            <td><input type="text" name="mobile" value="13012345678"></td>
        </tr>
        <tr>
            <td>短信验证码:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/code/sms?mobile=13012345678">发送验证码</a>
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>

测试

在这里插入图片描述
在这里插入图片描述

重构

现在我们的验证码生成器有两个实现的接口

@Autowired
private ValidateCodeGenerator imageCodeGenerator;

@Autowired
private ValidateCodeGenerator smsCodeGenerator;

使用模板方法重构,重构后的结构如下
在这里插入图片描述

校验码处理接口 ValidateCodeProcessor,封装不同校验码的处理逻辑

/**
 * 校验码处理器,封装不同校验码的处理逻辑
 * 
 * @author zhailiang
 *
 */
public interface ValidateCodeProcessor {

	/**
	 * 验证码放入session时的前缀
	 */
	String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";

	/**
	 * 创建校验码
	 * 
	 * @param request
	 * @throws Exception
	 * ServletWebRequest spring提供的一个工具类  可以封装请求和响应
	 */
	void create(ServletWebRequest request) throws Exception;

}

抽象实现 AbstractValidateCodeProcessor

public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {

	/**
	 * 操作session的工具类
	 */
	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
	/**
	 * 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
	 *
	 * 这个map的注入
	 * spring启动的时候,会查找map的value接口ValidateCodeGenerator的所有实现bean,
	 * 并把这个bean为value,bean的名称为key存入map中
	 *
	 * 这种行为叫依赖搜索
	 */
	@Autowired
	private Map<String, ValidateCodeGenerator> validateCodeGenerators;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.whale.security.core.validate.code.ValidateCodeProcessor#create(org.
	 * springframework.web.context.request.ServletWebRequest)
	 */
	@Override
	public void create(ServletWebRequest request) throws Exception {
		C validateCode = generate(request);//生成
		save(request, validateCode);//保存
		send(request, validateCode);//发送 这是一个抽象方法 需要子类去实现
	}

	/**
	 * 生成校验码
	 * 
	 * @param request
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private C generate(ServletWebRequest request) {
		String type = getProcessorType(request);
		String generatorName = type + "CodeGenerator";
		ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
		if (validateCodeGenerator == null) {
			throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
		}
		return (C) validateCodeGenerator.generate(request);
	}

	/**
	 * 保存校验码
	 *
	 * @param request
	 * @param validateCode
	 */
	private void save(ServletWebRequest request, C validateCode) {
		sessionStrategy.setAttribute(request, getSessionKey(request), validateCode);
	}

	/**
	 * 构建验证码放入session时的key
	 *
	 * @param request
	 * @return
	 */
	private String getSessionKey(ServletWebRequest request) {
		return SESSION_KEY_PREFIX + getProcessorType(request);
	}

	/**
	 * 发送校验码,由子类实现
     * 它的抽象方法 send由具体的子类实现
	 * 
	 * @param request
	 * @param validateCode
	 * @throws Exception
	 */
	protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;

    /**
     * 根据请求的url获取校验码的类型
     * @param request
     * @return
     */
	private String getProcessorType(ServletWebRequest request){
        String type = StringUtils.substringAfter(request.getRequest().getRequestURI(), "/code/");
        return type;

    }
}

两个子类分别实现发送功能 ImageCodeProcessor SmsCodeProcessor

ImageCodeProcessor

@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {

	/**
	 * 发送图形验证码,将其写到响应中
	 */
	@Override
	protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
		ImageIO.write(imageCode.getImage(), "JPEG", request.getResponse().getOutputStream());
	}

}

SmsCodeProcessor

@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {

	/**
	 * 短信验证码发送器
	 */
	@Autowired
	private SmsCodeSender smsCodeSender;
	
	@Override
	protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
//		String mobile= ServletRequestUtils.getStringParameter((ServletRequest) request, "mobile");
//		String mobile= ServletRequestUtils.getRequiredStringParameter((ServletRequest)request,"mobile");
		String mobile= ServletRequestUtils.getRequiredStringParameter(request.getRequest(),"mobile");
		smsCodeSender.send(mobile, validateCode.getCode());
	}

}

ValidateCodeController 简化

@RestController
public class ValidateCodeController implements Serializable {

    public static  final  String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key

    @Autowired
    private Map<String, ValidateCodeProcessor> validateCodeProcessors;

    /**
     * 创建验证码,根据验证码类型不同,调用不同的 {@link ValidateCodeProcessor}接口实现
     *
     * @param request
     * @param response
     * @param type
     * @throws Exception
     */
    @GetMapping("/code/{type}")
    public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
            throws Exception {
        validateCodeProcessors.get(type+"CodeProcessor").create(new ServletWebRequest(request,response));
        }
      }


短信登录开发

在这里插入图片描述
在这里插入图片描述

SmsCodeAuthenticationToken

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(String mobile) {
		super(null);
		this.principal = mobile;
		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
	 * @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
	// ========================================================================================================

	@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();
	}
}

SmsCodeAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

	private UserDetailsService userDetailsService;

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.authentication.AuthenticationProvider#
	 * authenticate(org.springframework.security.core.Authentication)
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

		String principal = (String) authenticationToken.getPrincipal();//token中的手机号
		UserDetails user = userDetailsService.loadUserByUsername(principal);//根据手机号拿到对应的UserDetails

		if (user == null) {
			throw new InternalAuthenticationServiceException("无法获取用户信息");
		}
		
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
		
		authenticationResult.setDetails(authenticationToken.getDetails());

		return authenticationResult;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.authentication.AuthenticationProvider#
	 * supports(java.lang.Class)
	 *
	 * AuthenticationManager 判断参数authentication是不是对应的token
	 */
	@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

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================

	private static String WHALE_FROM_MOBILE_KEY="mobile";

	private String mobileParameter = WHALE_FROM_MOBILE_KEY;
	private boolean postOnly = true;//当前处理器是否处理post请求

	// ~ Constructors
	// ===================================================================================================

	/**
	 * 当前过滤器处理的请求是什么
	 * 一个路径匹配器  手机表单登录的一个请求
	 */
	public SmsCodeAuthenticationFilter() {
		super(new AntPathRequestMatcher("/authentication/mobile", "POST"));

	}

	// ~ Methods
	// ========================================================================================================

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			//当前请求如果不是post请求则抛出异常
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}

		String mobile = obtainMobile(request);//从请求中获取mobile参数

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

		mobile = mobile.trim();

		//实例化token
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);


		//使用AuthenticationManager 调用 token
		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 usernameParameter
	 *            the parameter name. Defaults to "username".
	 */
	public void setMobileParameter(String usernameParameter) {
		Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
		this.mobileParameter = usernameParameter;
	}


	/**
	 * 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;
	}

}

SmsCodeAuthenticationSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
	
	@Autowired
	private AuthenticationSuccessHandler whaleAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler whaleAuthenticationFailureHandler;

	@Qualifier("myUserDetailsService")
	@Autowired
	private UserDetailsService userDetailsService;

	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(whaleAuthenticationSuccessHandler);
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(whaleAuthenticationFailureHandler);
		
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
		
	}

}

SmsCodeFilter

同ImageCodeFilter

校验短信验证码并登录
重构代码

BrowserSecurityConfig

同图片验证码过滤器一样
添加短信验证码过滤器
在这里插入图片描述
添加配置
在这里插入图片描述

测试

ok

重构

在这里插入图片描述
1两个验证码的过滤器合并为一个
2config配置整合
3重复字符串整合SecurityConstants

项目地址
https://github.com/whaleluo/securitydemo.git

Logo

Authing 是一款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务

更多推荐