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>


目录
相关文章
|
6月前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
6月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
6月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
663 2
|
7月前
|
人工智能 Java 机器人
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
Spring AI Alibaba集成Ollama,基于Java构建本地大模型应用,支持流式对话、knife4j接口可视化,实现高隐私、免API密钥的离线AI服务。
5865 2
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
存储 JSON Java
788 0
|
7月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
2406 0
|
8月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1215 3
|
12月前
|
存储 Java 数据库
Spring Boot 注册登录系统:问题总结与优化实践
在Spring Boot开发中,注册登录模块常面临数据库设计、密码加密、权限配置及用户体验等问题。本文以便利店销售系统为例,详细解析四大类问题:数据库字段约束(如默认值缺失)、密码加密(明文存储风险)、Spring Security配置(路径权限不当)以及表单交互(数据丢失与提示不足)。通过优化数据库结构、引入BCrypt加密、完善安全配置和改进用户交互,提供了一套全面的解决方案,助力开发者构建更 robust 的系统。
392 0