如今主流的登陸方式主要有 3 種:帳號密碼登陸、短信驗證碼登陸和第三方受權登陸,前面一節Spring security(三)---認證過程已分析了spring security帳號密碼方式登錄,如今咱們來分析一下spring security短信方式認證登錄。redis
Spring security 短信方式、IP驗證等相似模式登陸方式驗證,能夠根據帳號密碼方式登陸步驟仿寫出來,其主要以如下步驟進行展開:spring
自定義Filter:緩存
自定義Authenticationapp
自定義AuthenticationProvideride
自定義UserDetailsService工具
SecurityConfig配置post
自定義filter能夠根據UsernamePasswordAuthenticationFilter過濾器進行仿寫,其實質即實現AbstractAuthenticationProcessingFilter抽象類,主要流程分爲:ui
構建構造器,並在構造器中進行配置請求路徑以及請求方式的過濾this
自定義attemptAuthentication()認證步驟spa
在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](https://my.oschina.net/u/1162528) 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; } }
在filter以及後面的認證都須要使用到自定義的Authentication對象,自定義Authentication對象能夠根據UsernamePasswordAuthenticationToken進行仿寫,實現AbstractAuthenticationToken抽象類。https://www.jianshu.com/p/5e19f3a9f6dd
自定義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](https://my.oschina.net/u/1162528) public Object getCredentials() { return this.credentials=credentials; } [@Override](https://my.oschina.net/u/1162528) 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](https://my.oschina.net/u/1162528) public void eraseCredentials() { super.eraseCredentials(); } }
AuthenticationProvider最終認證策略入口,短信方式驗證需自定義AuthenticationProvider。能夠根據AbstractUserDetailsAuthenticationProvider進行仿寫,實現AuthenticationProvider以及MessageSourceAware接口。認證邏輯能夠定義實現。焦做國醫堂胃腸醫院正規嗎:http://jz.lieju.com/zhuankeyiyuan/37174867.htm
自定義AuthenticationProvider:
public class SmsAuthenticationProvide implements AuthenticationProvider, MessageSourceAware { private UserDetailsService userDetailsService; private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); [@Override](https://my.oschina.net/u/1162528) 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; } }
在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; } }
將自定義組件配置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(); } }
SecurityConfig主配置能夠參照第二節Spring Security(二)--WebSecurityConfigurer配置以及filter順序進行配置。
SecurityConfig主配置:
@Configurationpublic 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(); } }
RedisUtil工具類:
@Componentpublic 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配置類:
@Configurationpublic class RedisConfig {@Autowiredprivate 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; } }
能夠根據短信驗證登錄模式去實現相似的驗證方式,能夠結合本節的例子進行跟項目結合起來,減小開發時間。後續還有第三方登錄方式分析以案例。最後錯誤請評論指出!