Spring security(四)-spring boot +spring security簡訊認證+redis整合

Ccww發表於2019-06-29

  現在主流的登入方式主要有 3 種:賬號密碼登入、簡訊驗證碼登入和第三方授權登入,前面一節Spring security(三)---認證過程已分析了spring security賬號密碼方式登陸,現在我們來分析一下spring security簡訊方式認證登陸。

  Spring security 簡訊方式、IP驗證等類似模式登入方式驗證,可以根據賬號密碼方式登入步驟仿寫出來,其主要以以下步驟進行展開:

  1. 自定義Filter:
  2. 自定義Authentication
  3. 自定義AuthenticationProvider
  4. 自定義UserDetailsService
  5. SecurityConfig配置

1. 自定義filter:

  自定義filter可以根據UsernamePasswordAuthenticationFilter過濾器進行仿寫,其實質即實現AbstractAuthenticationProcessingFilter抽象類,主要流程分為:

  1. 構建構造器,並在構造器中進行配置請求路徑以及請求方式的過濾
  2. 自定義attemptAuthentication()認證步驟
  3. 在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,主要進行以下配置:

  1. 將預設AuthenticationManager(也可以定義的)設定到自定義的filter過濾器中
  2. 將自定義的UserDetailsService設定到自定義的AuthenticationProvide中以供使用
  3. 將過濾器新增到過濾鏈路中,實施過濾操作。(一般以加在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:github.com/Ccww-lx/spr…

歡迎轉載,標明出處!!

相關文章