實現流程
前排提示:需要對spring security底層用戶名密碼登陸源碼有所了解。不了解的可以看我上一篇博客:https://www.cnblogs.com/wwjj4811/p/14474866.html
類比用戶名密碼登陸流程:
1.進入MobileValidateFilter
,對用戶輸入的驗證碼進行校驗比對
2.手機認證過濾器MobileAuthenticationFilter
,校驗手機號是否存在
3.自定義MobileAuthenticationToken
提供給 MobileAuthenticationFilter
4.自定義MobileAuthenticationProvider
提供給 ProviderManager
處理
5.創建針對手機號查詢用戶信息的MobileUserDetailsService
,交給 MobileAuthenticationProvider
6.自定義MobileAuthenticationConfig
配置類將上面組件連接起來,添加到容器中
7.將MobileAuthenticationConfig
添加到spring security安全配置的過濾器鏈上
准備工作
自定義全局頁面響應類
@Data
public class R {
// 響應業務狀態
private Integer code;
// 響應消息
private String message;
// 響應中的數據
private Object data;
public R() {
}
public R(Object data) {
this.code = 200;
this.message = "OK";
this.data = data;
}
public R(String message, Object data) {
this.code = 200;
this.message = message;
this.data = data;
}
public R(Integer code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public static R ok() {
return new R(null);
}
public static R ok(String message) {
return new R(message, null);
}
public static R ok(Object data) {
return new R(data);
}
public static R ok(String message, Object data) {
return new R(message, data);
}
public static R build(Integer code, String message) {
return new R(code, message, null);
}
public static R build(Integer code, String message, Object data) {
return new R(code, message, data);
}
public String toJsonString() {
return JSON.toJSONString(this);
}
/**
* JSON字符串轉成 R 對象
*/
public static R format(String json) {
try {
return JSON.parseObject(json, R.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
自定義認證成功處理器和失敗處理器
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if(LoginResponseType.JSON.equals(securityProperties.getAuthentication().getLoginType())){
R result = R.ok("認證成功");
String string = result.toJsonString();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(result.toJsonString());
}else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if(LoginResponseType.JSON.equals(securityProperties.getAuthentication().getLoginType())){
R result = R.build(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
String string = result.toJsonString();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(result.toJsonString());
}else {
String referer = request.getHeader("Referer");
String lastUrl = StringUtils.substringBefore(referer,"?");
super.setDefaultFailureUrl(lastUrl+"?error");
super.onAuthenticationFailure(request, response, exception);
}
}
}
前端代碼thymeleaf(省略css)
<form th:action="@{/mobile/form}" action="index.html" method="post">
<div class="input-group mb-3">
<input id="mobile" name="mobile" type="text" class="form-control" placeholder="手機號碼">
<div class="input-group-append">
<div class="input-group-text">
<span class="fa fa-user"></span>
</div>
</div>
</div>
<div class="mb-3 row">
<div class="col-7">
<input type="text" name="code" class="form-control" placeholder="驗證碼">
</div>
<div class="col-5">
<a id="sendCode" th:attr="code_url=@{/code/mobile?mobile=}" class="btn btn-outline-primary btn-large" href="#"> 獲取驗證碼 </a>
</div>
</div>
<div class="col-4">
<button type="submit" class="btn btn-primary btn-block">登錄</button>
</div>
</div>
</form>
配置類
@Data
@Component
@ConfigurationProperties(prefix = "boot.security")
public class SecurityProperties {
private AuthenticationProperties authentication;
@Data
public static class AuthenticationProperties {
private String[] permitPaths;
}
}
配置文件:
boot:
security:
authentication:
permitPaths:
- /code/mobile
- /mobile/page
- /mobile/form
發送驗證碼模擬
public interface SmsSend {
boolean sendSms(String mobile, String content);
}
@Slf4j
@Component
public class SmsCodeSender implements SmsSend{
@Override
public boolean sendSms(String mobile, String content) {
String sendContent = String.format("短信驗證碼:%s", content);
log.info("手機號:{},驗證碼:{}",mobile,content);
return true;
}
}
發送驗證碼接口
@Controller
public class MobileLoginController {
public static final String SESSION_KEY = "SESSION_KEY_MOBILE_CODE";
@Autowired
SmsSend smsSend;
/**
* 前往手機短信登錄頁
*/
@RequestMapping("/mobile/page")
public String toMobilePage(){
return "login-mobile";
}
@RequestMapping("/code/mobile")
@ResponseBody
public R smsCode(HttpServletRequest request){
//這里用apache的隨機數工具類生成模擬發送驗證碼
//並將驗證碼存入session中,實際工作中可以存入到redis中,並加上電話號碼作為標識
String code = RandomStringUtils.randomNumeric(4);
request.getSession().setAttribute(SESSION_KEY, code);
smsSend.sendSms(request.getParameter("mobile"), code);
return R.ok();
}
}
實現MobileUserDetailsService
需要實現UserDetailsService接口。這里我就不查詢數據庫了,直接默認能查詢到一個User。(實際工作根據需要去實現該方法)
/**
* 通過手機號獲取用戶信息和權限
*/
@Component
public class MobileUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
//模擬查到了用戶
return new User("wj", "",true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
流程實現
MobileValidateFilter實現
@Component
public class MobileValidateFilter extends OncePerRequestFilter {
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.判斷是否是手機登錄,且post請求
if ("/mobile/form".equals(request.getRequestURI())
&& "post".equalsIgnoreCase(request.getMethod())) {
try {
validate(request);
} catch (AuthenticationException e) {
//處理異常
customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
//放行請求
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) {
//從session中獲取驗證碼
String sessionCode = (String)request.getSession().getAttribute(MobileLoginController.SESSION_KEY);
String code = request.getParameter("code");
//判斷驗證碼是否為空
if(StringUtils.isBlank(code)){
throw new ValidateCodeException("驗證碼不能為空");
}
//判斷session中驗證碼和用戶輸入的是否相同
if(!sessionCode.equalsIgnoreCase(code)){
throw new ValidateCodeException("驗證碼輸入錯誤");
}
}
}
MobileAuthenticationFilter實現
模仿UseranamePasswordAuthenticationFilter的實現,繼承AbstractAuthenticationProcessingFilter抽象類,因為輸入驗證碼,不需要用戶輸入密碼,所以比UseranamePasswordAuthenticationFilter少一個字段
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private boolean postOnly = true;
public MobileAuthenticationFilter() {
super(new AntPathRequestMatcher("/mobile/form", "POST"));
}
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();
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);
//sessionID,hostname
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 獲取用戶輸入的電話號碼
*/
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request,
MobileAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
MobileAuthenticationToken實現
模仿UseranamePasswordAuthenticationToken,繼承AbstractAuthenticationToken
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;//認證前是手機號,認證后是用戶信息
public MobileAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public MobileAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
/**
* 不需要密碼,所以返回一個null
*/
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
MobileAuthenticationProvider實現
DaoAuthenticationProvider的繼承樹如下:
我們需要實現AuthenticationProvider接口:
public class MobileAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 認證處理:
* 1.通過手機號碼,查詢用戶信息(UserDetailsService實現)
* 2.查詢到用戶信息,則認為認證通過,封裝Authentication對象
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobileAuthenticationToken mobileAuthenticationToken
= (MobileAuthenticationToken)authentication;
String mobile = (String) mobileAuthenticationToken.getPrincipal();
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
if(Objects.isNull(userDetails)){
throw new AuthenticationServiceException("手機號未注冊");
}
//認證通過
MobileAuthenticationToken token = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities());
token.setDetails(mobileAuthenticationToken.getDetails());
return token;
}
/**
* 通過這個方法,來選擇對應的Provider,即選擇MobileAuthenticationProvider
*/
@Override
public boolean supports(Class<?> authentication) {
return (MobileAuthenticationToken.class.isAssignableFrom(authentication));
}
}
MobileAuthenticationConfig配置類
@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
UserDetailsService mobileUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
mobileAuthenticationFilter.setAuthenticationManager(
http.getSharedObject(AuthenticationManager.class));
//成功和失敗的處理器
mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailsService);
//provider綁定到HttpSecurity上
// MobileAuthenticationFilter放到UsernamePasswordAuthenticationFilter之后
http.authenticationProvider(provider)
.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
spring security配置類
@Slf4j
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private MobileValidateFilter mobileValidateFilter;
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig;
/**
* @Author wen.jie
* @Description 資源權限配置
* @Date 2021/3/1 13:55
**/
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定義驗證表單
http.addFilterBefore(mobileValidateFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage(securityProperties.getAuthentication().getLoginPage())
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.authorizeRequests()//認證請求
.antMatchers(securityProperties.getAuthentication().getPermitPaths()).permitAll()//放行指定請求
.anyRequest().authenticated();//所有訪問請求都需認證
//將手機認證添加到過濾器鏈上
http.apply(mobileAuthenticationConfig);
}
}
啟動項目,控制台會打印security的過濾器鏈
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
MobileValidateFilter
UsernamePasswordAuthenticationFilter
MobileAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
記住我功能
默認情況下,記住我功能使用的是NullRememberMeServices
修改很簡單,MobileAuthenticationConfig中添加一行
@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
UserDetailsService mobileUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
mobileAuthenticationFilter.setAuthenticationManager(
http.getSharedObject(AuthenticationManager.class));
//記住我功能
mobileAuthenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class));
//成功和失敗的處理器
mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//設置provider的UserDetailsService
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailsService);
//provider綁定到HttpSecurity上
// MobileAuthenticationFilter放到UsernamePasswordAuthenticationFilter之后
http.authenticationProvider(provider)
.addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
同時修改spring security配置類:
@Slf4j
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private SecurityProperties securityProperties;
@Autowired @Qualifier("customUserDetailsService")
private UserDetailsService userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private MobileValidateFilter mobileValidateFilter;
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig;
@Bean
public JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//是否啟動項目時自動創建表
jdbcTokenRepository.setCreateTableOnStartup(false);
return jdbcTokenRepository;
}
/**
* @Author wen.jie
* @Description 資源權限配置
* @Date 2021/3/1 13:55
**/
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定義驗證表單
http.addFilterBefore(mobileValidateFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailureHandler)
.and()
.authorizeRequests()//認證請求
.antMatchers(securityProperties.getAuthentication().getPermitPaths()).permitAll()//放行指定請求
.anyRequest().authenticated()//所有訪問請求都需認證
.and()
.rememberMe()//記住我
.tokenRepository(jdbcTokenRepository())//保存登陸信息
.tokenValiditySeconds(60*60*24*7);//記住我功能有效時常
//將手機認證添加到過濾器鏈上
http.apply(mobileAuthenticationConfig);
}
}
記住我功能的表結構可自行百度