准备工作:
<!-- 导入security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
登录流程:
SecurityContextPersistenceFilter:是整个程序执行的入口,我们可以在这里做登录拦截判断,并验证我们的账户密码
第一步拦截请求,并验证手机验证码
/**
* 第一步:拦截请求,验证手机验证码是否正确
* 关靠这个无法执行,下一步需要写入口
*/
public class SmsCodeCheckFilter extends OncePerRequestFilter {
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
public void setMyAuthenticationFailureHandler(MyAuthenticationFailureHandler myAuthenticationFailureHandler) {
this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 不是做短信登录,直接放行
if(!new AntPathMatcher().match("/smsLogin", httpServletRequest.getRequestURI())) {
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
if (myAuthenticationFailureHandler != null) {
// 获取提交的手机号
String phone = httpServletRequest.getParameter("phone");
// 获取提交的验证码
String code = httpServletRequest.getParameter("code");
// 判断手机号和验证码
if (!StringUtils.hasText(phone)) {
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AccountExpiredException("手机号不能为空"));
return;
}
if (code == null) {
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AccountExpiredException("请填写验证码"));
return;
}
// 从session中获取手机号
String phoneFromSession = (String) httpServletRequest.getSession().getAttribute("phone");
// 从session中获取验证码
String codeFromSession = (String) httpServletRequest.getSession().getAttribute("code");
// 判断验证码
if (codeFromSession == null) {
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AccountExpiredException("验证码失效,请重新获取"));
return;
}
// 对比手机
if (!phone.equalsIgnoreCase(phoneFromSession)) {
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AccountExpiredException("验证码错误"));
return;
}
// 比对验证码
if (!code.equalsIgnoreCase(codeFromSession)) {
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AccountExpiredException("验证码错误"));
return;
}
// 放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
认证失败处理器:
/**
* 自定义认证失败处理器
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置响应字符集
httpServletResponse.setContentType("text/html;charset=utf8");
httpServletResponse.setCharacterEncoding("utf-8");
// 将错误信息告诉前端
httpServletResponse.getWriter().print(e.getMessage());
httpServletResponse.getWriter().flush();
httpServletResponse.getWriter().close();
}
}
认证成功处理器:
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 设置响应的字符集
httpServletResponse.setContentType("text/html;charset=utf8");
// 登录成功
httpServletResponse.getWriter().println("登录成功");
httpServletResponse.getWriter().println("用户" + authentication.getPrincipal()+"拥有权限:");
authentication.getAuthorities().forEach(grantedAuthority -> {
try {
httpServletResponse.getWriter().println(grantedAuthority.getAuthority());
} catch (IOException e) {
e.printStackTrace();
}
});
httpServletResponse.getWriter().flush();
httpServletResponse.getWriter().close();
}
}
UsernamePasswordAuthenticationFilter:封装的是用户名和密码,我们需要更换Security的验证规则为我们自己的,也就是说,我们需要重写这个类,实现我们自己的业务逻辑(我们只要封装一个手机号就好)。
第二步,自定义UsernamePasswordAuthenticationFilter
/**
* 第二步,入口大门,原本账号密码使用的是UsernamePasswordAuthenticationFilter
* 它比较的是账号和密码,而我们的短信登录是没有密码的,我们只要判断手机号是否存在就行、
* 因为在SmsCodeCheckFilter里已经验证过验证码了,所以我们这里要做的是拦截请求,获取参数
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/** 是否只支持post请求 */
private boolean postOnly = true;
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
public void setMyAuthenticationFailureHandler(MyAuthenticationFailureHandler myAuthenticationFailureHandler) {
this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
}
/**
* 这里必须要这个构造器
*/
public SmsAuthenticationFilter() {
// 第一个参数是登录的提交路径,第二个参数是请求方式
super(new AntPathRequestMatcher("/smsLogin", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
// 验证是否只能是post、验证请求方式是否是post
if (this.postOnly && !httpServletRequest.getMethod().equals("POST")) {
// 认证失败,报一个认证失败
myAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new InsufficientAuthenticationException("系统异常,请联系客服反馈"));
// 结束
return null;
}
// 从请求中获取手机号,验证码已在SmsCodeCheckFilter中进行
String phone = httpServletRequest.getParameter("phone");
// 判断是否没传过来
if (phone == null) {
// 根据UsernamePasswordAuthenticationFilter的写法,这里赋值为空字符
phone = "";
}
// 如果不为null,去掉前后空格
phone = phone.trim();
// 封装成为一个token对象,这里我们不能使用它原有的,因为他是账号和密码进行的校验,我们只有手机号
// 所以我们自定义一个自己的,这里转到SmsAuthenticationToken,完成类的设计
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(phone);
// 设置详细信息
this.setDetails(httpServletRequest, smsAuthenticationToken);
// 调用下一级,也就是认证管理,他是负责调用认证服务的,我们不用管,我们只需要修改认证服务就可以了
return this.getAuthenticationManager().authenticate(smsAuthenticationToken);
}
/**
* 这里对接收的token类进行了修改
* 由UsernamePasswordAuthenticationToken改为SmsAuthenticationToken
* @param request
* @param authRequest
*/
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
并且AbstractAuthenticationManager会封装成UsernamePasswordAuthenticationToken(其中包含了用户名和密码),而我们的手机验证码是免密登录是没有密码的,所以这里我们需要自定义我们自己的UsernamePasswordAuthenticationToken。
第三步,自定义UsernamePasswordAuthenticationToken
/**
* 第三步,设计一个我们自己需要的类
* 这里是根据UsernamePasswordAuthenticationToken来修改的
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 500L;
private final Object phone;
/**
* 认证完成之前,因为还没有权限
* 没有密码
* @param phone
*/
public SmsAuthenticationToken(Object phone) {
// 传了空权限
super((Collection)null);
// 设置手机号
this.phone = phone;
// 是否认证,这是一个标志,第一次肯定是没有认证的
this.setAuthenticated(false);
}
/**
* 这里是认证后,因为已经获取到权限了
* @param phone
* @param authorities
*/
public SmsAuthenticationToken(Object phone, Collection<? extends GrantedAuthority> authorities) {
// 权限列表
super(authorities);
// 设置手机号
this.phone = phone;
// 这个时候标志为true,已经认证过了
super.setAuthenticated(true);
}
/**
* 凭证:这里是密码
* @return
*/
@Override
public Object getCredentials() {
return this.phone;
}
/**
* 主要的:这里是账号
* @return
*/
@Override
public Object getPrincipal() {
return this.phone;
}
}
AuthenticationManager会调用DaoAuthenticationProvider(AuthenticationProvider)去进行认证,DaoAuthenticationProvider通过调用UserDetailsService从数据库中获取用户信息(UserDetails),之后使用passwordEncoder密码编码器对UsernamePasswordAuthenticationToken(我们已重写,并去除密码)中的密码和数据库中UserDetails(数据库中只有手机号,也没有密码)的密码进行比较。当认证成功后会封装成一个Authentication(包含用户名,密码,权限等)。
第四步,自定义认证比对规则
/**
* 第四步,自定义认证服务,它的认证服务调用的是passwordEncoder
* 判断的是密码。而我们没有密码,我们无需判断密码,只需要将对象查出来证明有就可以了
* 所以我们自己写一个这样的类
*/
public class SmsDaoAuthenticationProvider extends DaoAuthenticationProvider {
private UserDetailsService userDetailsService;
/**
* 这个方法必须要实现,如果该类直接实现AbstractUserDetailsAuthenticationProvider
* 就不需要写这个方法了,这个方法是用来校验密码的,不过我们不需要,我们没有密码
* 这里我把它清空,为了防止不必要的麻烦
* @param userDetails
* @param authentication
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
return;
}
/**
* 核心方法,做认证处理
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 断言判断这个类
Assert.isInstanceOf(SmsAuthenticationToken.class, authentication, "只支持的类型为:SmsAuthenticationToken,所传入的类型为:" + authentication.getClass().getSimpleName());
// 取出手机号
String phone = (String) authentication.getPrincipal();
// 断言手机号
Assert.hasText(phone, "没有该手机号");
// 不从缓存中拿,我们从数据库中获取
// 这里是把手机号当作用户名,用我们之前的查询语句
UserDetails user = getUserDetailsService().loadUserByUsername(phone);
// 原方法为: this.preAuthenticationChecks.check(user); 这里就不调用方法了,直接判断
if (!user.isAccountNonLocked()) {
throw new LockedException(this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));
} else if (!user.isEnabled()) {
throw new DisabledException(this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(this.messages.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
// 返回一个用户,意为已经认证过了
return this.createSuccessAuthentication(phone, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
// 我们只有手机号和权限
SmsAuthenticationToken result = new SmsAuthenticationToken(principal, user.getAuthorities());
// 设置详细信息
result.setDetails(authentication.getDetails());
// 返回结果
return result;
}
/**
* 这里说的是支持的类,在AbstractUserDetailsAuthenticationProvider中
* 它是这样的:
* return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
* 意味着他自己支持UsernamePasswordAuthenticationToken,而我们自己定义了一个,叫SmsAuthenticationToken
* 所以改为我们自己的
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
当我们做完这些工作之后,Security的认证流程是不知道我们的这些类的,所以我们需要添加一个配置,告诉Security使用的是我们自定义的,而不是原始的类。
最后一步,自定义配置
// security的配置
@Configuration
@EnableWebSecurity // 开启Security
public class SmsAuthConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private IVipUserService vipUserService;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
// 设置入口大门的认证失败处理
// 创建拦截类
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
// 成功的处理
smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
// 失败的处理
smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
// 设置认证管理类。由于我们自定义,现在指定一个原有的认证管理类
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置dao层的DetailsService为我们自定义的
SmsDaoAuthenticationProvider smsDaoAuthenticationProvider = new SmsDaoAuthenticationProvider();
smsDaoAuthenticationProvider.setUserDetailsService(vipUserService);
// 添加短信认证provider
http.authenticationProvider(smsDaoAuthenticationProvider);
// 添加短信认证filter
http.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}