【SpringSecurity+OAuth2+JWT入门到实战】11.短信验证码登录

登录流程
- SmsCodeAuthenticationFilter 短信登录请求
- SmsCodeAuthenticationProvider 提供短信登录处理的实现类
- SmsCodeAuthenticationToken 存放认证信息(包括未认证前的参数信息传递)
- 最后开发一个过滤器放在 短信登录请求之前,进行短信验证码的验证
因为这个过滤器只关心提交的验证码是否正常就行了。所以可以应用到任意业务中,对任意业务提交进行短信的验证
UsernamePasswordAuthenticationToken
security默认密码登录,在这个基础上把密码相关的代码清理掉
//// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.authentication;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
SmsCodeAuthenticationToken
core项目创建SmsCodeAuthenticationToken类
复制UsernamePasswordAuthenticationToken类全部内容到SmsCodeAuthenticationToken类
删除credentials值相关的代码, credentials字段去掉因为短信认证在授权认证前已经过滤了
package com.spring.security.authentication.mobile;import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
//在认证之前应该放手机号 在认证之后放认证用户
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super((Collection)null);
this.principal = mobile;
//没登录之前放手机号
this.setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
//登录成功放用户信息
super.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
@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");
} else {
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
UsernamePasswordAuthenticationFilter
写SmsCodeAuthenticationFilter同意去参考security默认拦截器
//// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.web.authentication;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
SmsCodeAuthenticationFilter
同样复制UsernamePasswordAuthenticationFilter代码
package com.spring.security.authentication.mobile;import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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 {
//定义携带手机号参数名
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
//要处理的请求连接
//登录页面form表单action <form action="/authentication/mobile" method="post">
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//获取手机号
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
//去掉空格
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// 把request里面的一些信息copy近token里面
// 后面认证成功的时候还需要copy这信息到新的token
this.setDetails(request, authRequest);
//认证行为已完成
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号
*
* @param request
* @return
*/
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
/**
* 设置认证信息去请求头
*
* @param request
* @param authRequest
*/
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return this.mobileParameter;
}
}
SmsCodeAuthenticationProvider
这个没有找到仿照的地方。没有发现和usernamePassword类型的提供provider
package com.spring.security.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 {
@Autowired
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;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
smsCodeFilter
直接复制ValidateCodeFilter类改名为:smsCodeFilter
package com.spring.security.validate.code;import com.spring.security.properties.SecurityProperties;
import com.spring.security.validate.code.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.stereotype.Component;
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;
/**
* 短信验证码过滤器
*/
@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
private Set<String> urls = new HashSet<>();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
//加入固定提交地址
urls.add("/authentication/mobile");
}
/**
* 做过滤器内部
*
* @param httpServletRequest http Servlet请求
* @param httpServletResponse
* @param filterChain 过滤器链
* @throws ServletException Servlet异常
* @throws IOException IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (StringUtils.startsWithIgnoreCase(url, httpServletRequest.getRequestURI())
&& StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) {
action = true;
}
}
if (action) {
try {
validateCode(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 验证代码
*
* @param servletWebRequest servlet的Web请求
* @throws ServletRequestBindingException Servlet请求绑定异常
*/
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
ValidateCode validateSession = (ValidateCode) sessionStrategy.getAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");
if (StringUtils.isEmpty(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空!");
}
if (validateSession == null) {
throw new ValidateCodeException("验证码不存在!");
}
if (validateSession.isExpire()) {
sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
throw new ValidateCodeException("验证码已过期!");
}
if (!StringUtils.startsWithIgnoreCase(validateSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不正确!");
}
sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS");
}
}
SmsCodeAuthenticationSecurityConfig
需要的几个东西已经准备好了。这里要进行配置把这些加入到 security的认证流程中去;
package com.spring.security.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 AuthenticationFailureHandler hkAuthenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler hkAuthenticationSuccessHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
// 把该过滤器交给管理器
// 图上流程,因为最先走的 短信认证的过滤器(不是验证码,只是认证)
// 要使用管理器来获取provider,所以把管理器注册进去
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//认证失败处理器
filter.setAuthenticationFailureHandler(hkAuthenticationFailureHandler);
//认证成功处理器
filter.setAuthenticationSuccessHandler(hkAuthenticationSuccessHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
//加到security认证流程
http
// 注册到AuthenticationManager中去
.authenticationProvider(smsCodeAuthenticationProvider)
// 添加到 UsernamePasswordAuthenticationFilter 之后
// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter
// 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求
// 所以就会落在我们自己的认证过滤器上。完成接下来的认证
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
BrowserSecurityConfig 应用方配置
package com.spring.security;import com.spring.security.authentication.HkAuthenticationFailureHandler;
import com.spring.security.authentication.HkAuthenticationSuccessHandler;
import com.spring.security.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import com.spring.security.properties.SecurityProperties;
import com.spring.security.validate.code.SmsCodeFilter;
import com.spring.security.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private HkAuthenticationSuccessHandler hkAuthenticationSuccessHandler;
@Autowired
private HkAuthenticationFailureHandler hkAuthenticationFailureHandler;
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Autowired
private SmsCodeFilter smsCodeFilter;
// 数据源是需要在使用处配置数据源的信息
@Autowired
private DataSource dataSource;
// 之前已经写好的 MyUserDetailsService
@Autowired
private UserDetailsService userDetailsService;
//SmsCodeAuthenticationSecurityConfig加入到认证流程
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
// org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.tokenRepository
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 该对象里面有定义创建表的语句
//create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
// 可以设置让该类来创建表
// 但是该功能只用使用一次,如果数据库已经存在表则会报错
//jdbcTokenRepository.setCreateTableOnStartup(true);
//两种方法二选一
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器
// 短信验证码校验过滤器
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")//登录页面路径
// 处理登录请求路径
.loginProcessingUrl("/authentication/form")
.successHandler(hkAuthenticationSuccessHandler) // 处理登录成功
.failureHandler(hkAuthenticationFailureHandler) // 处理登录失败
.and()
// 从这里开始配置记住我的功能
.rememberMe()
.tokenRepository(persistentTokenRepository())
// 新增过期配置,单位秒
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
// userDetailsService 是必须的。不然就报错
.userDetailsService(userDetailsService)
.and()
.authorizeRequests() // 授权配置
//不需要认证的路径
.antMatchers("/authentication/require", "/code/*", "/signIn.html", securityProperties.getBrowser().getLoginPage(), "/failure").permitAll()
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable()
//加入短信验证码认证流程
.apply(smsCodeAuthenticationSecurityConfig);
}
}
测试
启动项目访问:http://127.0.0.1:8080/signIn.html
获取验证码:
输入错误验证码:
输入正确验证码:
以上是 【SpringSecurity+OAuth2+JWT入门到实战】11.短信验证码登录 的全部内容, 来源链接: utcz.com/z/514149.html

