实现流程
前排提示:需要对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);
}
}
记住我功能的表结构可自行百度