准备工作:


<!-- 导入security依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>


登录流程:

spring security 登录流程加上短信验证码 spring security手机验证码登录_开发语言

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);
    }
}