spring-security-oauth2(七) 自定義簡訊登陸開發
阿新 • • 發佈:2018-12-29
簡訊登陸開發
原理
基本原理:SmsAuthenticationFilter接受請求生成SmsAuthenticationToken,然後交給系統的AuthenticationManager進行管理,然後找到SmsAuthenticationProvider,然後再呼叫UserDetailsService進行簡訊驗證,SmsAuthenticationSecurityConfig進行配置 SmsCaptchaFilter驗證碼過濾器 在請求之前進行驗證驗證
簡訊驗證主要是複製參考使用者名稱密碼流程,參考前面的原始碼分析。
程式碼
SmsAuthenticationToken
package com.rui.tiger.auth.core.authentication.mobile; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * 手機token * 參照:UsernamePasswordAuthenticationToken * @author CaiRui * @Date 2018/12/15 22:28 */ public class SmsAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 500L; private final Object principal;//使用者名稱 //private Object credentials; 密碼 手機登入驗證碼在登入前已經驗證 考慮手機驗證碼通用 沒有放到這裡 /** * 沒有認證成功 * @param mobile 手機號 */ public SmsAuthenticationToken(Object mobile) { super((Collection)null); this.principal = mobile; this.setAuthenticated(false); } /** * 認證成功同時進行許可權設定 * @param principal * @param authorities */ public SmsAuthenticationToken(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(); } }
SmsAuthenticationFilter
package com.rui.tiger.auth.core.authentication.mobile; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 手機登陸過濾器 * 參照:UsernamePasswordAuthenticationFilter * @author CaiRui * @Date 2018/12/16 10:39 */ public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== public static final String TIGER_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = TIGER_SECURITY_FORM_MOBILE_KEY; //public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; // private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; // ~ Constructors // =================================================================================================== //TODO /authentication/mobile 這些引數應該配置到字典中待優化 public SmsAuthenticationFilter() { // 攔截該路徑,如果是訪問該路徑,則標識是需要簡訊登入 super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } // ~ Methods // ======================================================================================================== public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 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(); SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * Enables subclasses to override the composition of the username, such as by * including additional values and a separator. * * @param request so that request attributes can be retrieved * * @return the username that will be presented in the <code>Authentication</code> * request token to the <code>AuthenticationManager</code> */ 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, SmsAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Sets the parameter name which will be used to obtain the mobile from the login * request. * * @param mobileParameter the parameter name. Defaults to "mobile". */ public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "mobile parameter must not be empty or null"); this.mobileParameter = mobileParameter; } public String getMobileParameter() { return mobileParameter; } /** * 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; } }
SmsAuthenticationProvider
package com.rui.tiger.auth.core.authentication.mobile;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @author CaiRui
* @Date 2018/12/16 10:38
*/
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken smsCaptchaAuthenticationToken= (SmsAuthenticationToken) authentication;
UserDetails user=userDetailsService.loadUserByUsername((String) smsCaptchaAuthenticationToken.getPrincipal());
if(user==null){
throw new InternalAuthenticationServiceException("無法獲取使用者資訊");
}
//認證通過
SmsAuthenticationToken authenticationTokenResult=new SmsAuthenticationToken(user,user.getAuthorities());
//將之前未認證的請求放進認證後的Token中
authenticationTokenResult.setDetails(smsCaptchaAuthenticationToken.getDetails());
return authenticationTokenResult;
}
//@Autowired
@Getter
@Setter
private UserDetailsService userDetailsService;//
/**
* AuthenticationManager 驗證該Provider是否支援 認證
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return aClass.isAssignableFrom(SmsAuthenticationToken.class);
}
}
SmsAuthenticationSecurityConfig
package com.rui.tiger.auth.core.authentication.mobile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* 手機許可權配置類
* @author CaiRui
* @Date 2018/12/16 13:42
*/
@Component
public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
//實現類怎麼確定? 自定義的實現??
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
smsAuthenticationProvider.setUserDetailsService(userDetailsService);
http
// 註冊到AuthenticationManager中去
.authenticationProvider(smsAuthenticationProvider)
// 新增到 UsernamePasswordAuthenticationFilter 之後
// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter
// 然後UsernamePasswordAuthenticationFilter的provider不支援這個地址的請求
// 所以就會落在我們自己的認證過濾器上。完成接下來的認證
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
SmsCaptchaFilter
package com.rui.tiger.auth.core.captcha.sms;
import com.rui.tiger.auth.core.captcha.CaptchaException;
import com.rui.tiger.auth.core.captcha.CaptchaProcessor;
import com.rui.tiger.auth.core.captcha.CaptchaVo;
import com.rui.tiger.auth.core.captcha.ImageCaptchaVo;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 手機驗證碼過濾器
* OncePerRequestFilter 過濾器只會呼叫一次
*
* @author CaiRui
* @date 2018-12-10 12:23
*/
@Setter
@Getter
@Slf4j
public class SmsCaptchaFilter extends OncePerRequestFilter implements InitializingBean {
//一般在配置類中進行注入
private AuthenticationFailureHandler failureHandler;
private SecurityProperties securityProperties;
/**
* 驗證碼攔截的路徑
*/
private Set<String> interceptUrlSet = new HashSet<>();
//session工具類
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//路徑匹配工具類
private AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* @throws ServletException
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//其它配置的需要驗證碼驗證的路徑
String configInterceptUrl = securityProperties.getCaptcha().getSms().getInterceptUrl();
if (StringUtils.isNotBlank(configInterceptUrl)) {
String[] configInterceptUrlArray = StringUtils.split(configInterceptUrl, ",");
interceptUrlSet = Stream.of(configInterceptUrlArray).collect(Collectors.toSet());
}
//簡訊登入請求驗證
interceptUrlSet.add("/authentication/mobile");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("驗證碼驗證請求路徑:[{}]", request.getRequestURI());
boolean action = false;// 預設不放行
for (String url : interceptUrlSet) {
if (antPathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
if (action) {
try {
validate(request);
} catch (CaptchaException captchaException) {
//失敗呼叫我們的自定義失敗處理器
failureHandler.onAuthenticationFailure(request, response, captchaException);
//後續流程終止
return;
}
}
//後續過濾器繼續執行
filterChain.doFilter(request, response);
}
/**
* 圖片驗證碼校驗
*
* @param request
*/
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
String smsSessionKey=CaptchaProcessor.CAPTCHA_SESSION_KEY+"sms";
// 拿到之前儲存的imageCode資訊
ServletWebRequest swr = new ServletWebRequest(request);
CaptchaVo smsCaptchaInSession = (CaptchaVo) sessionStrategy.getAttribute(swr, smsSessionKey);
String codeInRequest = ServletRequestUtils.getStringParameter(request, "smsCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new CaptchaException("驗證碼的值不能為空");
}
if (smsCaptchaInSession == null) {
throw new CaptchaException("驗證碼不存在");
}
if (smsCaptchaInSession.isExpried()) {
sessionStrategy.removeAttribute(swr, smsSessionKey);
throw new CaptchaException("驗證碼已過期");
}
if (!StringUtils.equals(smsCaptchaInSession.getCode(), codeInRequest)) {
throw new CaptchaException("驗證碼不匹配");
}
//驗證通過 移除快取
sessionStrategy.removeAttribute(swr, smsSessionKey);
}
}
BrowserSecurityConfig 瀏覽器配置同步調整
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.authentication.mobile.SmsAuthenticationSecurityConfig;
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
import com.rui.tiger.auth.core.captcha.sms.SmsCaptchaFilter;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* 瀏覽器security配置類
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;//簡訊登陸配置
/**
* 密碼加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 記住我持久化資料來源
* JdbcTokenRepositoryImpl CREATE_TABLE_SQL 建表語句可以先在資料庫中執行
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//第一次會執行CREATE_TABLE_SQL建表語句 後續會報錯 可以關掉
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//加入圖片驗證碼過濾器
CaptchaFilter captchaFilter = new CaptchaFilter();
captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
captchaFilter.setSecurityProperties(securityProperties);
captchaFilter.afterPropertiesSet();
//簡訊驗證碼的配置
SmsCaptchaFilter smsCaptchaFilter = new SmsCaptchaFilter();
smsCaptchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
smsCaptchaFilter.setSecurityProperties(securityProperties);
smsCaptchaFilter.afterPropertiesSet();
//將驗證碼的過濾器放在登陸的前面
http.addFilterBefore(smsCaptchaFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")//自定義登入請求
.loginProcessingUrl("/authentication/form")//自定義登入表單請求
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler)
.and()
//記住我相關配置
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRemberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/authentication/require", "/captcha/*")//此路徑放行 否則會陷入死迴圈
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域關閉
//簡訊登陸配置掛載
.apply(smsAuthenticationSecurityConfig)
;
}
}
ok 幾個核心類都開發完畢下面我們進行測試下
測試
- 登入頁面
- 點擊發送簡訊驗證碼,後臺日誌檢視驗證碼
- 返回到登入頁面,輸入驗證碼進行登陸確認
- 後臺複製真正傳送的驗證碼新增
- 提交簡訊登入
1.登入
2.傳送驗證碼 後臺日誌獲取
3.輸入簡訊驗證碼 先輸入錯誤的
4.返回提示
其它各種情況自行除錯驗證。
總結