1. 程式人生 > >SpringSecurity之簡訊驗證碼

SpringSecurity之簡訊驗證碼

簡訊驗證碼

簡訊驗證碼的傳送

需要實現對手機的傳送驗證碼,這裡只是簡單的處理,打印出來

  • 定義介面 //傳送簡訊的介面 public interface SmsCodeSender {

      void send(String mobile, String code);
      }
      //生成簡訊介面
      public interface ValidateCodeGenerator {
    
      ValidateCode generate(ServletWebRequest  request);
    
      }
    
  • 實現介面

      //傳送簡訊的介面的實現
      public class DefaultSmsCodeSender implements SmsCodeSender {
    
      /* (non-Javadoc)
       * [@see](https://my.oschina.net/weimingwei) com.imooc.security.core.validate.code.sms.SmsCodeSender#send(java.lang.String, java.lang.String)
    
      //生成簡訊介面的實現 
      [@Override](https://my.oschina.net/u/1162528)
      public void send(String mobile, String code) {
      	System.out.println("向手機"+mobile+"傳送簡訊驗證碼"+code);
      }
      }
    
  • 生成手機驗證碼 @Component("smsValidateCodeGenerator") public class SmsCodeGenerator implements ValidateCodeGenerator {

      	@Autowired
      	private SecurityProperties securityProperties;
    
      	/*
      	 * (non-Javadoc)
      	 *
      	 * [@see](https://my.oschina.net/weimingwei)
      	 * com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.
      	 * springframework.web.context.request.ServletWebRequest)
      	 */
      	public ValidateCode generate(ServletWebRequest request) {
      		String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLegth());
      		return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
      	}
    
      	public SecurityProperties getSecurityProperties() {
      		return securityProperties;
      	}
    
      	public void setSecurityProperties(SecurityProperties securityProperties) {
      		this.securityProperties = securityProperties;
      	}
      }
    
  • 傳送手機驗證碼

      @GetMapping("/code/sms")
      public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
         ImageCode imageCode = generate(request);
      	ValidateCode smsCode = imageCodeGenerator.generate(new ServletWebRequest(request));
      	sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, smsCode);
      	String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
      	smsCodeSender.send(mobile,smsCode.getCode());
      }
    

上面的部分實現了手機驗證碼的生成,接下來要做的就是手機驗證碼的驗證。

手機驗證碼的驗證

手機驗證碼的驗證因為不像使用者名稱密碼那樣可以直接登入,為了實現手機的驗證,需要模仿使用者名稱和密碼的登入方式,對使用者名稱登入的那一套進行改造,需要建立一個簡訊驗證過濾器SmsAuthenticationFilter,簡訊驗證碼的token,SmsAuthenticaitonToken,然後需要一個驗證手機號的Provider,SmsAuthenticationProvider,然後將手機號傳遞給UserDetailService進行相關的處理.

SmsAuthenticationToken的程式碼如下:

/**
 * 封裝登入資訊
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
	private static final long serialVersionUID = 420L;
	//放認證資訊(登入之前放手機號,登入成功之後存放使用者的資訊)
	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);
	}

	public Object getCredentials() {
		return null;
	}

	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();
	}
}

SmsCodeAuthenticationFilter.java

	public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
		// ~ Static fields/initializers
		// =====================================================================================
		//攔截的引數的名稱
		private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;
		//表示是否只允許post請求
		private boolean postOnly = true;

		// ~ Constructors
		// ===================================================================================================

		/**
		 * 指定過濾器攔截的請求
		 */
		public SmsCodeAuthenticationFilter() {
			super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST"));
		}

		// ~ Methods
		// ========================================================================================================

		public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
				throws AuthenticationException {
			//判斷是不是post請求
			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);
			//認證前將請求的資訊放入SmsCodeAuthenticationToken
			// Allow subclasses to set the "details" property
			setDetails(request, authRequest);

			return this.getAuthenticationManager().authenticate(authRequest);
		}


		/**
		 * 獲取手機號
		 */
		protected String obtainMobile(HttpServletRequest request) {
			return request.getParameter(mobileParameter);
		}

		/**
		 * 把請求的數放到認證請求裡面去。
		 * Provided so that subclasses may configure what is put into the
		 * authentication request's details property.
		 *
		 * @param request
		 *            that an authentication request is being created for
		 * @param authRequest
		 *            the authentication request object that should have its details
		 *            set
		 */
		protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
			authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
		}

		/**
		 * Sets the parameter name which will be used to obtain the username from
		 * the login request.
		 *
		 * @param usernameParameter
		 *            the parameter name. Defaults to "username".
		 */
		public void setMobileParameter(String usernameParameter) {
			Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
			this.mobileParameter = usernameParameter;
		}


		/**
		 * Defines whether only HTTP POST requests will be allowed by this filter.
		 * If set to true, and an authentication request is received which is not a
		 * POST request, an exception will be raised immediately and authentication
		 * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
		 * will be called as if handling a failed authentication.
		 * <p>
		 * Defaults to <tt>true</tt> but may be overridden by subclasses.
		 */
		public void setPostOnly(boolean postOnly) {
			this.postOnly = postOnly;
		}

		public final String getMobileParameter() {
			return mobileParameter;
		}

	}

SmsCodeAuthenticationProvider.java

	public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

		private UserDetailsService userDetailsService;

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.springframework.security.authentication.AuthenticationProvider#
		 * authenticate(org.springframework.security.core.Authentication)
		 */
		@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());
			/*
			SmsCodeAuthenticationFilter的
			  將之前沒驗證的SmsCodeAuthenticationToken內的請求資訊重新放入到新建立的token裡面
			 */
			authenticationResult.setDetails(authenticationToken.getDetails());
			return authenticationResult;
		}

		/*
		 *判斷傳遞進來的東西是不是SmsCodeAuthenticationToken
		 * (non-Javadoc)
		 * 
		 * @see org.springframework.security.authentication.AuthenticationProvider#
		 * supports(java.lang.Class)
		 */
		@Override
		public boolean supports(Class<?> authentication) {
			return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
		}

		public UserDetailsService getUserDetailsService() {
			return userDetailsService;
		}

		public void setUserDetailsService(UserDetailsService userDetailsService) {
			this.userDetailsService = userDetailsService;
		}
	}

SmsCodeAuthenticationSecurityConfig.java(短息驗證的配置)

	/**
	 * 該模組是要在app,也要在browser裡面用
	 */
	@Component
	public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity>{

		@Autowired
		private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;

		@Autowired
		private AuthenticationFailureHandler imoocAuthenticationFailureHandler;

		@Autowired
		private UserDetailsService userDetailsService;


		@Override
		public void configure(HttpSecurity builder) throws Exception {
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		//設定manager
		smsCodeAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
		//設定Provider
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		//將我們自定義的Provider加入到所有provider的集合裡面
		builder.authenticationProvider(smsCodeAuthenticationProvider)
				.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
		}
	}


	/**
	 * 專門用來做web安全應用的介面卡WebSecurityConfigurerAdapter
	 */
	@Configuration
	public class BrowswerSecurityConfig extends AbstractChannelSecurityConfig {
		@Autowired
		private SecurityProperties securityProperties;

		@Autowired
		private DataSource dataSource;

		@Autowired
		private UserDetailsService userDetailsService;

		@Autowired
		private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

		@Autowired
		private ValidateCodeSecurityConfig validateCodeSecurityConfig;

		/**
		 * 配置密碼編碼器
		 *
		 * @return
		 */
		@Bean
		public PasswordEncoder passwordEncoder() {
			return new BCryptPasswordEncoder();
		}
		@Primary
		@Bean(name = "dataSource")
		@ConfigurationProperties(prefix = "spring.datasource")
		public DataSource dataSource(){
			return DataSourceBuilder.create().build();
		}
		/**
		 * @param
		 * @throws Exception
		 */
		@Bean
		public PersistentTokenRepository persistentTokenRepository() {
			JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
			//配置資料來源
			tokenRepository.setDataSource(dataSource);
			//配置在啟動的時候建立表
	//        tokenRepository.setCreateTableOnStartup(true);
			return tokenRepository;

		}


		@Override
		protected void configure(HttpSecurity http) throws Exception {
			applyPasswordAuthenticationConfig(http);
			http.apply(validateCodeSecurityConfig)
					.and()
					.apply(smsCodeAuthenticationSecurityConfig)
					.and()
	//                .apply(imoocSocialSecurityConfig)
	//                .and()
					.rememberMe()
					.tokenRepository(persistentTokenRepository())
					.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
					.userDetailsService(userDetailsService)
					.and()
					.authorizeRequests()
					.antMatchers(
							SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
							SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
							securityProperties.getBrowser().getLoginPage(),
							SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*")
					.permitAll()
					.anyRequest()
					.authenticated()
					.and()
					.csrf().disable();

		}
	}

之後就可以實