一. 概述
整合SpringSecurity实现手机号码登录, 首先要了解SpringSecurity默认的账号密码登录流程
1.1 SpringSecurity默认账号密码认证流程
SpringSecurity默认的账号密码验证主要由
UsernamePasswordAuthenticationFilter
完成的,可以从UsernamePasswordAuthenticationFilter
入手
参考UsernamePasswordAuthenticationFilter详解
-
拦截登录请求:默认
/login
,可以通过配置HttpSecurity.loginProcessingUrl("/login")
设定 -
尝试认证:调用方法
UsernamePasswordAuthenticationFilter.attemptAuthentication()
-
查找具体Provider认证处理:调用
AuthenticationManager.authenticate(Authentication authentication)
方法认证返回认证信息.AuthenticationManager
管理着Provider
,会根据传参的Token
类型找到对应的Provider
处理,UsernamePasswordAuthenticationToken
对应的Provider
为DaoAuthenticationProvider
-
Session 策略处理: 认证没有错误就进行Session 策略处理,
sessionStrategy.onAuthentication(authResult, request, response);
-
认证成功:
SecurityContextHolder.getContext().setAuthentication(authResult)
把认证结果放到当前线程,这一步就表示认证成功 -
记住我策略处理:
rememberMeServices.loginSuccess(request, response, authResult);
-
认证失败处理: 调用
SecurityContextHolder.clearContext();
清除认证结果,调用rememberMeServices.loginFail(request, response);
记住我逻辑处理,调用failureHandler.onAuthenticationFailure(request, response, failed);
失败处理
1.2 实现手机号码登录思路
清楚账号密码认证流程后,实现手机号码登录思路就清晰了, 模仿一下账号密码认证流程就行
- 编写短信验证码校验过滤器
SmsCodeFilter
- 模仿
UsernamePasswordAuthenticationFilter
重写一个手机号码认证过滤PhoneNumAuthenticationFilter
- 模仿
UsernamePasswordAuthenticationToken
重写一个手机号码TOKENPhoneNumAuthenticationToken
- 模仿
DaoAuthenticationProvider
重写一个手机号码登录认证处理DaoPhoneNumAuthenticationProvider
二. SpringBootDemo
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.1 手机号码认证TOKEN : PhoneNumAuthenticationToken.java
/**
* 手机号码认证TOKEN
*/
public class PhoneNumAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* principal的作用有两个, 在未登录之前是用户名,那么在登录之后是用户的信息。
*/
private final Object principal;
/**
* 构造
* @param principal 手机号码
*/
public PhoneNumAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(false);
}
/**
* 构造
*
* @param principal 用户信息
* @param authorities 用户权限列表
*/
public PhoneNumAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(true);
}
/**
* 正常这个是返回密码,但手机登录没有密码,不用管
* @return
*/
@Override
public Object getCredentials() {
return null;
}
/**
* 获取手机号或用户信息
* @return
*/
@Override
public Object getPrincipal() {
return this.principal;
}
}
2.2 手机号码登录认证处理: PhoneNumAuthenticationToken.java
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 手机号码认证TOKEN
*/
public class PhoneNumAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* principal的作用有两个, 在未登录之前是用户名,那么在登录之后是用户的信息。
*/
private final Object principal;
/**
* 构造
* @param principal 手机号码
*/
public PhoneNumAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(false);
}
/**
* 构造
*
* @param principal 用户信息
* @param authorities 用户权限列表
*/
public PhoneNumAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// 用于指示AbstractSecurityInterceptor是否应向AuthenticationManager提供身份验证令牌
setAuthenticated(true);
}
/**
* 正常这个是返回密码,但手机登录没有密码,不用管
* @return
*/
@Override
public Object getCredentials() {
return null;
}
/**
* 获取手机号或用户信息
* @return
*/
@Override
public Object getPrincipal() {
return this.principal;
}
}
2.3 手机号码认证过滤: PhoneNumAuthenticationFilter.java
/**
* 手机号码拦截器, 获取手机号码
* @author a10.11.5
*/
public class PhoneNumAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public PhoneNumAuthenticationFilter() {
super(new AntPathRequestMatcher("/phoneLogin", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (!Objects.equals(request.getMethod(),"POST")) {
throw new AuthenticationServiceException(
"身份验证方法需为:'POST'请求");
}
// 获取手机号
String phoneNum = Optional.ofNullable(request.getParameter(Constants.PHONE_NUM_PARAMETER)).map(String::trim).orElse("");
// new 手机号码验证Token
PhoneNumAuthenticationToken authRequest = new PhoneNumAuthenticationToken(phoneNum);
// 身份验证详细信息
authRequest.setDetails(super.authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}
}
2.4 短信验证码过滤器: SmsCodeFilter.java
/**
* 短信验证码验证过滤器
*/
@Component
public class SmsCodeFilter extends OncePerRequestFilter {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
/**
* uri = /phoneLogin 即手机号码登录才拦截
*/
if (Objects.equals(Constants.SMS_LOGIN_URI,request.getRequestURI())) {
try{
// 验证手机验证码
validateProcess(request);
}catch (AuthenticationException ex) {
customizeAuthencationFailureHandler.onAuthenticationFailure(request, response, ex);
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 验证手机验证码
* @param request
*/
private void validateProcess(HttpServletRequest request){
// 获取手机号
String msgCode = stringRedisTemplate.opsForValue().get(Constants.SMS_CODE_SESSION_KEY);
String code = request.getParameter(Constants.MSG_CODE);
if(Strings.isBlank(code)) {
throw new InternalAuthenticationServiceException("短信验证码不能为空.");
}
if(null == msgCode) {
throw new InternalAuthenticationServiceException("短信验证码已失效.");
}
if(!code.equals(msgCode)) {
throw new InternalAuthenticationServiceException("短信验证码错误.");
}
}
}
2. 4 Security 配置 SecurityConfig.java
把
phoneNumAuthenticationFilter
和DaoPhoneNumAuthenticationProvider
&SmsCodeFilter
装进配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 数据源
*/
@Resource
private DataSource dataSource;
/**
* 用户信息服务
*/
@Resource
private UserAuthentication userAuthentication;
/**
* 成功处理
*/
@Resource
private CustomizeAuthencationSuccessHandler customizeAuthencationSuccessHandler;
/**
* 失败处理
*/
@Resource
private CustomizeAuthencationFailureHandler customizeAuthencationFailureHandler;
/**
* 用户登出处理
*/
@Resource
private UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 多用户登录处理
*/
@Resource
private MutilpleSessionHandler mutilpleSessionHandler;
/**
* 没有访问权限处理
*/
@Resource
private NoPermitAccessHandler noPermitAccessHandler;
/**
* 密码编码器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 手机号码登录验证处理
*/
@Resource
private DaoPhoneNumAuthenticationProvider daoPhoneNumAuthenticationProvider;
/**
* 信息验证码过滤器
*/
@Resource
private SmsCodeFilter smsCodeFilter;
/**
* 把AuthenticationManager公开
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置自定义验证查询/加密工具
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userAuthentication).passwordEncoder(passwordEncoder());
}
/**
* `记住我`功能配置
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(false);
return jdbcTokenRepository;
}
/**
* 手机号码登录拦截器
* @return
*/
@Bean
public PhoneNumAuthenticationFilter phoneNumAuthenticationFilter() throws Exception {
// 手机号码拦截器, 获取手机号码
PhoneNumAuthenticationFilter phoneNumAuthenticationFilter = new PhoneNumAuthenticationFilter();
phoneNumAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
//使用手机号登录失败了如何处理
phoneNumAuthenticationFilter.setAuthenticationFailureHandler(customizeAuthencationFailureHandler);
// 使用手机号登录成功了如何处理
phoneNumAuthenticationFilter.setAuthenticationSuccessHandler(customizeAuthencationSuccessHandler);
return phoneNumAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 加入短信验证码过滤器
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 加入手机号码登录过滤器
.addFilterAfter(phoneNumAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 加入手机号码登录验证提供者
.authenticationProvider(daoPhoneNumAuthenticationProvider)
// 表单登录
.formLogin()
// 未登录跳转登录页面
.loginPage("/login.html")
// 指定登录路径
.loginProcessingUrl("/login")
// 用户登录成功的处理
.successHandler(customizeAuthencationSuccessHandler)
// 用户登录失败的处理
.failureHandler(customizeAuthencationFailureHandler)
// 记住我
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
// 因为用户传入过来的token, 需要再次进行校验
.userDetailsService(userAuthentication)
.tokenValiditySeconds(3600)
// .alwaysRemember(true)
// 认证配置
.and()
.authorizeRequests()
//不拦截的Url
.antMatchers("/login.html", "/image/code", "/smsCode", "/css/**", "/js/**", "/phoneLogin").permitAll()
.anyRequest() //所有的请求
.authenticated() //认证之后就可以访问
// 多端登录限制,限制一个账号同时只能一个人登录
.and()
.sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(mutilpleSessionHandler)
.and()
// 登出配置
.and()
.logout()
.logoutUrl("/logout")
// 登出成功处理
.logoutSuccessHandler(userLogoutSuccessHandler)
// 无权限访问配置
.and()
.exceptionHandling()
.accessDeniedHandler(noPermitAccessHandler)
.and()
.csrf().disable();
}