SpringBoot整合Spring Security,使用短信验证码方式进行登录(七)

简介: 该类和图片验证码差不多,只是多个手机号判断,需要注意的是,这里拦截的是/smslogin接口,该接口为短信登录的接口,该接口不像/login接口由security提供,需要我们自己实现。

一、编写短信验证码实体类

package com.example.securityzimug.config.auth.smscode;
import java.time.LocalDateTime;
public class SmsCode {
    private String code; //短信验证码
    private LocalDateTime expireTime; //过期时间
    private String mobile;
    public SmsCode(String code, int expireAfterSeconds,String mobile){
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
        this.mobile = mobile;
    }
    public boolean isExpired(){
        return  LocalDateTime.now().isAfter(expireTime);
    }
    public String getCode() {
        return code;
    }
    public String getMobile() {
        return mobile;
    }
}


二、编写控制器,获取验证码接口

package com.example.securityzimug.controller;
import com.example.securityzimug.config.auth.MyUserDetails;
import com.example.securityzimug.config.auth.MyUserDetailsServiceMapper;
import com.example.securityzimug.config.auth.exception.AjaxResponse;
import com.example.securityzimug.config.auth.exception.CustomException;
import com.example.securityzimug.config.auth.exception.CustomExceptionType;
import com.example.securityzimug.config.auth.smscode.SmsCode;
import com.example.securityzimug.utils.MyContants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.Random;
@Slf4j
@RestController
public class SmsController {
    @Resource
    MyUserDetailsServiceMapper myUserDetailsServiceMapper;
    @RequestMapping(value = "/smscode",method = RequestMethod.GET)
    public AjaxResponse sms(@RequestParam String mobile, HttpSession session){
        //检查该手机号是否注册,没注册则不能通过手机号登录
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(mobile);
        if(myUserDetails == null){
            return AjaxResponse.error(
                    new CustomException(CustomExceptionType.USER_INPUT_ERROR,
                            "您输入的手机号未曾注册")
            );
        }
        SmsCode smsCode = new SmsCode(String.valueOf(new Random().nextInt(9000)+1000),60,mobile);
        //TODO 调用短信服务提供商的接口发送短信
        //模拟发送了短信
        log.info(smsCode.getCode()  + "+>" + mobile);
        session.setAttribute(MyContants.SMS_SESSION_KEY,smsCode);
        return AjaxResponse.success("短信验证码已经发送");
    }
}

然后记得放行该接口:

20200923232351394.png


三、编写过滤器

该类和图片验证码差不多,只是多个手机号判断,需要注意的是,这里拦截的是/smslogin接口,该接口为短信登录的接口,该接口不像/login接口由security提供,需要我们自己实现。

package com.example.securityzimug.config.auth.smscode;
import com.example.securityzimug.config.auth.MyAuthenticationFailureHandler;
import com.example.securityzimug.config.auth.MyUserDetails;
import com.example.securityzimug.config.auth.MyUserDetailsServiceMapper;
import com.example.securityzimug.utils.MyContants;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;
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;
@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {
    @Resource
    MyUserDetailsServiceMapper myUserDetailsServiceMapper;
    @Resource
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        if(StringUtils.equals("/smslogin",request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
            try{
                //验证谜底与用户输入是否匹配
                validate(new ServletWebRequest(request));
            }catch(AuthenticationException e){
                myAuthenticationFailureHandler.onAuthenticationFailure(
                        request,response,e
                );
                return;
            }
        }
        filterChain.doFilter(request,response);
    }
    //验证规则
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        HttpSession session = request.getRequest().getSession();
        SmsCode codeInSession = (SmsCode)session.getAttribute(MyContants.SMS_SESSION_KEY);
        String mobileInRequest = request.getParameter("mobile");
        String codeInRequest = request.getParameter("smsCode");
        if(StringUtils.isEmpty(mobileInRequest)){
            throw new SessionAuthenticationException("手机号码不能为空");
        }
        if(StringUtils.isEmpty(codeInRequest)) {
            throw new SessionAuthenticationException("短信验证码不能为空");
        }
        if(Objects.isNull(codeInSession)) {
            throw new SessionAuthenticationException("短信验证码不存在");
        }
        if(codeInSession.isExpired()) {
            session.removeAttribute(MyContants.SMS_SESSION_KEY);
            throw new SessionAuthenticationException("短信验证码已经过期");
        }
        if(!codeInSession.getCode().equals(codeInRequest)) {
            throw new SessionAuthenticationException("短信验证码不正确");
        }
        if(!codeInSession.getMobile().equals(mobileInRequest)) {
            throw new SessionAuthenticationException("短信发送目标与您输入的手机号不一致");
        }
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(mobileInRequest);
        if(Objects.isNull(myUserDetails)){
            throw new SessionAuthenticationException("您输入的手机号不是系统的注册用户");
        }
        session.removeAttribute(MyContants.SMS_SESSION_KEY);
    }
}


四、编写过滤器,匹配/smslogin

package com.example.securityzimug.config.auth.smscode;
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;
/**
 * 模仿UsernamePasswordAuthenticationFilter编写手机号认证过滤器
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private boolean postOnly = true;
    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/smslogin", "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 moblie = obtainMobile(request);
        if (moblie == null) {
            moblie = "";
        }
        moblie = moblie.trim();
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(moblie);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    protected void setDetails(HttpServletRequest request,
                              SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
    public final String getMobileParameter() {
        return mobileParameter;
    }
}


然后编写token类

package com.example.securityzimug.config.auth.smscode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
 * 模仿UsernamePasswordAuthenticationToken编写SmsCodeAuthenticationToken
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    //存放认证信息,认证之前放的是手机号,认证之后UserDetails
    private final Object principal;
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }
    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }
    public Object getPrincipal() {
        return this.principal;
    }
    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();
    }
    @Override
    public Object getCredentials() {
        return null;
    }
}


然后编写provider:

package com.example.securityzimug.config.auth.smscode;
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;
/**
 * Spring Security默认使用DaoAuthenticationProvider进行认证,所以我们要自己定义provider
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    protected UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if(userDetails == null){
            throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
        }
        SmsCodeAuthenticationToken authenticationResult
                = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}


五、编写短信验证的配置类(也可以写在SecurityConfig那里面)

package com.example.securityzimug.config.auth.smscode;
import com.example.securityzimug.config.auth.MyAuthenticationFailureHandler;
import com.example.securityzimug.config.auth.MyAuthenticationSuccessHandler;
import com.example.securityzimug.config.auth.MyUserDetailsService;
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;
/**
 * 可以把该类放在SecurityConfig里,但是代码较多,抽出来好些
 */
@Component
public class SmsCodeSecurityConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Resource
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Resource
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Resource
    MyUserDetailsService myUserDetailsService;
    @Resource
    SmsCodeValidateFilter smsCodeValidateFilter;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(myUserDetailsService);
        //设置短信验证码过滤器在用户名密码鉴权过滤器之前
        http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
        //设置短信验证码鉴权过滤器在用户名密码鉴权过滤器之后,这样保证了先判断验证码,再查询数据库获取用户信息
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}


最后到SecurityConfig里面进行配置:

    @Resource
    SmsCodeSecurityConfig smsCodeSecurityConfig;


        //添加短信验证码过滤器
        http.apply(smsCodeSecurityConfig);


最后的最后,贴上前端登录页所有代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <script src="https://cdn.staticfile.org/jquery/1.12.3/jquery.min.js"></script>
</head>
<body>
<h1>字母哥业务系统登录</h1>
<form action="/login" method="post">
    <span>用户名称</span><input type="text" name="uname" id="username"/> <br>
    <span>用户密码</span><input type="password" name="pword" id="password"/> <br>
    <span>验证码</span><input type="text" name="captchaCode" id="captchaCode"/>
    <img src="/kaptcha" id="kaptcha" width="110px" height="40px"/> <br>
    <input type="button" onclick="login()" value="登陆">
<!--    <input type="submit" value="登陆">-->
    <label><input type="checkbox" name="remember-me" id="remember-me"/>记住密码</label>
</form>
<h1>短信登陆</h1>
<form action="/smslogin" method="post">
    <span>手机号码:</span><input type="text" name="mobile" id="mobile"> <br>
    <span>短信验证码:</span><input type="text" name="smsCode" id="smsCode" >
    <input type="button" onclick="getSmsCode()" value="获取"><br>
    <input type="button" onclick="smslogin()" value="登陆">
</form>
<script>
    window.onload = function () {
        var kaptchaImg = document.getElementById("kaptcha");
        kaptchaImg.onclick = function () {
            kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
        }
    };
    function login() {
        var username = $("#username").val();
        var password = $("#password").val();
        var captchaCode = $("#captchaCode").val();
        var rememberMe = $("#remember-me").is(":checked");
        if (username === "" || password === "") {
            alert('用户名或密码不能为空');
            return;
        }
        $.ajax({
            type: "POST",
            url: "/login",
            data: {
                "uname": username,
                "pword": password,
                "captchaCode": captchaCode,
                "remember-me-new": rememberMe
            },
            success: function (json) {
                if(json.isok){
                    location.href = json.data;
                }else{
                    alert(json.message)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }
    function getSmsCode() {
        $.ajax({
            type: "get",
            url: "/smscode",
            data: {
                "mobile": $("#mobile").val()
            },
            success: function (json) {
                if(json.isok){
                    alert(json.data)
                }else{
                    alert(json.message)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }
    function smslogin() {
        var mobile = $("#mobile").val();
        var smsCode = $("#smsCode").val();
        if (mobile === "" || smsCode === "") {
            alert('手机号和短信验证码均不能为空');
            return;
        }
        $.ajax({
            type: "POST",
            url: "/smslogin",
            data: {
                "mobile": mobile,
                "smsCode": smsCode
            },
            success: function (json) {
                if(json.isok){
                    location.href = json.data;
                }else{
                    alert(json.message)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }
</script>
</body>
</html>


目录
相关文章
|
22天前
|
Java 测试技术 开发者
springboot学习四:Spring Boot profile多环境配置、devtools热部署
这篇文章主要介绍了如何在Spring Boot中进行多环境配置以及如何整合DevTools实现热部署,以提高开发效率。
47 2
|
22天前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
45 1
|
22天前
|
Java API Spring
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中拦截器的入门教程和实战项目场景实现的详细指南。
17 0
springboot学习七:Spring Boot2.x 拦截器基础入门&实战项目场景实现
|
22天前
|
Java API Spring
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
20 0
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
|
23天前
|
Java 测试技术 Spring
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
这篇文章介绍了Spring Boot中配置文件的语法、如何读取配置文件以及如何通过静态工具类读取配置文件。
29 0
springboot学习三:Spring Boot 配置文件语法、静态工具类读取配置文件、静态工具类读取配置文件
|
24天前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
22天前
|
Java Spring
springboot 学习十一:Spring Boot 优雅的集成 Lombok
这篇文章是关于如何在Spring Boot项目中集成Lombok,以简化JavaBean的编写,避免冗余代码,并提供了相关的配置步骤和常用注解的介绍。
74 0
|
24天前
|
监控 数据可视化 Java
springBoot:actuator&admin 图形可视化&spring 打包 (七)
本文介绍了Spring Boot Actuator及其图形化管理界面Spring Boot Admin的使用方法,包括依赖导入、服务端与客户端配置、以及如何打包为JAR和WAR文件并部署。通过这些步骤,可以实现应用的监控和管理功能。
|
22天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
113 1
|
6天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
84 62