Spring Security 实现手机验证码登录

 

思路:参考用户名密码登录过滤器链,重写认证和授权

 

示例如下(该篇示例以精简为主,演示主要实现功能,全面完整版会在以后的博文中发出):

 

由于涉及内容较多,建议先复制到本地工程中,然后在细细研究。

 

1.   新建Maven项目  sms-code-validate

 

2.   pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
        http://maven.apache.org/xsd/maven-4.0.0.xsd">


    <modelVersion>4.0.0</modelVersion>
    <groupId>com.java</groupId>
    <artifactId>sms-code-validate</artifactId>
    <version>1.0.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>


    <dependencies>

        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.11</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>


        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
            <version>1.2.8.RELEASE</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

 

3.   启动类  SmsCodeStarter.java

package com.java;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * <blockquote><pre>
 * 
 * 主启动类
 * 
 * </pre></blockquote>
 * 
 * @author Logan
 *
 */
@SpringBootApplication
public class SmsCodeStarter {

    public static void main(String[] args) {
        SpringApplication.run(SmsCodeStarter.class, args);
    }

}

 

4.   ValidateCode.java

package com.java.validate.code;

import java.time.LocalDateTime;

/**
 * 验证码封装类
 * 
 * @author Logan
 *
 */
public class ValidateCode {

    /**
     * 验证码
     */
    private String code;

    /**
     * 过期时间
     */
    private LocalDateTime expireTime;

    /**
     * 指定验证码和有效分钟数的构造方法
     * 
     * @param code 验证码
     * @param validityMinutes 有效分钟数
     */
    public ValidateCode(String code, int validityMinutes) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusMinutes(validityMinutes);
    }

    /**
     * 指定验证码和过期时间的构造方法
     * 
     * @param code 验证码
     * @param expireTime 过期时间
     */
    public ValidateCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    public String getCode() {
        return code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

}

 

5.   CodeGenerator.java

package com.java.validate.generator;

import org.apache.commons.lang3.RandomStringUtils;

import com.java.validate.code.ValidateCode;

/**
 * 验证码生成器
 * 
 * @author Logan
 *
 */
public class CodeGenerator {

    /**
     * 验证码生成方法
     * 
     * @param length 验证码长度
     * @param validityMinutes 过期分钟数
     * @return
     */
    public static ValidateCode generate(int length, int validityMinutes) {
        String code = RandomStringUtils.randomNumeric(length);
        return new ValidateCode(code, validityMinutes);
    }

}

 

6.   ValidateCodeSender.java

package com.java.validate.sender;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;

/**
 * 验证码发送器
 * 
 * @author Logan
 *
 */
@Component
public class ValidateCodeSender {

    /**
     * 模拟发送手机验证码,此处发回浏览器,实际情况根据短信服务商做调整
     * 
     * @param response HTTP响应对象
     * @param mobile 手机号
     * @param code 验证码
     */
    public void sendSmsCode(HttpServletResponse response, String mobile, String code) {
        System.out.println(String.format("模拟向手机号【%s】发送验证码【%s】", mobile, code));
        write(response, "验证码为:" + code);
    }

    /**
     * 发送HTTP响应信息
     * 
     * @param response HTTP响应对象
     * @param message 信息内容
     */
    private void write(HttpServletResponse response, String message) {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.write(message);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }

}

 

7.   ValidateCodeFilter.java

package com.java.validate.filter;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

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

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.java.controller.ValidateCodeController;
import com.java.validate.code.ValidateCode;

/**
 * 校验验证码过滤器
 * 
 * @author Logan
 * @createDate 2019-02-07
 *
 */
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {

    /**
     * 需要校验短信验证码的请求
     */
    private List<String> smsCodeUrls = new ArrayList<>();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        /**
         * 如果需要校验短信验证码的请求集合中,包含当前请求,则进行短信验证码校验
         */
        if (smsCodeUrls.contains(request.getRequestURI())) {
            if (smsCodeValid(request, response)) {

                // 校验通过,继续向后执行
                filterChain.doFilter(request, response);
            }

        }

        // 其它请求,直接放过
        else {
            filterChain.doFilter(request, response);
        }

    }

    @Override
    protected void initFilterBean() throws ServletException {

        // 初始化添加需要校验的请求到集合中,可由配置文件中配置,此处为了简洁,直接添加
        smsCodeUrls.add("/login/mobile");
    }

    /**
     * 短信验证码是否有效
     * 
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @return 有效,返回true;无效,返回false
     * @throws ServletRequestBindingException
     */
    private boolean smsCodeValid(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        String smsCode = ServletRequestUtils.getStringParameter(request, "smsCode");
        ValidateCode validateCode = (ValidateCode) request.getSession().getAttribute(ValidateCodeController.SESSION_CODE_KEY);
        if (StringUtils.isBlank(smsCode)) {
            write(response, "验证码不能为空!");
            return false;
        } else if (null == validateCode) {
            write(response, "验证码不存在!");
            return false;
        } else if (LocalDateTime.now().isAfter(validateCode.getExpireTime())) {
            write(response, "验证码已过期!");
            return false;
        } else if (!StringUtils.equals(smsCode, validateCode.getCode())) {
            write(response, "验证码不正确!");
            return false;
        }

        // 验证成功,移除Session中验证码
        request.getSession().removeAttribute(ValidateCodeController.SESSION_CODE_KEY);
        return true;
    }

    /**
     * 发送HTTP响应信息
     * 
     * @param response HTTP响应对象
     * @param message 信息内容
     */
    private void write(HttpServletResponse response, String message) {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.write(message);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            writer.close();
        }
    }

}

 

8.   自定义UserDetailsService实现类,具体逻辑根据实际情况调整。  

SecurityUserDetailsService.java

package com.java.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * UserDetailsService实现类
 * 
 * @author Logan
 *
 */
@Component
public class SecurityUserDetailsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 数据库存储密码为加密后的密文(明文为123456)
        String password = passwordEncoder.encode("123456");

        System.out.println("username: " + username);
        System.out.println("password: " + password);

        // 模拟查询数据库,获取属于Admin和Normal角色的用户
        User user = new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("Admin,Normal"));

        return user;
    }

}

 

9.   获取主机信息接口,模拟演示功能需要

HostController.java

package com.java.controller;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HostController {

    @GetMapping("/getHostMessage")
    public Map<String, Object> getHostMessage() {
        Map<String, Object> map = new HashMap<>();
        try {
            InetAddress serverHost = InetAddress.getLocalHost();
            map.put("hostname", serverHost.getHostName());
            map.put("hostAddress", serverHost.getHostAddress());
        } catch (UnknownHostException e) {
            e.printStackTrace();
            map.put("msg", e.getMessage());
        }

        return map;

    }

}

 

10.   验证码生成接口,可扩展集成图片验证码,后续博文中会发出。

ValidateCodeController.java

package com.java.controller;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.java.validate.code.ValidateCode;
import com.java.validate.generator.CodeGenerator;
import com.java.validate.sender.ValidateCodeSender;

/**
 * 创建验证码接口
 * 
 * @author Logan
 *
 */
@RestController
@RequestMapping("/code")
public class ValidateCodeController {

    /**
     * 验证码存放Session中的key
     */
    public static final String SESSION_CODE_KEY = "code";

    /**
     * 验证码长度,可以提取到配置中,此处只做演示,简单处理
     */
    private int length = 6;

    /**
     * 过期分钟数,可以提取到配置中,此处只做演示,简单处理
     */
    private int validityMinutes = 30;

    /**
     * 验证码发送器
     */
    @Autowired
    private ValidateCodeSender validateCodeSender;

    /**
     * 创建短信验证码接口
     * 
     * @param request 请求对象
     * @param response 响应对象
     * @param mobile 手机号
     */
    @GetMapping("/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) {
        ValidateCode validateCode = CodeGenerator.generate(length, validityMinutes);

        // 存储验证码到Session中,登录时验证
        request.getSession().setAttribute(SESSION_CODE_KEY, validateCode);

        // 调用验证码发送器发送短信验证码
        validateCodeSender.sendSmsCode(response, mobile, validateCode.getCode());
    }

}

 

11.   AuthenticationSuccessHandler.java

package com.java.authentication.handler;

import java.io.IOException;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;

/**
 * 授权成功处理器
 * 
 * @author Logan
 * @createDate 2019-02-07
 *
 */
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private static final Log logger = LogFactory.getLog(AuthenticationSuccessHandler.class);

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        // 登录成功,如果未设置跳转页面,自动跳转到首页
        if (null == savedRequest) {
            logger.info("登录成功,自动跳转到登录页面");
            getRedirectStrategy().sendRedirect(request, response, "/main.html");
        }

        // 设置重定向页面,执行重定向
        else {

            String targetUrl = savedRequest.getRedirectUrl();
            logger.info("targetUrl: " + targetUrl);
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }

}

 

12.   SmsCodeAuthenticationToken.java

package com.java.authentication.mobile;

import java.util.Collection;

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

/**
 * <pre>
 * 
 * 短信验证码Token,封装短信验证码登录信息。
 * 
 * 参照{@link org.springframework.security.authentication.UsernamePasswordAuthenticationToken}
 * 
 * </pre>
 * 
 * @author Logan
 *
 */
public class SmsCodeAuthenticationToken  extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final Object principal;

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

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        super.setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param mobile
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================

    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }

}

 

13.   SmsCodeAuthenticationFilter.java

package com.java.authentication.mobile;

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

import org.apache.commons.lang3.StringUtils;
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;

/**
 * <pre>
 * 
 * 短信验证码过滤器,
 * 
 * 参照{@link org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter}
 * 
 * </pre>
 * 
 * @author Logan
 *
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // ~ Static fields/initializers
    // =====================================================================================

    public static final String MOBILE_KEY = "mobile";

    private String mobileParameter = MOBILE_KEY;
    private boolean postOnly = true;

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

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

    // ~ Methods
    // ========================================================================================================

    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 = StringUtils.trimToEmpty(request.getParameter(mobileParameter));
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

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

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

    /**
     * Provided so that subclasses may configure what is put into the
     * authentication request‘s details property.
     *
     * @param request that an authentication request is being created for
     * @param authRequest the authentication request object that should have its
     *            details set
     */
    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * Sets the parameter name which will be used to obtain the mobile from the
     * login request.
     * 
     * @param mobileParameter the parameter name. Defaults to "mobile".
     */
    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    /**
     * Defines whether only HTTP POST requests will be allowed by this filter.
     * If set to true, and an authentication request is received which is not a
     * POST request, an exception will be raised immediately and authentication
     * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
     * will be called as if handling a failed authentication.
     * <p>
     * Defaults to <tt>true</tt> but may be overridden by subclasses.
     */
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getMobileParameter() {
        return mobileParameter;
    }
}

 

14.   SmsCodeAuthenticationProvider.java

package com.java.authentication.mobile;

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;

/**
 * 短信验证码授权认证类
 * 
 * @author Logan
 * @createDate 2019-02-07
 *
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 未认证Token
        SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
        UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());

        if (null == user) {
            throw new InternalAuthenticationServiceException("未绑定用户!");
        }

        // 已认证的Token
        SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(user, user.getAuthorities());

        // 复制之前的请求信息到认证后的Token中
        authenticationToken.setDetails(token.getDetails());

        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

}

 

15.   ApplicationContextConfig.java

package com.java.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 配置文件类
 * 
 * @author Logan
 *
 */
@Configuration
public class ApplicationContextConfig {

    /**
     * <blockquote><pre>
     * 
     * 配置密码编码器,Spring Security 5.X必须配置,否则登录时报空指针异常
     * 
     * </pre></blockquote>
     * 
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

16.   RepositoryConfig.java

package com.java.config;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

/**
 * 数据库相关配置
 * 
 * @author Logan
 *
 */
@Configuration
public class RepositoryConfig {

    @Bean
    public PersistentTokenRepository tokenRepository(DataSource dataSource) {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // tokenRepository.setCreateTableOnStartup(true); // 第一次启动时可使用此功能自动创建表,第二次要关闭,否则表已存在会启动报错
        return tokenRepository;
    }

}

 

17.   SmsCodeSecurityConfig.java

package com.java.config;

import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import com.java.authentication.handler.AuthenticationSuccessHandler;
import com.java.authentication.mobile.SmsCodeAuthenticationFilter;
import com.java.authentication.mobile.SmsCodeAuthenticationProvider;

/**
 * 短信验证码安全配置,串联自定义短信验证码验证流程
 * 
 * @author Logan
 * @createDate 2019-02-07
 *
 */
@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationSuccessHandler successHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        // 此处采用new的方式,而不是@Component和@Autowired结合,目的为了方便安装和卸载,可重用可移植性强
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();

        // 设置AuthenticationManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);

        // 短信验证码认证Provider类
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        // 设置短信验证码认证Provider类到AuthenticationManager管理集合中
        http.authenticationProvider(smsCodeAuthenticationProvider)

                // 设置短信验证码在用户名密码验证过滤器之后验证
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }

}

 

18.   LoginConfig.java

package com.java.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import com.java.validate.filter.ValidateCodeFilter;

/**
 * 登录相关配置
 * 
 * @author Logan
 *
 */
@Configuration
public class LoginConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PersistentTokenRepository tokenRepository;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private ValidateCodeFilter validateCodeFilter;

    @Autowired
    private SmsCodeSecurityConfig smsCodeSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.apply(smsCodeSecurityConfig)

                // 设置验证码过滤器到过滤器链中,在UsernamePasswordAuthenticationFilter之前执行
                .and().addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)

                // 设置自定义表单登录页面
                .formLogin().loginPage("/login.html")

                // 设置登录验证请求地址为自定义登录页配置action ("/authentication/form")
                .loginProcessingUrl("/login/form")
                
                // 设置默认登录成功跳转页面
                .defaultSuccessUrl("/main.html")

                /* 授权请求设置 */
                .and().authorizeRequests()

                // 设置不需要授权的请求
                .antMatchers("/js/*", "/code/*", "/login.html").permitAll()

                // 其它任何请求都需要验证权限
                .anyRequest().authenticated()

                /* 记住我功能设置 */
                .and().rememberMe().tokenRepository(tokenRepository)

                // 【记住我功能】有效期为两周
                .tokenValiditySeconds(3600 * 24 * 14)

                // 设置UserDetailsService
                .userDetailsService(userDetailsService)

                // 暂时停用csrf,否则会影响验证
                .and().csrf().disable();
    }

}

 

19.   src/main/resources  下配置文件如下

Spring Security 实现手机验证码登录

 

20.   application.properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.32.10:3306/security?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.dbcp2.initial-size=5
spring.datasource.dbcp2.max-total=20

 

21.   login.html

<!DOCTYPE html>
<html>

    <head>
        <title>登录</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
        <script>
            function sendSmsCode() {
                var mobile = $("#mobile").val().trim();

                // 简单校验由11位数字组成
                var reg = /^\d{11}$/;
                if(reg.test(mobile)) {
                    $.ajax({
                        type: "get",
                        url: "/code/sms?mobile=" + mobile,
                        async: true,
                        success: function(data) {
                            alert(data);
                        }
                    });
                } else {
                    alert("手机号输入格式错误");
                }
            }
        </script>
    </head>

    <body>

        <!--登录框-->
        <div align="center">
            <h2>用户自定义登录页面</h2>
            <fieldset style="width: 360px;">
                <legend>表单登录框</legend>
                <form action="/login/form" method="post">
                    <table>
                        <tr>
                            <th>用户名:</th>
                            <td><input name="username" /> </td>
                        </tr>
                        <tr>
                            <th>密码:</th>
                            <td><input type="password" name="password" /> </td>
                        </tr>
                        <tr>
                            <th>记住我:</th>
                            <td><input type="checkbox" name="remember-me" value="true" checked="checked" /></td>
                        </tr>
                        <tr>
                            <th></th>
                            <td></td>
                        </tr>
                        <tr>
                            <td colspan="2" align="center"><button type="submit">登录</button></td>
                        </tr>
                    </table>
                </form>
            </fieldset>
            <fieldset style="width: 360px;margin-top: 30px;">
                <legend>手机验证码登录框</legend>
                <form action="/login/mobile" method="post">
                    <table>
                        <tr>
                            <th>手机号:</th>
                            <td><input id="mobile" name="mobile" value="13166668888" /></td>
                        </tr>
                        <tr>
                            <th>验证码:</th>
                            <td>
                                <input id="smsCode" name="smsCode" />
                                <button type="button" onclick="sendSmsCode()">发送手机验证码</button>
                            </td>
                        </tr>
                        <tr>
                            <td colspan="2" align="center"><button type="submit">登录</button></td>
                        </tr>
                    </table>
                </form>
            </fieldset>

        </div>

    </body>

</html>

 

22.   main.html

<!DOCTYPE html>
<html>

    <head>
        <title>首页</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
        <script>
            function getHostMessage() {
                $.ajax({
                    type: "get",
                    url: "/getHostMessage",
                    async: true,
                    success: function(data) {
                        $("#msg").val(JSON.stringify(data));
                    }
                });
            }
        </script>
    </head>

    <body>

        <div>
            <h2>首页</h2>
            <table>
                <tr>
                    <td><button onclick="getHostMessage()">获取主机信息</button></td>
                </tr>
            </table>

        </div>

        <!--响应内容-->
        <div>
            <textarea id="msg" style="width: 800px;height: 800px;"></textarea>
        </div>

    </body>

</html>

 

23.   js/jquery-3.3.1.min.js  可在官网下载

https://code.jquery.com/jquery-3.3.1.min.js

 

 24.   创建数据库

DROP DATABASE IF EXISTS security;
CREATE DATABASE security;
USE security;
create table persistent_logins (
    username varchar(64) not null, 
    series varchar(64) primary key, 
    token varchar(64) not null, 
    last_used timestamp not null
);

 

25.   运行 SmsCodeStarter.java , 启动测试

浏览器输入首页  http://localhost:8080/main.html

地址栏自动跳转到登录页面,如下:

 Spring Security 实现手机验证码登录

 

 表单登录可自行研究,只讲解手机验证码登录过程

单击【发送手机验证码】按钮,控制台和浏览器都会显示生成验证码。

输入正确的手机验证码,单击【登录】按钮。跳转到首页,如下所示:

Spring Security 实现手机验证码登录

 

获取主机信息接口功能调用正常。

 

验证码输入错误情况可自习研究。

 

 

 

搭建完成!

 

 

 .

Spring Security 实现手机验证码登录

上一篇:haproxy2.0 dataplaneapi 简单说明


下一篇:axios请求和vue-resource