zongmin

本文在前文 Spring Security 入门(一):认证和原理分析 的基础上介绍图形验证码和手机短信验证码登录的实现。

图形验证码

在用户登录时,一般通过表单的方式进行登录都会要求用户输入验证码,Spring Security默认没有实现图形验证码的功能,所以需要我们自己实现。

实现流程分析

前文中实现的用户名、密码登录是在UsernamePasswordAuthenticationFilter过滤器进行认证的,而图形验证码一般是在用户名、密码认证之前进行验证的,所以需要在UsernamePasswordAuthenticationFilter过滤器之前添加一个自定义过滤器 ImageCodeValidateFilter,用来校验用户输入的图形验证码是否正确。自定义过滤器继承 OncePerRequestFilter 类,该类是 Spring 提供的在一次请求中只会调用一次的 filter。

自定义的过滤器 ImageCodeValidateFilter 首先会判断请求是否为 POST 方式的登录表单提交请求,如果是就将其拦截进行图形验证码校验。如果验证错误,会抛出自定义异常类对象 ValidateCodeException,该异常类需要继承 AuthenticationException 类。在自定义过滤器中,我们需要手动捕获自定义异常类对象,并将捕获到自定义异常类对象交给自定义失败处理器进行处理。


kpatcha 使用

Kaptcha 是谷歌提供的生成图形验证码的工具,参考地址为:https://github.com/penggle/kaptcha,依赖如下:

<!-- 图形验证码工具 kaptcha -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

☕ 创建 KaptchaConfig 配置类

package com.example.config;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;

/**
 * 图形验证码的配置类
 */
@Configuration
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha captchaProducer() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        // 是否有边框
        properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
        // 边框颜色
        properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");
        // 验证码图片的宽和高
        properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");
        properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "40");
        // 验证码颜色
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");
        // 验证码字体大小
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");
        // 验证码生成几个字符
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
        // 验证码随机字符库
        properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
        // 验证码图片默认是有线条干扰的,我们设置成没有干扰
        properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

kaptcha 配置的参数说明(定义在 Constants 常量类中):

  • kaptcha.border:是否有图片边框,合法值:yes,no;默认值为 yes。
  • kaptcha.border.color:边框颜色,合法值:rgb 或者 white,black,blue;默认值为 black。
  • kaptcha.border.thickness 边框厚度,合法值:>0;默认为 1。
  • kaptcha.image.width:图片宽,默认值为 200px。
  • kaptcha.image.height:图片高,默认值为 50px。
  • kaptcha.producer.impl:图片实现类,默认值为com.google.code.kaptcha.impl.DefaultKaptcha
  • kaptcha.textproducer.impl:文本实现类,默认值为com.google.code.kaptcha.text.impl.DefaultTextCreator
  • kaptcha.textproducer.char.string:文本集合,验证码值从此集合中获取,默认值为 abcde2345678gfynmnpwx
  • kaptcha.textproducer.char.length:验证码长度,默认值为 5。
  • kaptcha.textproducer.font.names:字体,默认值为 Arial, Courier。
  • kaptcha.textproducer.font.size:字体大小,默认值为 40px。
  • kaptcha.textproducer.font.color:字体颜色,合法值:rgb 或者 white,black,blue;默认值为black。
  • kaptcha.textproducer.char.space:文字间隔,默认值为 2px。
  • kaptcha.noise.impl:干扰实现类,com.google.code.kaptcha.impl.NoNoise为没有干扰。默认值为 com.google.code.kaptcha.impl.DefaultNoise
  • kaptcha.noise.color:干扰线颜色,合法值:rgb 或者 white,black,blue;默认值为 black。
  • kaptcha.obscurificator.impl:图片样式,合法值:水纹 com.google.code.kaptcha.impl.WaterRipple,鱼眼 com.google.code.kaptcha.impl.FishEyeGimpy, 阴影 com.google.code.kaptcha.impl.ShadowGimpy;默认值为 com.google.code.kaptcha.impl.WaterRipple
  • kaptcha.background.impl:背景实现类,默认值为com.google.code.kaptcha.impl.DefaultBackground
  • kaptcha.background.clear.from:背景颜色渐变,开始颜色,默认值为light grey
  • kaptcha.background.clear.to:背景颜色渐变, 结束颜色,默认值为 white。
  • kaptcha.word.impl:文字渲染器 实现类,默认值为 com.google.code.kaptcha.text.impl.DefaultWordRenderer
  • kaptcha.session.key:session key,默认值为KAPTCHA_SESSION_KEY
  • kaptcha.session.date:session date,默认值为KAPTCHA_SESSION_DATE

☕ 创建验证码的实体类 CheckCode

package com.example.entity;

import java.io.Serializable;
import java.time.LocalDateTime;

public class CheckCode implements Serializable {
    private String code;           // 验证码字符
    private LocalDateTime expireTime;  // 过期时间

    /**
     * @param code 验证码字符
     * @param expireTime 过期时间,单位秒
     */
    public CheckCode(String code, int expireTime) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
    }

    public CheckCode(String code) {
        // 默认验证码 60 秒后过期
        this(code, 60);
    }

    // 是否过期
    public boolean isExpried() {
        return this.expireTime.isBefore(LocalDateTime.now());
    }

    public String getCode() {
        return this.code;
    }
}

☕ 在 LoginController 中添加获取图形验证码的 Controller 方法

package com.example.constans;

public class Constants {
    // Session 中存储图形验证码的属性名
    public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
}

@Controller
public class LoginController {

    @Autowired
    private DefaultKaptcha defaultKaptcha;
    //...
    
    @GetMapping("/code/image")
    public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 创建验证码文本
        String capText = defaultKaptcha.createText();
        // 创建验证码图片
        BufferedImage image = defaultKaptcha.createImage(capText);

        // 将验证码文本放进 Session 中
        CheckCode code = new CheckCode(capText);
        request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, code);

        // 将验证码图片返回,禁止验证码图片缓存
        response.setHeader("Cache-Control", "no-store");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setContentType("image/jpeg");
        ImageIO.write(image, "jpg", response.getOutputStream());
    }    
}

☕ 在 login.html 中添加验证码功能

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <h3>表单登录</h3>
    <form method="post" th:action="@{/login/form}">
        <input type="text" name="name" placeholder="用户名"><br>
        <input type="password" name="pwd" placeholder="密码"><br>

        <input name="imageCode" type="text" placeholder="验证码"><br>
        <img th:onclick="this.src=\'/code/image?\'+Math.random()" th:src="@{/code/image}" alt="验证码"/><br>
        <div th:if="${param.error}">
            <span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span>
        </div>
        <button type="submit">登录</button>
    </form>
</body>
</html>

☕ 更改安全配置类 SpringSecurityConfig,设置访问/code/image不需要任何权限

@EnableWebSecurity       // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    /**
     * 定制基于 HTTP 请求的用户访问控制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //...
        // 开启基于 HTTP 请求访问控制
        http.authorizeRequests()
                // 以下访问不需要任何权限,任何人都可以访问
                .antMatchers("/login/page", "/code/image").permitAll()
                // 以下访问需要 ROLE_ADMIN 权限
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 以下访问需要 ROLE_USER 权限
                .antMatchers("/user/**").hasAuthority("ROLE_USER")
                // 其它任何请求访问都需要先通过认证
                .anyRequest().authenticated();
		//...            
    }
    //...
}

☕ 测试

访问localhost:8080/login/page,出现图形验证的信息


自定义验证码过滤器

⭐️ 创建自定义异常类 ValidateCodeException

package com.example.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * 自定义验证码校验错误的异常类,继承 AuthenticationException
 */
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String msg, Throwable t) {
        super(msg, t);
    }

    public ValidateCodeException(String msg) {
        super(msg);
    }
}

⭐ 自定义图形验证码校验过滤器 ImageCodeValidateFilter

package com.example.config.security;

import com.example.constans.Constants;
import com.example.entity.CheckCode;
import com.example.exception.ValidateCodeException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;

@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {

    private String codeParamter = "imageCode";  // 前端输入的图形验证码参数名

    @Autowired
    private CustomAuthenticationFailureHandler authenticationFailureHandler;  // 自定义认证失败处理器

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 非 POST 方式的表单提交请求不校验图形验证码
        if ("/login/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
            try {
                // 校验图形验证码合法性
                validate(request);
            } catch (ValidateCodeException e) {
                // 手动捕获图形验证码校验过程抛出的异常,将其传给失败处理器进行处理
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }

        // 放行请求,进入下一个过滤器
        filterChain.doFilter(request, response);
    }

    // 判断验证码的合法性
    private void validate(HttpServletRequest request) {
        // 获取用户传入的图形验证码值
        String requestCode = request.getParameter(this.codeParamter);
        if(requestCode == null) {
            requestCode = "";
        }
        requestCode = requestCode.trim();

        // 获取 Session
        HttpSession session = request.getSession();
        // 获取存储在 Session 里的验证码值
        CheckCode savedCode = (CheckCode) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        if (savedCode != null) {
            // 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
            session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
        }

        // 校验出错,抛出异常
        if (StringUtils.isBlank(requestCode)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }

        if (savedCode == null) {
            throw new ValidateCodeException("验证码不存在");
        }

        if (savedCode.isExpried()) {
            throw new ValidateCodeException("验证码过期");
        }

        if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
            throw new ValidateCodeException("验证码输入错误");
        }
    }
}

⭐️ 更改安全配置类 SpringSecurityConfig,将自定义过滤器添加过滤器链中

@EnableWebSecurity       // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    @Autowired
    private ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)
    //...
    
    /**
     * 定制基于 HTTP 请求的用户访问控制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //...        
        // 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
    }
    //...
}

完整的安全配置类 SpringSecurityConfig 如下:

package com.example.config;

import com.example.config.security.CustomAuthenticationFailureHandler;
import com.example.config.security.CustomAuthenticationSuccessHandler;
import com.example.config.security.ImageCodeValidateFilter;
import com.example.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity       // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Autowired
    private CustomAuthenticationSuccessHandler authenticationSuccessHandler; // 自定义认证成功处理器

    @Autowired
    private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器

    @Autowired
    private ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)

    /**
     * 密码编码器,密码不能明文存储
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        // 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中
        return new BCryptPasswordEncoder();
    }

    /**
     * 定制用户认证管理器来实现用户认证
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 采用内存存储方式,用户认证信息存储在内存中
        // auth.inMemoryAuthentication()
        //        .withUser("admin").password(passwordEncoder()
        //        .encode("123456")).roles("ROLE_ADMIN");
        
        // 不再使用内存方式存储用户认证信息,而是动态从数据库中获取
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * 定制基于 HTTP 请求的用户访问控制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 启动 form 表单登录
        http.formLogin()
                // 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问
                .loginPage("/login/page")
                // 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求
                .loginProcessingUrl("/login/form")
                // 设置登录表单中的用户名参数,默认为 username
                .usernameParameter("name")
                // 设置登录表单中的密码参数,默认为 password
                .passwordParameter("pwd")
                // 认证成功处理,如果存在原始访问路径,则重定向到该路径;如果没有,则重定向 /index
                //.defaultSuccessUrl("/index")
                // 认证失败处理,重定向到指定地址,默认为 loginPage() + ?error;该路径不设限访问
                //.failureUrl("/login/page?error");
                // 不再使用 defaultSuccessUrl() 和 failureUrl() 方法进行认证成功和失败处理,
                // 使用自定义的认证成功和失败处理器
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler);

        // 开启基于 HTTP 请求访问控制
        http.authorizeRequests()
                // 以下访问不需要任何权限,任何人都可以访问
                .antMatchers("/login/page", "/code/image").permitAll()
                // 以下访问需要 ROLE_ADMIN 权限
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 以下访问需要 ROLE_USER 权限
                .antMatchers("/user/**").hasAuthority("ROLE_USER")
                // 其它任何请求访问都需要先通过认证
                .anyRequest().authenticated();

        // 关闭 csrf 防护
        http.csrf().disable();
        
        // 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);        
    }

    /**
     * 定制一些全局性的安全配置,例如:不拦截静态资源的访问
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 静态资源的访问不需要拦截,直接放行
        web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
    }
}

⭐ 测试

访问localhost:8080/login/page,等待 60 秒后,输入正确的用户名、密码和验证码:

验证码过期,重定向到localhost:8080/login/page?error,显示错误信息:


手机短信验证码

一般登录除了用户名、密码登录,还可以使用手机短信验证码登录,Spring Security默认没有实现手机短信验证码的功能,所以需要我们自己实现。

实现流程分析

手机短信验证码登录和前面的带有图形验证码的用户名、密码登录流程类似,红色标记的部分是需要我们自定义实现的类。

我们首先分析下带有图形验证码的用户名、密码登录流程:

  1. 在 ImageCodeValidateFilter 过滤器中校验用户输入的图形验证码是否正确。

  2. 在 UsernamePasswordAuthenticationFilter 过滤器中将 username 和 password 生成一个用于认证的 Token(UsernamePasswordAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。

  3. AuthenticationManager 管理器寻找到一个合适的处理器 DaoAuthenticationProvider 来处理 UsernamePasswordAuthenticationToken。

  4. DaoAuthenticationProvider 通过 UserDetailsService 接口的实现类 CustomUserDetailsService 从数据库中获取指定 username 的相关信息,并校验用户输入的 password。如果校验成功,那就认证通过,用户信息类对象 Authentication 标记为已认证。

  5. 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中。

仿照上述流程,我们分析手机短信验证码登录流程:

  1. 仿照 ImageCodeValidateFilter 过滤器设计 MobileVablidateFilter 过滤器,该过滤器用来校验用户输入手机短信验证码。
  2. 仿照 UsernamePasswordAuthenticationFilter 过滤器设计 MobileAuthenticationFilter 过滤器,该过滤器将用户输入的手机号生成一个 Token(MobileAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。
  3. AuthenticationManager 管理器寻找到一个合适的处理器 MobileAuthenticationProvider 来处理 MobileAuthenticationToken,该处理器是仿照 DaoAuthenticationProvider 进行设计的。
  4. MobileAuthenticationProvider 通过 UserDetailsService 接口的实现类 MobileUserDetailsService 从数据库中获取指定手机号对应的用户信息,此处不需要进行任何校验,直接将用户信息类对象 Authentication 标记为已认证。
  5. 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中,此处的操作不需要我们编写。

最后通过自定义配置类 MobileAuthenticationConfig 组合上述组件,并添加到安全配置类 SpringSecurityConfig 中。


模拟发送短信验证码

✏️ 在 UserMapper 接口中添加根据 mobile 查询用户的方法

public interface UserMapper {
    //...
    @Select("select * from user where mobile = #{mobile}")
    User selectByMobile(String mobile);    
}

✏️ 创建 UserService 类,编写判断指定 mobile 是否存在的方法

package com.example.service;

import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 判断指定 mobile 是否存在
     */
    public boolean isExistByMobile(String mobile) {
        return userMapper.selectByMobile(mobile) != null;
    }
}

✏️ 创建 MobileCodeSendService 类,模拟手机短信验证码发送服务

package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class MobileCodeSendService {

    /**
     * 模拟发送手机短信验证码
     */
    public void send(String mobile, String code) {
        String sendContent = String.format("验证码为 %s,请勿泄露!", code);
        log.info("向手机号 " + mobile + " 发送短信:" + sendContent);
    }
}

✏️ 在 LoginController 中添加手机短信验证码相关的 Controller 方法

public class Constants {
    //...
    // Session 中存储手机短信验证码的属性名
    public static final String MOBILE_SESSION_KEY = "MOBILE_SESSION_KEY";
}
@Controller
public class LoginController {
    //...
    @Autowired 
    private MobileCodeSendService mobileCodeSendService;  // 模拟手机短信验证码发送服务

    @Autowired
    private UserService userService;   
    //...
    
    @GetMapping("/mobile/page")
    public String mobileLoginPage() {  // 跳转到手机短信验证码登录页面
        return "login-mobile";
    }

    @GetMapping("/code/mobile")
    @ResponseBody
    public Object sendMoblieCode(String mobile, HttpServletRequest request) { 
        // 随机生成一个 4 位的验证码
        String code = RandomStringUtils.randomNumeric(4);

        // 将手机验证码文本存储在 Session 中,设置过期时间为 10 * 60s
        CheckCode mobileCode = new CheckCode(code, 10 * 60);
        request.getSession().setAttribute(Constants.MOBILE_SESSION_KEY, mobileCode);

        // 判断该手机号是否注册
        if(!userService.isExistByMobile(mobile)) {
            return new ResultData<>(1, "该手机号不存在!");
        }

        // 模拟发送手机短信验证码到指定用户手机
        mobileCodeSendService.send(mobile, code);
        return new ResultData<>(0, "发送成功!");
    }    
}

✏️ 编写手机短信验证码登录页面 login-mobile.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
    <form method="post" th:action="@{/mobile/form}">
        <input id="mobile" name="mobile" type="text" placeholder="手机号码"><br>
        <div>
            <input name="mobileCode" type="text" placeholder="验证码">
            <button type="button" id="sendCode">获取验证码</button>
        </div>
        <div th:if="${param.error}">
            <span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span>
        </div>
        <button type="submit">登录</button>
    </form>

    <script>
        // 获取手机短信验证码
        $("#sendCode").click(function () {
            var mobile = $(\'#mobile\').val().trim();
            if(mobile == \'\') {
                alert("手机号不能为空");
                return;
            }
            // /code/mobile?mobile=123123123
            var url = "/code/mobile?mobile=" + mobile;
            $.get(url, function(data){
                alert(data.msg);
            });
        });
    </script>
</body>
</html>

✏️ 更改安全配置类 SpringSecurityConfig,设置访问/mobile/page/code/mobile不需要任何权限

@EnableWebSecurity       // 开启 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    /**
     * 定制基于 HTTP 请求的用户访问控制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //...
        // 开启基于 HTTP 请求访问控制
        http.authorizeRequests()
                // 以下访问不需要任何权限,任何人都可以访问
                .antMatchers("/login/page", "/code/image","/mobile/page", "/code/mobile").permitAll()
                // 以下访问需要 ROLE_ADMIN 权限
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 以下访问需要 ROLE_USER 权限
                .antMatchers("/user/**").hasAuthority("ROLE_USER")
                // 其它任何请求访问都需要先通过认证
                .anyRequest().authenticated();
		//...            
    }
    //...
}

✏️ 测试

访问localhost:8080/mobile/page,页面显示:

手机号码输入11111111111,控制台输出:

向手机号 11111111111 发送短信:验证码为 2561,请勿泄露!

浏览器弹出窗口显示”发送成功“。


自定义认证流程配置

分类:

技术点:

相关文章:

  • 2021-05-06
  • 2021-12-05
  • 2021-10-05
  • 2021-12-03
  • 2021-10-18
  • 2022-02-07
  • 2022-12-23
猜你喜欢
  • 2021-12-25
  • 2022-01-01
  • 2021-12-25
  • 2021-12-03
  • 2021-05-23
  • 2021-09-02
  • 2022-02-18
相关资源
相似解决方案