SpringSecurity(六)簡訊驗證碼登入
由 SpringSecurity(四)認證流程 我們已經知道了Spring Security
使用者名稱和密碼的登入流程。仿照使用者名稱和密碼登入編寫一個簡訊驗證碼登入
手機驗證碼登入流程圖
簡訊驗證碼
新建一個SmsCode類,裡面有三個屬性:String code(驗證碼字串)、LocalDateTime expireTime(過期時間)、String mobile(手機號)。省略簡訊驗證碼的傳送過程。大概步驟如下:
1. 使用者點擊發送驗證碼按鈕
2. 隨機生成一個code字串,新建SmsCode物件,設定驗證碼字串code、手機號mobile和過期時間expireTime
3. 將SmsCode物件存入session
實現手機驗證碼登入
1. 新建 SmsCodeAuthenticationToken,對應使用者名稱密碼登入的UsernamePasswordAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 500L; private final Object principal; public SmsCodeAuthenticationToken(Object 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(); } }
2. 新建 SmsCodeAuthenticationFilter,對應使用者名稱密碼登入的UsernamePasswordAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } 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(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } 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 setMobileParameter(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,對應使用者名稱密碼登入的DaoAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private MyUserService myUserService;
public MyUserService getMyUserService() {
return myUserService;
}
public void setMyUserService(MyUserService myUserService) {
this.myUserService = myUserService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication;
UserDetails user = myUserService.loadUserByMobile((String)smsCodeAuthenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取使用者資訊");
}
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities());
result.setDetails(smsCodeAuthenticationToken.getDetails());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
4. 修改 MyUserService
@Component
public class MyUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
LoginUser loginUser = new LoginUser(s,new BCryptPasswordEncoder().encode("123456"),authorityLists);
loginUser.setNickName("成");
return loginUser;
}
public UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException {
// 通過手機號mobile去資料庫裡查詢使用者以及使用者許可權
List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
LoginUser loginUser = new LoginUser(mobile,new BCryptPasswordEncoder().encode("123456"),authorityLists);
loginUser.setNickName("成");
return loginUser;
}
}
5. 新建 SmsCodeAuthenticationSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyUserService MyUserService;
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setMyUserService(MyUserService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
6. 新建SmsCodeFilter過濾器,用於驗證簡訊驗證碼是否正確。使用過濾器來驗證簡訊驗證碼的好處是,可以任意設定哪些地址需要簡訊驗證碼驗證之後才能訪問,不僅僅只使用於登入。
@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private Set<String> urls = new HashSet<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
// 這裡配置需要攔截的地址 ......
urls.add("/authentication/mobile"); //
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) {
action = true;
break;
}
}
if (action) {
try {
validate(httpServletRequest);
} catch (SmsCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest request) {
SmsCode smsCode = (SmsCode)request.getSession().getAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
String smsCodeRequest = request.getParameter("smsCode");
if (smsCodeRequest == null || smsCodeRequest.isEmpty()) {
throw new SmsCodeException("簡訊驗證碼不能為空");
}
if (smsCode == null) {
throw new SmsCodeException("簡訊驗證碼不存在");
}
if (smsCode.isExpired()) {
request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
throw new SmsCodeException("簡訊驗證碼已過期");
}
if(!smsCodeRequest.equalsIgnoreCase(smsCode.getCode())) {
throw new SmsCodeException("簡訊驗證碼錯誤");
}
if(!smsCode.getMobile().equals(request.getParameter("mobile"))) {
throw new SmsCodeException("輸入的手機號與傳送簡訊驗證碼的手機號不一致");
}
request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
}
}
7. SpringSecurityConfig配置檔案,將SmsCodeAuthenticationSecurityConfig和SmsCodeFilter注入
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserService myUserService;
@Autowired
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler authenticationFailHandler;
@Autowired
private ImageCodeFilter imageCodeFilter;
@Autowired
private SmsCodeFilter smsCodeFilter;
// 注入簡訊登入的相關配置
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) // 將ImageCodeFilter過濾器設定在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/authentication/*","/login/*","/code/*") // 不需要登入就可以訪問
.permitAll()
.antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能訪問
.antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能訪問
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/authentication/login") // 訪問需要登入才能訪問的頁面,如果未登入,會跳轉到該地址來
.loginProcessingUrl("/authentication/form")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailHandler)
;
http.apply(smsCodeAuthenticationSecurityConfig);
}
// 密碼加密方式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 重寫方法,自定義使用者
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");
// auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
auth.userDetailsService(myUserService); // 注入MyUserService,這樣SpringSecurity會呼叫裡面的loadUserByUsername(String s)
}
}
程式碼地址 https://github.com/923226145/SpringSecurity/tree/master/chapter5