文章目錄
SpringSecurity進行用戶登錄認證時,通過UsernamePasswordAuthenticationFilter獲取用戶信息,獲取一個UsernamePasswordAuthenticationToken,將該token設置給AuthenticationManager進行管理,選擇不同的Provider進行認證處理,在Provider中通過UserDetaitlsService獲取用戶信息進行認證,生成新的認證信息。
在短信驗證時,我們只要按照登錄驗證的步驟進行重新寫一個自己的過濾器、token類、provider即可。
1、自己實現一個SmsAuthenticationToken類
/**
* 短信驗證碼token
*
* 仿照UsernamePasswordAuthenticationToken
*/
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 getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@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();
}
}
2、自己實現一個SmsCodeAuthenticationFilter,驗證用戶
/**
* 仿照UsernamePasswordAuthenticationFilter
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String IMOOC_FROM_MOBILE_KEY = "mobile";
private String mobileParameter = "username";
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
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();
// 生成token
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
// 調用 AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 獲取手機號
* @param request
* @return
*/
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username 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;
}
}
3、自己實現一個SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
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;
}
/**
* 根據該方法判斷 AuthenticationManager選擇哪個 Provider進行認證處理
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
4、驗證碼驗證過濾器
/**
* 繼承一個過濾器,實現驗證碼驗證
*/
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 初始化設置
* 將配置文件中的值讀取,存在urls中
* @throws ServletException
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.split(securityProperties.getCode().getSms().getUrl(),",");
// 添加配置的url
for (String configUrl : configUrls) {
urls.add(configUrl);
}
// 添加攔截的url
urls.add("authentication/mobile");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
/**
* 如果是登錄請求,並且是post請求就執行驗證
*
*/
boolean action = false;
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
if (action) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
return ;
}
}
// if (StringUtils.equals("authentication/from", request.getRequestURI())
// && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
//
// try {
// validate(new ServletWebRequest(request));
// } catch (ValidateCodeException e) {
// authenticationFailureHandler.onAuthenticationFailure(request,response,e);
// return ;
// }
// }
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 讀取session
ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request,
ValidateCodeController.SESSION_KEY);
// 獲取驗證碼的值
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),"smsCode");
if (StringUtils.isEmpty(codeInRequest)) {
throw new ValidateCodeException("驗證碼的值不能爲空");
}
if (codeInSession == null) {
throw new ValidateCodeException("驗證碼不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("驗證碼已過期");
}
if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)) {
throw new ValidateCodeException("驗證碼不匹配");
}
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
5、將SmsCodeAuthenticationFitler和SmsCodeAuthenticationProvider進行配置
將短信驗證碼登錄驗證的filter和provider,進行單獨配置。一般在項目中,短信驗證碼會是一個公共項目組件。
/**
* 配置SmsCodeAuthenticationFilter
* 和SmsCodeAuthenticationProvider
*/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
6、驗證碼的驗證進行配置
/**
* 覆蓋springboot對security的默認配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
/**
* 注入登錄成功操作類
*/
@Autowired
private AuthenticationSuccessHandler imoocAutheticationSuccessHandler;
/**
* 注入登錄失敗的處理器
*/
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailuredHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
/**
* 將短信驗證的單獨配置引入進來,通過HttpSecurity 的apply()方法進行配置
*/
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
/**
* 用戶密碼加密類配置
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 這裏可以返回自己實現的加密類
return new BCryptPasswordEncoder();
}
/**
* 記住我的配置
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository () {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啓動的時候創建表
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 驗證碼過濾器配置
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
// 設置失敗處理器
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailuredHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// 短信驗證碼過濾器配置
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
// 設置失敗處理器
smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailuredHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() // 表單登錄 http.httpBasic() httpbasic登錄
// .loginPage("/imooc-signIn.html") // 配置登錄頁面
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/from") // 配置登錄請求
.successHandler(imoocAutheticationSuccessHandler) // 登錄成功處理器
.failureHandler(imoocAuthenticationFailuredHandler)
.and()
.rememberMe() // 配置記住我
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests() // 請求驗證
// .antMatchers("/imooc-signIn.html").permitAll()// 匹配不需要身份認證的路徑
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage(),
"/code/*").permitAll()
.anyRequest() // 任何請求
.authenticated() // 都需要身份認證
.and()
.csrf().disable()
.apply(smsCodeAuthenticationSecurityConfig); // 關閉僞造跨站請求防護功能
}
}