Spring Security 实现短信验证码登录功能

之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已;Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑。

开发短信验证码接口

获取验证码

短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码。

@GetMapping("/code/sms")
	public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
		// 创建验证码
		ValidateCode smsCode = createCodeSmsCode(request);
		// 将验证码放到session中
		sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode);
		String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
		// 发送验证码
		smsCodeSender.send(mobile, smsCode.getCode());
	}

前端代码

<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" rel="external nofollow" >发送验证码</a>
				</td>
			</tr>

短信验证码流程原理

短信验证码登录和用户名密码登录对比

步骤流程

  • 首先点击登录应该会被SmsAuthenticationFilter过滤器处理,这个过滤器拿到请求以后会在登录请求中拿到手机号,然后封装成自定义的一个SmsAuthenticationToken(未认证)。
  • 这个Token也会传给AuthenticationManager,因为AuthenticationManager整个系统只有一个,它会检索系统中所有的AuthenticationProvider,这时候我们要提供自己的SmsAuthenticationProvider,用它来校验自己写的SmsAuthenticationToken的手机号信息。
  • 在校验的过程中同样会调用UserDetailsService,把手机号传给它让它去读用户信息,去判断是否能登录,登录成功的话再把SmsAuthenticationToken标记为已认证。
  • 到这里为止就是短信验证码的认证流程,上面的流程并没有提到校验验证码信息,其实它的验证流程和图形验证码验证流程也是类似,同样是在SmsAuthenticationFilter过滤器之前加一个过滤器来验证短信验证码

代码实现

SmsCodeAuthenticationToken

  • 作用:封装认证Token
  • 实现:可以继承AbstractAuthenticationToken抽象类,该类实现了Authentication接口
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	private final Object principal;
	/**
	 * 进入SmsAuthenticationFilter时,构建一个未认证的Token
	 *
	 * @param mobile
	 */
	public SmsCodeAuthenticationToken(String mobile) {
		super(null);
		this.principal = mobile;
		setAuthenticated(false);
	}
	/**
	 * 认证成功以后构建为已认证的Token
	 *
	 * @param principal
	 * @param authorities
	 */
	public SmsCodeAuthenticationToken(Object principal,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		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();
	}
}

SmsCodeAuthenticationFilter

  • 作用:处理短信登录的请求,构建Token,把请求信息设置到Token中。
  • 实现:该类可以模仿UsernamePasswordAuthenticationFilter类,继承AbstractAuthenticationProcessingFilter抽象类
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private String mobileParameter = "mobile";
	private boolean postOnly = true;
 /**
 * 表示要处理的请求路径
 */
	public SmsCodeAuthenticationFilter() {
 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 mobile = obtainMobile(request);
		if (mobile == null) {
			mobile = "";
		}
		mobile = mobile.trim();
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
		// 把请求信息设到Token中
		setDetails(request, authRequest);
		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;
	}
}

SmsAuthenticationProvider

  • 作用:提供认证Token的校验逻辑,配置为能够支持SmsCodeAuthenticationToken的校验
  • 实现:实现AuthenticationProvider接口,实现其两个方法。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
	private UserDetailsService userDetailsService;
 /**
 * 进行身份认证的逻辑
 *
 * @param authentication
 * @return
 * @throws AuthenticationException
 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

		UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
		if (user == null) {
			throw new InternalAuthenticationServiceException("无法获取用户信息");
		}

		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}
 /**
 * 表示支持校验的Token,这里是SmsCodeAuthenticationToken
 *
 * @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;
	}
}

ValidateCodeFilter

  • :校验短信验证码
  • 实现:和图形验证码类似,继承OncePerRequestFilter接口防止多次调用,主要就是验证码验证逻辑,验证通过则继续下一个过滤器。
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
	/**
	 * 验证码校验失败处理器
	 */
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	/**
	 * 系统配置信息
	 */
	@Autowired
	private SecurityProperties securityProperties;
	/**
	 * 系统中的校验码处理器
	 */
	@Autowired
	private ValidateCodeProcessorHolder validateCodeProcessorHolder;
	/**
	 * 存放所有需要校验验证码的url
	 */
	private Map<String, ValidateCodeType> urlMap = new HashMap<>();
	/**
	 * 验证请求url与配置的url是否匹配的工具类
	 */
	private AntPathMatcher pathMatcher = new AntPathMatcher();
	/**
	 * 初始化要拦截的url配置信息
	 */
	@Override
	public void afterPropertiesSet() throws ServletException {
		super.afterPropertiesSet();
		urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
		addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
	}
	/**
	 * 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
	 *
	 * @param urlString
	 * @param type
	 */
	protected void addUrlToMap(String urlString, ValidateCodeType type) {
		if (StringUtils.isNotBlank(urlString)) {
			String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
			for (String url : urls) {
				urlMap.put(url, type);
			}
		}
	}
	/**
	 * 验证短信验证码
	 *
	 * @param request
	 * @param response
	 * @param chain
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		ValidateCodeType type = getValidateCodeType(request);
		if (type != null) {
			logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
			try {
				// 进行验证码的校验
				validateCodeProcessorHolder.findValidateCodeProcessor(type)
						.validate(new ServletWebRequest(request, response));
				logger.info("验证码校验通过");
			} catch (ValidateCodeException exception) {
				// 如果校验抛出异常,则交给我们之前文章定义的异常处理器进行处理
				authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
				return;
			}
		}
		// 继续调用后边的过滤器
		chain.doFilter(request, response);
	}
	/**
	 * 获取校验码的类型,如果当前请求不需要校验,则返回null
	 *
	 * @param request
	 * @return
	 */
	private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
		ValidateCodeType result = null;
		if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) {
			Set<String> urls = urlMap.keySet();
			for (String url : urls) {
				if (pathMatcher.match(url, request.getRequestURI())) {
					result = urlMap.get(url);
				}
			}
		}
		return result;
	}
}

添加配置

SmsCodeAuthenticationSecurityConfig

作用:配置SmsCodeAuthenticationFilter,后面需要把这些配置加到主配置类BrowserSecurityConfig

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

	@Autowired
	private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;

	@Autowired
	private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;

	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private PersistentTokenRepository persistentTokenRepository;

	@Override
	public void configure(HttpSecurity http) throws Exception {

		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		// 设置AuthenticationManager
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		// 设置登录成功处理器
		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
		// 设置登录失败处理器
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
		String key = UUID.randomUUID().toString();
		smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository));

		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		// 将自己写的Provider加到Provider集合里去
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
	}
}

BrowserSecurityConfig

作用:主配置类;添加短信验证码配置类、添加SmsCodeAuthenticationSecurityConfig配置

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	@Autowired
	private SecurityProperties securityProperties;
	@Autowired
	private DataSource dataSource;
	@Autowired
	private UserDetailsService userDetailsService;
	@Autowired
	private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
	@Autowired
	private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
	@Autowired
	private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 验证码校验过滤器
		ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
		// 将验证码校验过滤器加到 UsernamePasswordAuthenticationFilter 过滤器之前
		http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
				.formLogin()
				// 当用户登录认证时默认跳转的页面
				.loginPage("/authentication/require")
				// 以下这行 UsernamePasswordAuthenticationFilter 会知道要处理表单的 /authentication/form 请求,而不是默认的 /login
				.loginProcessingUrl("/authentication/form")
				.successHandler(meicloudAuthenticationSuccessHandler)
				.failureHandler(meicloudAuthenticationFailureHandler)
				// 配置记住我功能
				.and()
				.rememberMe()
				// 配置TokenRepository
				.tokenRepository(persistentTokenRepository())
				// 配置Token过期时间
				.tokenValiditySeconds(3600)
				// 最终拿到用户名之后,使用UserDetailsService去做登录
				.userDetailsService(userDetailsService)
				.and()
				.authorizeRequests()
				// 排除对 "/authentication/require" 和 "/meicloud-signIn.html" 的身份验证
				.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll()
				// 表示所有请求都需要身份验证
				.anyRequest()
				.authenticated()
				.and()
				.csrf().disable()// 暂时把跨站请求伪造的功能关闭掉
				// 相当于把smsCodeAuthenticationSecurityConfig里的配置加到上面这些配置的后面
				.apply(smsCodeAuthenticationSecurityConfig);
	}
	/**
	 * 记住我功能的Token存取器配置
	 *
	 * @return
	 */
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
		tokenRepository.setDataSource(dataSource);
		// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
		tokenRepository.setCreateTableOnStartup(true);
		return tokenRepository;
	}
}

总结

到此这篇关于Spring Security 实现短信验证码登录功能的文章就介绍到这了,更多相关spring security 验证码登录内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Security Oauth2.0 实现短信验证码登录示例

    本文介绍了Spring Security Oauth2.0 实现短信验证码登录示例,分享给大家,具体如下: 定义手机号登录令牌 /** * @author lengleng * @date 2018/1/9 * 手机号登录令牌 */ public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecur

  • SpringBoot + SpringSecurity 短信验证码登录功能实现

    实现原理 在之前的文章中,我们介绍了普通的帐号密码登录的方式: SpringBoot + Spring Security 基本使用及个性化登录配置. 但是现在还有一种常见的方式,就是直接通过手机短信验证码登录,这里就需要自己来做一些额外的工作了. 对SpringSecurity认证流程详解有一定了解的都知道,在帐号密码认证的过程中,涉及到了以下几个类:UsernamePasswordAuthenticationFilter(用于请求参数获取),UsernamePasswordAuthentica

  • Spring Security OAuth2集成短信验证码登录以及第三方登录

    前言 基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring Boot环境下使用OAuth2.0,提供了开箱即用的组件.但是在开发过程中我们会发现由于Spring Security OAuth2的组件特别全面,这样就导致了扩展很不方便或者说是不太容易直指定扩展的方案,例如: 图片验证码登录 短信验证码登录 微信小程序登录 第三方系统登录 CAS单点登录

  • Spring Security登录添加验证码的实现过程

    登录添加验证码是一个非常常见的需求,网上也有非常成熟的解决方案,其实,要是自己自定义登录实现这个并不难,但是如果需要在 Spring Security 框架中实现这个功能,还得稍费一点功夫,本文就和小伙伴来分享下在 Spring Security 框架中如何添加验证码. 关于 Spring Security 基本配置,这里就不再多说,小伙伴有不懂的可以参考我的书<SpringBoot+Vue全栈开发实战>,本文主要来看如何加入验证码功能. 准备验证码 要有验证码,首先得先准备好验证码,本文采用

  • Spring Security实现验证码登录功能

    这篇文章主要介绍了Spring Security实现验证码登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在spring security实现登录注销功能的基础上进行开发. 1.添加生成验证码的控制器. (1).生成验证码 /** * 引入 Security 配置属性类 */ @Autowired private SecurityProperties securityProperties; @Override public ImageC

  • Spring Security 实现短信验证码登录功能

    之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已:Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑. 开发短信验证码接口 获取验证码 短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码. @GetMapping("/code/

  • 基于 antd pro 的短信验证码登录功能(流程分析)

    概要 最近使用 antd pro 开发项目时遇到个新的需求, 就是在登录界面通过短信验证码来登录, 不使用之前的用户名密码之类登录方式. 这种方式虽然增加了额外的短信费用, 但是对于安全性确实提高了不少. antd 中并没有自带能够倒计时的按钮, 但是 antd pro 的 ProForm components 中倒是提供了针对短信验证码相关的组件. 组件说明可参见: https://procomponents.ant.design/components/form 整体流程 通过短信验证码登录的

  • vue实现短信验证码登录功能(流程详解)

    无论是移动端还是pc端登录或者注册界面都会见到手机验证码登录这个功能,输入手机号,得到验证码,最后先服务器发送请求,保存登录的信息,一个必不可少的功能 思路 1,先判断手机号和验证是否为空, 2,点击发送验证码,得到验证码 3,输入的验证码是否为空和是否正确, 4,最后向服务发送请求 界面展示 1.准备工作 这个会对input进行封装处理 <template> <div class="text_group"> <div class="input_

  • springboot短信验证码登录功能的实现

    1 .构造手机验证码:使用 random 对象生成要求的随机数作为验证码,例如 4 位验证码: 1000~9999 之间随机数: 2 .使用接口向短信平台发送手机号和验证码数据,然后短信平台再把验证码发送到制定手机号上,接口参数一般包括:目标手机号,随机验证码 (或包含失效时间),平台接口地址,平台口令: 3 .保存接口返回的信息(一般为 json 文本数据,然后需转换为 json 对象格式): 4 .将手机号 - 验证码.操作时间存入 Session 中,作为后面验证使用: 5 .接收用户填写

  • SSM项目实现短信验证码登录功能的示例代码

    目录 1.登入网站 zz短信平台 2.导入工具类MessageUtil 3.ajax 模块 4. html页面 5.编写controller层 1.登入网站 zz短信平台 http://sms_developer.zhenzikj.com/zhenzisms_user/login.html 导入pom依赖 <dependency> <groupId>com.zhenzikj</groupId> <artifactId>zhenzisms</artifa

  • Spring Security 自定义短信登录认证的实现

    自定义登录filter 上篇文章我们说到,对于用户的登录,security通过定义一个filter拦截login路径来实现的,所以我们要实现自定义登录,需要自己定义一个filter,继承AbstractAuthenticationProcessingFilter,从request中提取到手机号和验证码,然后提交给AuthenticationManager: public class SmsAuthenticationFilter extends AbstractAuthenticationPro

  • Spring MVC 中 短信验证码功能的实现方法

    在外部网站中短信的验证很有必要,比如在实现注册.验证用户信息等的情况下.在SpringMVC中的实现如下: 短信接口 短信接口,有些企业会购买的有移动的短信平台接口.如果是个人或者是小企业可以使用一些云服务的.比如百度的API Store上面的. 我使用的是:http://apistore.baidu.com/apiworks/servicedetail/1018.html 当然短信接口肯定都是要付费的,而且是基于模板的,具体的使用说明可以看这个网址里面的使用说明. 前端界面 前端的界面,可能如

随机推荐