spring security 自定义图形验证码(web/前后端分离)

一、准备工作

1.1 导入pom 所需依赖

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <!-- <version>2.7.18</version>-->
    </parent>


 <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- thymeleaf 相关依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

1.2 常量类


    /**
     *  图形验证码
     */
    public static final String SESSION_IMAGE = "session-verifyimage";


    /**
     *  登录的url
     */
    public static final String LOGIN_URL = "/user/login";

二、web端自定义图像验证码

2.1  配置security 配置文件

package com.fasion.config;

import com.fasion.security.LoginImageVerifyFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @Author: LQ
 * @Date 2024/8/26 20:49
 * @Description: security 配置
 */
@Configuration
@Slf4j
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    /**
     * 自定义数据源,从内存中,后期自己写一个mybatis 从数据库查询
     * @throws Exception
     */
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("test").password("{noop}12345").authorities("admin").build());
        return userDetailsManager;
    }

    /**
     *  自定义authenticationManager 管理器,将自定义的数据源加到其中
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }


    /**
     *  用自己的认证管理器
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     *  登录   自定义过滤器
     * @return
     */
    @Bean
    public LoginImageVerifyFilter loginImageVerifyFilter() throws Exception {
        LoginImageVerifyFilter verifyFilter = new LoginImageVerifyFilter();
        verifyFilter.setFilterProcessesUrl("/login.do");// 认证地址
        verifyFilter.setUsernameParameter("loginId");
        verifyFilter.setPasswordParameter("loginPwd");
        verifyFilter.setVerifyImageParams("imageCode");// 图像验证码的参数

        // 认证成功处理逻辑
        verifyFilter.setAuthenticationSuccessHandler((req,resp,auth) -> {
            resp.sendRedirect("/main.html");
        });
        // 认证失败处理逻辑
        verifyFilter.setAuthenticationFailureHandler((req,resp,ex) -> {
            log.info("ex信息:{}",ex.getMessage());
            req.getSession().setAttribute("errMsg",ex.getMessage());
            resp.sendRedirect("/");// 跳到首页
        });

        // 自定义自己的管理器
        verifyFilter.setAuthenticationManager(authenticationManagerBean());

        return verifyFilter;
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/").permitAll()   //放行登录首页
                .mvcMatchers("/kap.jpg").permitAll() // 放行图像验证码
                //.mvcMatchers("/static/**").permitAll() // 静态目录放行
                .anyRequest()
                .authenticated()
                .and()
                .formLogin() //表单设置
                .and()
                .csrf().disable();// 关闭csrf 防护

        // 自定义过滤器替换默认的
        http.addFilterAt(loginImageVerifyFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

2.2  web端配置

package com.fasion.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Author: LQ
 * @Date 2024/8/26 20:55
 * @Description:传统web开发
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {


    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/main.html").setViewName("main");
    }

//    @Override
//    public void addResourceHandlers(ResourceHandlerRegistry registry) {
//        registry.addResourceHandler("/static/").addResourceLocations("/static/**");
//    }
}

2.3 图片验证码工具生成类

该类是利用hutool 包提供的工具类生成图片验证码,具体请参考文档   概述 | Hutool

,由浏览器直接写出图片,该地方如果是集群环境可以将图形验证码的code存到redis中,登录时候再取出来验证;

package com.fasion.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.generator.RandomGenerator;
import com.fasion.constants.ComConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.io.IOException;

/**
 *  图像验证码
 */
@Controller
@Slf4j
public class ComController {

    /**
     *  获取图像验证码
     * @param response
     */
    @RequestMapping("kap.jpg")
    public void getVerifyImage(HttpSession session,HttpServletResponse response) {
        RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
        //定义图形验证码的长、宽、验证码位数、干扰线数量
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(120, 40,4,19);
        lineCaptcha.setGenerator(randomGenerator);
        lineCaptcha.createCode();
        //设置背景颜色
        lineCaptcha.setBackground(new Color(249, 251, 220));
        //生成四位验证码
        String code = lineCaptcha.getCode();
        log.info("图形验证码生成成功:{}",code);
        session.setAttribute(ComConstants.SESSION_IMAGE,code);

        response.setContentType("image/jpeg");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        try {
            lineCaptcha.write(response.getOutputStream());
        } catch (IOException e) {
            log.error("图像验证码获取失败:",e);
        }
    }


}

2.4 验证码过滤器

package com.fasion.security;

import com.fasion.constants.ComConstants;
import com.fasion.exception.CustomerException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.ObjectUtils;

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

/**
 * @Author: LQ
 * @Date 2024/8/26 20:58
 * @Description: 登录验证,图形验证码
 */
@Slf4j
public class LoginImageVerifyFilter extends UsernamePasswordAuthenticationFilter {

    private String verifyImageParams = "captcha";

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

        // 获取图像验证码
        String imageCode = request.getParameter(getVerifyImageParams());
        String realCode = (String) request.getSession().getAttribute(ComConstants.SESSION_IMAGE);
        log.info("传过来的图像验证码为:{},session中实际的是:{}",imageCode,realCode);

        if (!ObjectUtils.isEmpty(imageCode) && !ObjectUtils.isEmpty(realCode) &&
                imageCode.equalsIgnoreCase(realCode)) {
            // 调用父类的认证方法
            return super.attemptAuthentication(request,response);
        }

        throw new CustomerException("图像验证码不正确!!!");
    }

    public String getVerifyImageParams() {
        return verifyImageParams;
    }

    public void setVerifyImageParams(String verifyImageParams) {
        this.verifyImageParams = verifyImageParams;
    }
}

2.5 自定义异常类

package com.fasion.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * @Author: LQ
 * @Date 2024/8/26 21:07
 * @Description: 自定义异常
 */
public class CustomerException extends AuthenticationException {


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

2.6 前端页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
    <!-- 引入样式 -->
    <style type="text/css">
        #app{width: 600px;margin: 28px auto 10px }
        img{cursor: pointer;}
    </style>
</head>
<body>
   <div id="app">
       <form th:action="@{/login.do}" method="post" >
           <div>
               <label>用户名:</label>
               <input type="text" name="loginId">
           </div>
           <div>
               <label>密码:</label>
               <input type="text" name="loginPwd" >
           </div>
           <div>
               <label>图像验证码:</label>
               <input type="text" name="imageCode">
               <img src="/kap.jpg">
           </div>
           <div>
               <label>错误信息:<span th:text="${session.errMsg}"></span></label>
           </div>
           <div>
              <button type="submit" name="登录">登录</button>
           </div>
       </form>
   </div>

</body>
</html>

2.6.1 前端效果

2.6.2 登录失败展示效果

2.6.3 登录成功


三、前后端分离自定义验证码(json数据格式)

3.1  security 配置

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    /**
     *  自定义数据源
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("test").password("{noop}1234").authorities("admin").build());
        return userDetailsManager;
    }

    /**
     *  配置数据源
     *
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService());
    }


    /**
     *  显示指定自己的 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public LoginVerifyCaptchaFilter loginVerifyImgFilter() throws Exception {
        LoginVerifyCaptchaFilter filter = new LoginVerifyCaptchaFilter();
        filter.setImageParams("verifyImg");// 图形验证码请求参数
        filter.setUsernameParameter("loginId");
        filter.setPasswordParameter("pwd");
        filter.setFilterProcessesUrl("/login.do");
        // 成功的响应
        filter.setAuthenticationSuccessHandler((req,resp,auth) -> {
            Map<String,Object> resMap = new HashMap<>();
            resMap.put("code","0000");
            resMap.put("msg","登录成功!");
            resMap.put("data",auth);
            WebUtils.writeJson(resp,resMap);
        });
        //失败的处理
        filter.setAuthenticationFailureHandler((req,resp,ex) -> {
            Map<String,Object> resMap = new HashMap<>();
            resMap.put("code","5001");
            resMap.put("msg",ex.getMessage());
            WebUtils.writeJson(resp,resMap);
        });

        // 指定自己的authenticationmanager
        filter.setAuthenticationManager(authenticationManagerBean());

        return filter;
    }




    /**
     *  springsecurity 配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/comm/kaptcha.jpg").permitAll()// 该路径放行
                .mvcMatchers("/").permitAll()// 入口页放行
                .anyRequest().authenticated()// 所有请求都需要认证
                .and()
                .formLogin()// 表单配置
                .loginPage("/")
                .and()
                .csrf().disable();//关闭csrf 防护


        // 定义登录图形过滤器,替换掉UsernamePasswordAuthenticationFilter
        http.addFilterAt(loginVerifyImgFilter(), UsernamePasswordAuthenticationFilter.class);

    }

3.2 图片验证码base生成

@RestController
@Slf4j
public class CommController {


    /**
     * 获取图形验证码
     * @param session
     * @param response
     * @return
     */
    @RequestMapping("/comm/kaptcha.jpg")
    public Map<String,String> image(HttpSession session, HttpServletResponse response) {
        // 自定义纯数字的验证码(随机4位数字,可重复)
        RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(92, 40,4,10);
        lineCaptcha.setGenerator(randomGenerator);
        // 重新生成code
        lineCaptcha.createCode();
        // 获取
        String captchaCode = lineCaptcha.getCode();
        log.info("获取到验证码为:{}",captchaCode);
        session.setAttribute(ComConst.SESSION_CAPTCHA,captchaCode);

        // 转为base64
        String imageBase64 = lineCaptcha.getImageBase64();
        HashMap<String, String> resMap = MapUtil.newHashMap();
        resMap.put("code","0000");
        resMap.put("data",imageBase64);
        return resMap;
    }

}

3.2.1  postman 效果

一般由后台将图片转为base64后,前端再通过传过来的base64 评价 image/ 到 img标签的src 就可以显示出来;需要加上前缀:data:image/jpeg;base64, 后面再把返回的data中的结果拼接到后面

3.3 验证码过滤器(核心类)

该过滤器需要加到配置security 配置里面,用来替换到默认的 UsernamePasswordAuthenticationFilter 过来器,所以之前配置的

formLogin.loginPage("/") 
.loginProcessingUrl("/doLogin")    //form表单提交地址(POST)
//.defaultSuccessUrl("/main",true)    //登陆成功后跳转的页面,也可以通过Handler实现高度自定义
.successForwardUrl("/main")
这些配置实际都会失效
public class LoginVerifyCaptchaFilter extends UsernamePasswordAuthenticationFilter {


    private String imageParams = "verifyImg";


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

        Map<String,String> userMap = null;
        try {
            // 用户信息
            userMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 获取图形验证码
        String reqImgCode = userMap.get(getImageParams());
        String username = userMap.get(getUsernameParameter());
        String password = userMap.get(getPasswordParameter());
        // 获取session 的验证码
        String realCode = (String)request.getSession().getAttribute(ComConst.SESSION_CAPTCHA);

        // 图形验证码通过
        if (!ObjectUtils.isEmpty(reqImgCode) && !ObjectUtils.isEmpty(realCode) && realCode.equalsIgnoreCase(reqImgCode)) {
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        throw new CustomerException("图形验证码错误!");
      //  return super.attemptAuthentication(request,response);
    }

    public String getImageParams() {
        return imageParams;
    }

    public void setImageParams(String imageParams) {
        this.imageParams = imageParams;
    }
}

3.4 新建一个测试类 

@RestController
public class HelloController {


    @RequestMapping("hello")
    public String hello() {
        return "hello web security ";
    }

}

3.5 验证结果

我们看到 hello 接口是受到保护的,没有认证是访问不了的

3.5.1  访问hello接口

这个时候登录成功后再将登录接口返回的cookie 信息放到hello接口中请求

3.6 增加异常处理

该地方是用来处理用户未登录,接口提示需要用户有认证信息,这个时候我们没有登录访问受限接口 hello 就会提示,请认证后再来请求接口,新增一个工具类,用于将写出json数据

/**
 * 写出json 数据
 *
 * @param response
 * @throws Exception
 */
public static void writeJson(HttpServletResponse response, Object object) {
    response.setContentType("application/json;charset=UTF-8");
    response.setCharacterEncoding("UTF-8");
    response.setHeader("Cache-Control", "no-cache");
    PrintWriter pw = null;
    try {
        pw = response.getWriter();
        pw.print(JSONUtil.toJsonStr(object));
        pw.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (pw != null) {
            pw.close();
        }
    }
}
.authenticationEntryPoint(((request, response, authException) -> {
                    // 判断是否有登录
                    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                    if (authentication == null) {
                        WebUtils.writeJson(response,"请认证后再来请求接口");
                    } else {
                        WebUtils.writeJson(response,authException.getLocalizedMessage());
                    }
                }))

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要实现Spring Security自定义短信验证码登录,需要遵循以下步骤: 1. 配置短信验证码的过滤器 在Spring Security的配置类中,添加一个短信验证码过滤器,用于拦截短信验证码登录请求,并校验验证码是否正确。可以参考以下代码: ``` @Bean public SmsCodeFilter smsCodeFilter() throws Exception { SmsCodeFilter smsCodeFilter = new SmsCodeFilter(); smsCodeFilter.setAuthenticationManager(authenticationManagerBean()); smsCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); return smsCodeFilter; } ``` 2. 实现短信验证码的校验逻辑 创建一个实现了`AuthenticationProvider`接口的短信验证码认证提供者,并在其中实现短信验证码的校验逻辑。可以参考以下代码: ``` @Component public class SmsCodeAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); String smsCode = (String) authenticationToken.getCredentials(); // 校验短信验证码 if (smsCodeIsValid(mobile, smsCode)) { // 构造认证通过的令牌 SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(mobile); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } else { throw new BadCredentialsException("短信验证码不正确"); } } private boolean smsCodeIsValid(String mobile, String smsCode) { // 根据手机号和短信验证码进行校验 // ... return true; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } } ``` 3. 配置AuthenticationManager 在Spring Security的配置类中,配置`AuthenticationManager`,并将自定义的短信验证码认证提供者加入到其中。可以参考以下代码: ``` @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(smsCodeAuthenticationProvider()); } ``` 4. 配置登录接口 在Spring Security的配置类中,配置短信验证码登录的登录接口。可以参考以下代码: ``` @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(smsCodeFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/sms-login").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .and() .csrf().disable(); } ``` 5. 发送短信验证码 在登录接口中,添加发送短信验证码的逻辑。可以参考以下代码: ``` @PostMapping("/sms-code") @ResponseBody public String sendSmsCode(@RequestParam String mobile) { // 发送短信验证码 // ... return "success"; } ``` 以上就是Spring Security自定义短信验证码登录的实现步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星空寻流年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值