现在主流的登录方式主要有 3 种:账号密码登录、短信验证码登录和第三方授权登录, 前面一节 Spring security(三)— 认证过程已分析了 spring security 账号密码方式登陆,现在我们来分析一下 spring security 短信方式认证登陆。
Spring security 短信方式、IP 验证等类似模式登录方式验证,可以根据 账号密码方式登录 步骤仿写出来,其主要以以下步骤进行展开:
- 自定义 Filter:
- 自定义 Authentication
- 自定义 AuthenticationProvider
- 自定义 UserDetailsService
- SecurityConfig 配置
1. 自定义 filter:
自定义 filter 可以根据 UsernamePasswordAuthenticationFilter 过滤器进行仿写,其实质即实现 AbstractAuthenticationProcessingFilter 抽象类,主要流程分为:
- 构建构造器,并在构造器中进行配置请求路径以及请求方式的过滤
- 自定义 attemptAuthentication()认证步骤
- 在 2 步骤中认证过程中需要 AuthenticationProvider 进行最终的认证,在认证 filter 都需要将 AuthenticationProvider 设置进 filter 中,而管理 AuthenticationProvider 的是AuthenticationManager,因此我们创建过滤器 filter 的时候需要设置 AuthenticationManager,这步具体详情在5.1 SecurityConfig 配置步骤。
在第 2 步中 attemptAuthentication() 认证方法 主要进行以下步骤:
   1).post 请求认证;   2).request 请求获取手机号码和验证码;   3). 用自定义的 Authentication 对象封装手机号码和验证码;   4). 使用 AuthenticationManager.authenticate()方法进行验证。
自定义 filter 实现代码:
public class SmsAuthenticationfilter extends AbstractAuthenticationProcessingFilter {
private boolean postOnly = true;
public SmsAuthenticationfilter() {super(new AntPathRequestMatcher(SecurityConstants.APP_MOBILE_LOGIN_URL, "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod());
}
Assert.hasText(SecurityConstants.MOBILE_NUMBER_PARAMETER, "mobile parameter must not be empty or null");
String mobile = request.getParameter(SecurityConstants.MOBILE_NUMBER_PARAMETER);
String smsCode = request.ge+tParameter(SecurityConstants.MOBILE_VERIFY_CODE_PARAMETER);
if (mobile == null) {mobile="";}
if(smsCode == null){smsCode="";}
mobile = mobile.trim();
smsCode = smsCode.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile,smsCode);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}
}
2. Authentication:
在 filter 以及后面的认证都需要使用到自定义的 Authentication 对象,自定义 Authentication 对象可以根据 UsernamePasswordAuthenticationToken 进行仿写,实现 AbstractAuthenticationToken 抽象类。
自定义 SmsAuthenticationToken:
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public SmsAuthenticationToken(Object principal,Object credentials) {super(null);
this.principal = principal;
this.credentials=credentials;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {super(null);
this.principal = principal;
this.credentials=credentials;
setAuthenticated(true);
}
@Override
public Object getCredentials() {return this.credentials=credentials;}
@Override
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();
}
}
3.AuthenticationProvider
AuthenticationProvider 最终认证策略入口,短信方式验证需自定义 AuthenticationProvider。可以根据 AbstractUserDetailsAuthenticationProvider 进行仿写,实现 AuthenticationProvider 以及 MessageSourceAware 接口。认证逻辑可以定义实现。
自定义 AuthenticationProvider:
public class SmsAuthenticationProvide implements AuthenticationProvider, MessageSourceAware {
private UserDetailsService userDetailsService;
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@Override
public void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);
}
@Override
public Authentication authenticate(Authentication authentication) {
Assert.isInstanceOf(SmsAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
// 将验证信息保存在 SecurityContext 以供 UserDetailsService 进行验证
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authenticationToken);
String mobile = (String) authenticationToken.getPrincipal();
if (mobile == null) {throw new InternalAuthenticationServiceException("can't obtain user info ");
}
mobile = mobile.trim();
// 进行验证以及获取用户信息
UserDetails user = userDetailsService.loadUserByUsername(mobile);
if (user == null) {throw new InternalAuthenticationServiceException("can't obtain user info ");
}
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(user, user.getAuthorities());
return smsAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {return (SmsAuthenticationToken.class.isAssignableFrom(authentication));
}
public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}
public UserDetailsService getUserDetailsService() {return userDetailsService;}
}
4. UserDetailsService
在 AuthenticationProvider 最终认证策略入口,认证方式实现逻辑是在 UserDetailsService。可以根据自己项目自定义认证逻辑。
自定义 UserDetailsService:
public class SmsUserDetailsService implements UserDetailsService {
@Autowired
private RedisUtil redisUtil;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从 SecurityContext 获取认证所需的信息(手机号码、验证码)SecurityContext context = SecurityContextHolder.getContext();
SmsAuthenticationToken authentication = (SmsAuthenticationToken) context.getAuthentication();
if(!additionalAuthenticationChecks(username,authentication)){return null;}
// 获取用户手机号码对应用户的信息,包括权限等
return new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("admin")));
}
public boolean additionalAuthenticationChecks(String mobile, SmsAuthenticationToken smsAuthenticationToken) {
// 获取 redis 中手机键值对应的 value 验证码
String smsCode = redisUtil.get(mobile).toString();
// 获取用户提交的验证码
String credentials = (String) smsAuthenticationToken.getCredentials();
if(StringUtils.isEmpty(credentials)){return false;}
if (credentials.equalsIgnoreCase(smsCode)) {return true;}
return false;
}
}
5.SecurityConfig
5.1 自定义 Sms 短信验证组件配置 SecurityConfig
将自定义组件配置 SecurityConfig 中,可以根据 AbstractAuthenticationFilterConfigurer(子类 FormLoginConfigurer)进行仿写 SmsAuthenticationSecurityConfig,主要进行以下配置:
- 将默认 AuthenticationManager(也可以定义的)设置到自定义的 filter 过滤器中
- 将自定义的 UserDetailsService 设置到自定义的 AuthenticationProvide 中以供使用
- 将过滤器添加到过滤链路中,实施过滤操作。(一般以加在 UsernamePasswordAuthenticationFilter 前)
配置 SmsAuthenticationSecurityConfig:
@Component
public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
// 创建并配置好自定义 SmsAuthenticationfilter,SmsAuthenticationfilter smsAuthenticationfilter = new SmsAuthenticationfilter();
smsAuthenticationfilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationfilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler());
smsAuthenticationfilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler());
// 创建并配置好自定义 SmsAuthenticationProvide
SmsAuthenticationProvide smsAuthenticationProvide=new SmsAuthenticationProvide();
smsAuthenticationProvide.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsAuthenticationProvide);
// 将过滤器添加到过滤链路中
http.addFilterAfter(smsAuthenticationfilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() {return new CustomAuthenticationSuccessHandler();
}
@Bean
public CustomAuthenticationFailureHandler customAuthenticationFailureHandler() {return new CustomAuthenticationFailureHandler();
}
}
5.2 SecurityConfig 主配置
SecurityConfig 主配置可以参照第二节 Spring Security(二)–WebSecurityConfigurer 配置以及 filter 顺序进行配置。
SecurityConfig 主配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {http.headers().frameOptions().disable().and()
.formLogin()
.loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE)
// 配置 form 登陆的自定义 URL
.loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL)
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
// 配置 smsAuthenticationSecurityConfig
.apply(smsAuthenticationSecurityConfig)
.and()
// 运行通过 URL
.authorizeRequests()
.antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL,
SecurityConstants.APP_USER_REGISTER_URL)
.permitAll()
.and()
.csrf().disable();
}
@Bean
public ObjectMapper objectMapper(){return new ObjectMapper();
}
}
6. 其他
6.1 redis
RedisUtil 工具类:
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true 成功 false 失败
*/
public boolean set(String key, Object value) {
try {redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time 要大于 0 如果 time 小于等于 0 将设置无限期
* @return true 成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {set(key, value);
}
return true;
} catch (Exception e) {e.printStackTrace();
return false;
}
}
}
redisConfig 配置类:
@Configuration
public class RedisConfig {
@Autowired
private RedisProperties properties;
@Bean
@SuppressWarnings("all")
@ConditionalOnClass(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash 的 key 也采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序列化方式采用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序列化方式采用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
@Qualifier("redisConnectionFactory")
public RedisConnectionFactory redisConnectionFactory(){RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
redisConfig.setHostName(properties.getHost());
redisConfig.setPort(properties.getPort());
redisConfig.setPassword(RedisPassword.of(properties.getPassword()));
redisConfig.setDatabase(properties.getDatabase());
//redis 连接池数据设置
JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();
if (this.properties.getTimeout() != null) {Duration timeout = this.properties.getTimeout();
builder.readTimeout(timeout).connectTimeout(timeout);
}
RedisProperties.Pool pool = this.properties.getJedis().getPool();
if (pool != null) {builder.usePooling().poolConfig(this.jedisPoolConfig(pool));
}
JedisClientConfiguration jedisClientConfiguration = builder.build();
// 根据两个配置类生成 JedisConnectionFactory
return new JedisConnectionFactory(redisConfig,jedisClientConfiguration);
}
private JedisPoolConfig jedisPoolConfig(RedisProperties.Pool pool) {JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(pool.getMaxActive());
config.setMaxIdle(pool.getMaxIdle());
config.setMinIdle(pool.getMinIdle());
if (pool.getMaxWait() != null) {config.setMaxWaitMillis(pool.getMaxWait().toMillis());
}
return config;
}
}
7. 总结
可以根据短信验证登陆模式去实现类似的验证方式,可以结合本节的例子进行跟项目结合起来,减少开发时间。后续还有第三方登陆方式分析以案例。最后错误请评论指出!
代码下载:
demo:https://github.com/Ccww-lx/sp…
最后可关注公众号:【Ccww 笔记】一起学习, 每天会分享干货,还有学习视频领取!