springboot2.3集成springsecurity实现模拟短信验证码登陆

基本原理:登陆页请求后台生成需要验证的验证码,将验证码和手机号存入session中,同时将验证码发送到手机(这里只是模拟)。真正登陆的时候,先通过自定义的验证输入验证码是否正确的过滤器。然后再执行通过手机号进行认证的filter,provider(都是自定义实现)等,实现认证,登陆系统。

实现

1 创建一个短信验证码的实体,里边存放需要校验的字符串,过期时间,手机号

package com.itgo.springboot.springsecurity.config;

import java.time.LocalDateTime;

/**
 * @Description: 定义的获取短信验证码的实体
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 18:02
 * @Copyright: (c) 2019-2022  XXXX公司
 */
public class SmsCode {
    // 验证码
    private String code;
    // 设置过期的时间
    private LocalDateTime expireTime;

    private String mobile;

    public SmsCode(String code, int expireTimeData,String mobile) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTimeData);
        this.mobile = mobile;
    }

    /**
     * @Description: 是否过期
     **/
    public boolean isExpire(){
        return LocalDateTime.now().isAfter(expireTime);
    }

    public String getCode() {
        return code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
}

2:后台生成谜底

package com.itgo.springboot.springsecurity.controller;

import com.fasterxml.jackson.databind.util.JSONPObject;
import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.SmsCode;
import com.itgo.springboot.springsecurity.entity.SysUser;
import com.itgo.springboot.springsecurity.service.SysUserService;
import com.itgo.springboot.utils.MyContants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;

/**
 * @Description: 模拟短信登陆的控制器
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 17:32
 * @Copyright: (c) 2019-2022  XXXX公司
 */
@RestController
@Slf4j
public class CmsController {

    @Resource
    private SysUserService sysUserService;
    @Resource
    private MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;


    @GetMapping("/cms/getCmsCodeByMobile")
    @ResponseBody
    public String getCmsCodeByMobile(HttpServletRequest request, HttpServletResponse response, HttpSession session,
                                          String mobile) throws IOException, ServletException {
        // 先通过电话号码查询用户
        SysUser sysUser = sysUserService.loadUserByUsername(mobile);
        if (Objects.isNull(sysUser)) {
            return mobile+"未注册!";
        }
        // 通过调用短信提供商的接口,获取验证码,这里只是模拟
        SmsCode smsCode = new SmsCode(RandomStringUtils.randomNumeric(4), 60, mobile);
        // 保存到session中
        log.info("验证码:" + smsCode.getCode());
        session.setAttribute(MyContants.CMS_CODE_KEY, smsCode);

        return "短信验证已经发送!"+smsCode.getCode();
    }
}

3:验证输入的验证码是否有效的过滤器(只是做验证码是否有效,还没有涉及到认证)

package com.itgo.springboot.springsecurity.config.sms;

import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.SmsCode;
import com.itgo.springboot.springsecurity.entity.SysUser;
import com.itgo.springboot.springsecurity.service.SysUserService;
import com.itgo.springboot.utils.MyContants;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;

/**
 * @Description: 自定义的验证短信验证码是否有效 的过滤器
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 21:36
 * @Copyright: (c) 2019-2022  XXXX公司
 */
@Component
public class SmsValidCodeFilter extends OncePerRequestFilter {

    @Resource
    SysUserService sysUserService;
    @Resource
    MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 做校验
        if ("/loginBysms".equals(request.getRequestURI()) && "post".equalsIgnoreCase(request.getMethod())) {
            try {
                validaSms(request);
            } catch (AuthenticationException e) {
                mySimpleUrlAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }

        // 如果一切正常,直接放行
        filterChain.doFilter(request, response);
    }


    /**
     * @Description: 校验短信是否正确
     * @author libiao
     * @Date 2020-6-22 21:39
     * @Param
     * @return
     **/
    private void validaSms(HttpServletRequest request) {
        String mobileInRequest = request.getParameter("mobile");
        String smsCodeInRequest = request.getParameter("smsCode");
        if (StringUtils.isBlank(mobileInRequest)) {
            throw new SessionAuthenticationException("手机号不能为空!");
        }
        if (StringUtils.isBlank(smsCodeInRequest)) {
            throw new SessionAuthenticationException("验证码不能为空!");
        }
        // 校验手机号是否 在系统中注册
        SysUser sysUser = sysUserService.loadUserByUsername(mobileInRequest);
        if (Objects.isNull(sysUser)) {
            throw new  SessionAuthenticationException("手机号不在系统中注册!");
        }
        // 校验输入的手机号是否和session中的手机号一致
        HttpSession session = request.getSession();
        SmsCode smsCode = (SmsCode) session.getAttribute(MyContants.CMS_CODE_KEY);
        if (Objects.isNull(smsCode)) {
            throw new  SessionAuthenticationException("系统中没有生成验证码!");
        } else if (StringUtils.isBlank(smsCode.getCode())) {
            throw new  SessionAuthenticationException("系统中没有生成验证码!");
        }
        if (!StringUtils.equalsIgnoreCase(mobileInRequest, smsCode.getMobile())) {
            throw new  SessionAuthenticationException("输入的手机号和短信发送的手机号不一致!");
        }
        // 验证码是否过期
        if (smsCode.isExpire()) {
            session.removeAttribute(MyContants.CMS_CODE_KEY);
            throw new  SessionAuthenticationException("验证码已经过期!");
        }
        // 验证码是否一致
        if (!StringUtils.equalsIgnoreCase(smsCodeInRequest, smsCode.getCode())) {
            throw new  SessionAuthenticationException("验证码输入错误!");
        }

        // 通过验证,删除session中的值
        session.removeAttribute(MyContants.CMS_CODE_KEY);
    }
}

4:如果通过了验证码的过滤器,则需要执行通过手机号认证的过滤器(模仿UsernamePasswordAuthenticationFilter实现)

package com.itgo.springboot.springsecurity.config.sms;

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Description: 依照
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 21:54
 * @Copyright: (c) 2019-2022  XXXX公司
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileKeyParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/loginBysms", "POST"));
    }



    @Override
    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 = obtainUsername(request);
        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();
        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }


    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(mobileKeyParameter);
    }


    protected void setDetails(HttpServletRequest request,
                              SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }


    public void setUsernameParameter(String mobileKeyParameter) {
        Assert.hasText(mobileKeyParameter, "mobile parameter must not be empty or null");
        this.mobileKeyParameter = mobileKeyParameter;
    }



    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileKeyParameter() {
        return mobileKeyParameter;
    }


}

5:自定义SmsAuthenticationToken。(模拟UsernamePasswordAuthenticationToken)

package com.itgo.springboot.springsecurity.config.sms;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
 * @Description: 自定义的通过短信验证码实现登陆的Token,仿照UsernamePasswordAuthenticationToken
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 22:03
 * @Copyright: (c) 2019-2022  XXXX公司
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;


    public SmsAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     *
     * @param principal
     * @param authorities
     */
    public SmsAuthenticationToken(Object principal,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }
    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

6:SmsCodeAuthenticationFilter需要通过AuthenticationManager调用Provider来具体认证

package com.itgo.springboot.springsecurity.config.sms;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.Objects;

/**
 * @Description: 由SmsCodeAuthenticationFilter调用这个provider实现验证 ,仿照
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 22:10
 * @Copyright: (c) 2019-2022  XXXX公司
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {



    private UserDetailsService userDetailsService;


    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }


    /**
     * @Description: 实现验证
     * @author libiao
     * @Date 2020-6-22 22:17
     * @Param
     * @return
     **/
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        // 认证主体,是有SmsCodeAuthenticationFilter调用,主体放入的就是手机号
        String mobile = (String) authenticationToken.getPrincipal();
        // 通过手机号去加载用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        if (Objects.isNull(userDetails)) {
            throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
        }
        // 设置认证过后的Token
        SmsAuthenticationToken smsAuthenticationTokenResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        smsAuthenticationTokenResult.setDetails(authenticationToken.getDetails());

        return smsAuthenticationTokenResult;
    }

    /**
     * @Description: 如果是SmsCodeAuthenticationFilter调用的,才执行这个provider进行手机号的认证
     * @author libiao
     * @Date 2020-6-22 22:22
     * @Param
     * @return
     **/
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

7:单独一个配置类,配置

package com.itgo.springboot.springsecurity.config.sms;

import com.itgo.springboot.springsecurity.config.MySavedRequestAwareAuthenticationSuccessHandler;
import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.UserDetailsServiceImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @Description: 自定义的通过短信验证用户的配置
 * @auther: libiao
 * @Email: libiao@163.com
 * @Date: 2020-6-22 22:23
 * @Copyright: (c) 2019-2022  XXXX公司
 */
@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Resource
    MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;
    @Resource
    MySavedRequestAwareAuthenticationSuccessHandler mySavedRequestAwareAuthenticationSuccessHandler;
    @Resource
    UserDetailsServiceImpl userDetailsService;
    @Resource
    SmsValidCodeFilter smsValidCodeFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setPostOnly(true);
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(mySavedRequestAwareAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(mySimpleUrlAuthenticationFailureHandler);
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        // 设置校验验证码和用手机号加载认证的过滤器执行顺序
        http.addFilterBefore(smsValidCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authenticationProvider(smsCodeAuthenticationProvider);
        http.addFilterAfter(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);

    }
}

8:在springsecurity的配置类中,配置短信认证的config,使其生效

// 设置短信验证的配置类有效
        http.apply(smsCodeSecurityConfig);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值