Springboot基于SpringSecurity手机短信登录


1. 短信验证码的生成

首先自定义一个短信验证码类

package com.blog.security.smscode;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * blog
 *
 * @author : xgj
 * @description : 短信验证码类
 * @date : 2020-08-20 07:36
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
    private String phoneNumber;
    private String code;
    private LocalDateTime expireTime;

    public boolean isExpired() {
        return  LocalDateTime.now().isAfter(expireTime);
    }
}

接着在Controller中加入生成短信验证码相关请求对应的方法:

    @GetMapping("/smscode")
    @ResponseBody
    public Map<String,String> sms(@RequestParam String mobile, HttpSession session) {
        Map<String,String> map = new HashMap<>(16);
        if(userService.queryByPhoneNumber(mobile) == null){
            map.put("jsonmsg","手机号未注册");

        }else{
            //这里我偷懒了,具体实现可以调用短信验证提供商的api
            SmsCode smsCode = new SmsCode(mobile,RandomStringUtils.randomNumeric(6),LocalDateTime.now().plusSeconds(60));
            session.setAttribute("sms_key",smsCode);
            System.out.println(smsCode.getCode());
            map.put("jsonmsg","发送验证码成功");
        }
    return map;
    }

然后搞定登陆界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head th:replace="_fragments :: head(~{::title})">
    <title>博客管理登录</title>
</head>

<body class="login-bg">

<br>
<br>
<br>
<div class="m-container-small m-padded-tb-massive disabled" style="max-width: 30em !important;">
    <div class="ur container">
        <div class="ui middle aligned center aligned grid">
            <div class="column">
                <h2 class="ui teal image header">
                    <div class="content">
                        手机号码登录
                    </div>
                </h2>
                <form class="ui large form" method="post" action="#" th:action="@{/sms/login}">
                    <div class="ui  segment">
                        <div class="field">
                            <div class="ui left icon input">
                                <i class="user icon"></i>
                                <input type="text" id="mobile" name="mobile" placeholder="手机号码">
                            </div>
                        </div>
                        <div class="field">
                            <div class="ui left icon input">
                                <i class="lock icon"></i>
                                <input type="text" name="smsCode" placeholder="验证码">
                            </div>
                        </div>
                        <button class="ui fluid large teal submit button">登 录</button>
                    </div>
                    <div class="ui mini negative message" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></div>
                </form>
                <button class="ui pink header item" onclick="getSmsCode()">获取验证码</button>

            </div>
        </div>
    </div>
</div>

<!--/*/<th:block th:replace="_fragments :: script">/*/-->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<!--/*/</th:block>/*/-->

<script>

    function getSmsCode() {
        $.ajax({
            type: "get",
            url: "/smscode",
            data: {
                "mobile": $("#mobile").val()
            },
            success: function (json) {
                if (json.isok) {
                    alert(json.data)
                } else {
                    alert(json.jsonmsg)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }


</script>

</body>
</html>

2.实现短信验证流程

在这个流程中,我们自定义了一个名为SmsCodeAuthenticationFilter的过滤器来拦截短信验证码登录请求,并将手机号码封装到一个叫SmsCodeAuthenticationToken的对象中。在Spring Security中,认证处理都需要通过AuthenticationManager来代理,所以这里我们依旧将SmsCodeAuthenticationToken交由AuthenticationManager处理。接着我们需要定义一个支持处理SmsCodeAuthenticationToken对象的SmsCodeAuthenticationProvider,SmsCodeAuthenticationProvider调用UserDetailService的loadUserByUsername方法来处理认证。与用户名密码认证不一样的是,这里是通过SmsCodeAuthenticationToken中的手机号去数据库中查询是否有与之对应的用户,如果有,则将该用户信息封装到UserDetails对象中返回并将认证后的信息保存到Authentication对象中。

为了实现这个流程,我们需要定义SmsCodeAuthenticationFilter、SmsCodeAuthenticationToken、SmsCodeAuthenticationProvider和SmsCodeUserDetailsService,并将这些组建组合起来添加到Spring Security中。下面我们来逐步实现这个过程。
然后在整个过滤链前添加SmsCodeValidateFilter进行验证码验证。

  • SmsCodeValidateFilter
package com.blog.security.smscode;

import com.blog.pojo.User;
import com.blog.security.PublicAuthenticationFailureHandler;
import com.blog.service.UserService;
import com.blog.util.MyContants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

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;

/**
 * blog
 *
 * @author : xgj
 * @description : 前置手机验证登录
 * @date : 2020-08-20 12:29
 **/
@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {

    @Autowired
    private UserService userService;

    @Autowired
    private PublicAuthenticationFailureHandler publicAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws IOException, ServletException {
        if (StringUtils.equals(MyContants.SMS_FILTER_URL, request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(), MyContants.REQUEST_MAPPING_POST)) {

            try {
                //验证谜底与用户输入是否匹配
                validate(new ServletWebRequest(request));
            } catch (AuthenticationException e) {
                publicAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        filterChain.doFilter(request, response);

    }

    private void validate(ServletWebRequest request) throws SessionAuthenticationException {
        Logger logger = LoggerFactory.getLogger(getClass());

        HttpSession session = request.getRequest().getSession();
        SmsCode codeInSession = (SmsCode) session.getAttribute("sms_key");
        String mobileInRequest = request.getParameter("mobile");
        String codeInRequest = request.getParameter("smsCode");
        logger.info("SmsCodeValidateFilter--->"+codeInSession.toString()+mobileInRequest+codeInRequest);

        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("sms_key");
            throw new SessionAuthenticationException("短信验证码已经过期");
        }

        if (!codeInSession.getCode().equals(codeInRequest)) {
            throw new SessionAuthenticationException("短信验证码不正确");
        }

        if (!codeInSession.getPhoneNumber().equals(mobileInRequest)) {
            throw new SessionAuthenticationException("短信发送目标与您输入的手机号不一致");
        }

        User user = userService.queryByPhoneNumber(mobileInRequest);
        if (Objects.isNull(user)) {
            throw new SessionAuthenticationException("您输入的手机号不是系统的注册用户");
        }
        session.removeAttribute("sms_key");

    }
}
  • SmsCodeAuthenticationFilter
package com.blog.security.smscode;

import com.blog.util.MyContants;
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 实现
 *
 * @author xgj
 * @date : 2020-08-20 12:29
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    private String mobileParameter = MyContants.SMS_FORM_MOBILE_KEY;
    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        // 短信登录的请求 post 方式的 /sms/login
        super(new AntPathRequestMatcher(MyContants.SMS_FILTER_URL, MyContants.REQUEST_MAPPING_POST));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !MyContants.REQUEST_MAPPING_POST.equals(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        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 String getMobileParameter() {
        return mobileParameter;
    }

    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;
    }
}
  • SmsCodeAuthenticationProvider
package com.blog.security.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;



/**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 *
 * @author xgj
 * @date : 2020-08-20 12:29
 */

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal(); 
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);

        if(userDetails == null){
            throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
        }
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
    public UserDetailsService getUserDetailService() {
        return userDetailsService;
    }

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

}

  • SmsCodeAuthenticationToken
package com.blog.security.smscode;


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

import java.util.Collection;

/**
 * 短信登录 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 实现
 *
 * @author xgj
 * @since 2019/1/9 13:47
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(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();
    }
}
  • SmsCodeUserDetailsService
package com.blog.security.service;


import com.blog.pojo.Role;
import com.blog.pojo.User;
import com.blog.pojo.UserRole;
import com.blog.service.RoleService;
import com.blog.service.UserRoleService;
import com.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * blog
 *
 * @author : xgj
 * @description : 基于短信验证的userdetails
 * @date : 2020-08-17 09:24
 **/
@Service("smsCodeUserDetailsService")
public class SmsCodeUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private UserRoleService userRoleService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息
        User user = userService.queryByPhoneNumber(s);
        // 判断用户是否存在
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 添加权限
        List<UserRole> userRoles = userRoleService.listByUserId(user.getId());
        for (UserRole userRole : userRoles) {
            Role role = roleService.selectById(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 返回UserDetails实现类
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
    }
}

然后编写配置类

package com.blog.config;

import com.blog.security.PublicAuthenticationFailureHandler;
import com.blog.security.PublicAuthenticationSuccessHandler;
import com.blog.security.service.CustomUserDetailsService;
import com.blog.security.service.SmsCodeUserDetailsService;
import com.blog.security.smscode.SmsCodeAuthenticationFilter;
import com.blog.security.smscode.SmsCodeAuthenticationProvider;
import com.blog.security.smscode.SmsCodeValidateFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * @author xgj
 * 手机短信验证
 */
@Component("smsCodeAuthenticationSecurityConfig")
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    @Qualifier("smsCodeUserDetailsService")
    private SmsCodeUserDetailsService smsCodeUserDetailsService;
    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailService(smsCodeUserDetailsService);
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);


    }
}

最后配置生效

    @Override
    protected void configure(HttpSecurity http) throws Exception {
                // 添加短信验证码校验过滤器
        http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
                ...
                .apply(smsCodeAuthenticationSecurityConfig);
    }

其实类似的邮箱验证登录也大概可以使用这个流程。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM