security整合captcha验证码

前述

SpringSecurity整合验证码登录实现

一、引入依赖

<properties>
    <velocity-engine-core.version>2.3</velocity-engine-core.version>
    <kaptcha.version>2.3.2</kaptcha.version>
</properties>
<dependencies>
    <!--验证码-->
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>${velocity-engine-core.version}</version>
    </dependency>
    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>${kaptcha.version}</version>
    </dependency>
</dependencies>

二、新增登录失败处理器

/**
 * 登录失败处理器
 *
 * @author LiJunYi
 * @date 2022/07/27
 */
@Component
@Slf4j
public class CustomAuthenticationFailureImpl implements AuthenticationFailureHandler
{

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.error(e.getMessage());
        String msg = "验证码";
        if (e.getMessage().contains(msg))
        {
            int code = HttpStatus.ERROR;
            ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, e.getMessage())));
        }else
        {
            int code = HttpStatus.UNAUTHORIZED;
            msg = "用户名或密码错误";
            ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
        }
    }
}

三、自定义验证验证码过滤器

/**
 * 自定义验证代码过滤
 *
 * @author LiJunYi
 * @date: 2022/07/27
 */
@Component
public class ValidateCodeFilter extends OncePerRequestFilter
{

    @Resource
    private CustomAuthenticationFailureImpl authenticationFailure;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
    {
        String loginUrl = "/userLogin";
        if (loginUrl.equalsIgnoreCase(request.getRequestURI())
                && HttpMethod.POST.equalsIgnoreCase(request.getMethod())) {
            try {
                HttpSession session = request.getSession();
                // Session中的校验码
                String sessionImgCode = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
                // Session 中校验码过期时间
                LocalDateTime expireTime = (LocalDateTime) session.getAttribute(Constants.KAPTCHA_SESSION_DATE);
                // 客户端提交的校验码
                String requestImgCode = request.getParameter(com.example.codefilter.common.constant.Constants.CAPTCHA_CODE);
                if (StrUtil.isEmpty(requestImgCode)) {
                    throw new ValidateCodeException("验证码不能为空!");
                }
                if (expireTime == null || LocalDateTime.now().isAfter(expireTime)) {
                    // 清除Session中校验码相关信息
                    session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
                    session.removeAttribute(Constants.KAPTCHA_SESSION_DATE);
                    throw new ValidateCodeException("验证码已过期!");
                }
                if (!requestImgCode.equalsIgnoreCase(sessionImgCode)) {
                    throw new ValidateCodeException("验证码不正确!");
                }
                // 清除Session中校验码相关信息
                session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
                session.removeAttribute(Constants.KAPTCHA_SESSION_DATE);
            } catch (ValidateCodeException e) {
                authenticationFailure.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

四、修改SecurityConfig,加入自定义过滤器

/**
 * @version 1.0.0
 * @className: SecurityConfig
 * @description: SpringSecurity 5.7.x新用法配置
 * @author: LiJunYi
 * @create: 2022/7/26 8:43
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
    /**
     * 自定义用户登录处理逻辑
     */
    private final MyUserDetailsServiceImpl userDetailsService;

    /**
     * 登录成功处理
     */
    private final CustomAuthenticationSuccessImpl customAuthenticationSuccess;

    /**
     * 登录失败处理
     */
    private final CustomAuthenticationFailureImpl customAuthenticationFailure;

    /**
     * 注销成功后处理
     */
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * 未经授权处理程序
     */
    private final AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 并发登录控制处理
     */
    private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;

    /**
     * 验证代码过滤
     */
    private final ValidateCodeFilter validateCodeFilter;

    /**
     * 构造函数注入
     *
     * @param userDetailsService          用户详细信息服务
     * @param logoutSuccessHandler        注销成功处理程序
     * @param unauthorizedHandler         未经授权处理程序
     * @param expiredSessionStrategy      过期会话策略
     * @param customAuthenticationSuccess 自定义身份验证成功
     * @param customAuthenticationFailure 登录失败处理器
     * @param validateCodeFilter  自定义验证码过滤器
     */
    @Autowired
    public SecurityConfig(MyUserDetailsServiceImpl userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler,
                          AuthenticationEntryPointImpl unauthorizedHandler,
                          CustomExpiredSessionStrategyImpl expiredSessionStrategy,
                          CustomAuthenticationSuccessImpl customAuthenticationSuccess, CustomAuthenticationFailureImpl customAuthenticationFailure, ValidateCodeFilter validateCodeFilter) {
        this.userDetailsService = userDetailsService;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.unauthorizedHandler = unauthorizedHandler;
        this.expiredSessionStrategy = expiredSessionStrategy;
        this.customAuthenticationSuccess = customAuthenticationSuccess;
        this.customAuthenticationFailure = customAuthenticationFailure;
        this.validateCodeFilter = validateCodeFilter;
    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
        return configuration.getAuthenticationManager();
    }

    /**
     * 配置加密方式
     *
     * @return {@link PasswordEncoder}
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
     * 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
     * @return {@link HttpSessionEventPublisher}
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * 过滤器链
     *
     * @param http http
     * @return {@link SecurityFilterChain}
     * @throws Exception 异常
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception
    {
        // 添加验证码校验过滤器,在UsernamePasswordAuthenticationFilter过滤器前执行
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests(authorize ->
                authorize.mvcMatchers("/login","/userLogin","/captchaCode","/noPermission","/static/css/**","/static/util/javascript/**").permitAll()
                        .anyRequest().authenticated()
        )
                .csrf().disable()
                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/userLogin")
                        // 登录成功后的处理器
                        .successHandler(customAuthenticationSuccess)
                        // 登录失败处理器
                        .failureHandler(customAuthenticationFailure)
                        .permitAll());
        // 退出过滤器
        http.logout(logout -> logout
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler(logoutSuccessHandler));
        // 无权限访问处理器
        http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
        // 并发会话控制
        http.sessionManagement(session ->  session
                .maximumSessions(1)
                .expiredSessionStrategy(expiredSessionStrategy));
        http.userDetailsService(userDetailsService);
        return http.build();
    }

    /**
     * 验证码生产商
     *
     * @return {@link DefaultKaptcha}
     */
    @Bean
    public DefaultKaptcha captchaProducer() {
        Properties properties = new Properties();
        // 显示边框
        properties.setProperty("kaptcha.border","yes");
        // 边框颜色
        properties.setProperty("kaptcha.border.color","105,179,90");
        // 字体颜色
        properties.setProperty("kaptcha.textproducer.font.color","blue");
        // 字体大小
        properties.setProperty("kaptcha.textproducer.font.size","35");
        // 图片宽度
        properties.setProperty("kaptcha.image.width","125");
        // 图片高度
        properties.setProperty("kaptcha.image.height","40");
        // 验证码长度
        properties.setProperty("kaptcha.textproducer.char.length","4");
        // 文本内容 从设置字符中随机抽取
        properties.setProperty("kaptcha.textproducer.char.string","0A1B2C3D4E5F6g7h8i9jk");
        DefaultKaptcha kaptcha = new DefaultKaptcha();
        kaptcha.setConfig(new Config(properties));
        return kaptcha;
    }
}

五、新增验证码接口

/**
 * @version 1.0.0
 * @className: ValidateController
 * @description: 获取验证码
 * @author: LiJunYi
 * @create: 2022/7/27 9:49
 */
@Controller
public class ValidateController
{
    @Resource
    private Producer captchaProducer;

    @GetMapping("/captchaCode")
    public void  createImgCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 生成图形校验码内容
        String text = captchaProducer.createText();
        // 将验证码内容存入HttpSession
        request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text);
        // 将验证码有效期存入HttpSession,60秒有效
        request.getSession().setAttribute(Constants.KAPTCHA_SESSION_DATE, LocalDateTime.now().plusSeconds(60));
        // 生成图形校验码图片
        BufferedImage bufferedImage = captchaProducer.createImage(text);
        // 将校验码图片信息输出到浏览器
        ImageIO.write(bufferedImage, "jpeg", response.getOutputStream());
    }
}

六、登录页修改

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>系统登录</title>
    <link rel="stylesheet" type="text/css" th:href="@{/static/css/elemui-2.15.9.css}"/>
</head>
<body>

<div class="el-form-item">
    <div class="el-form-item__content" style="margin-left: 80px;">
        <h1>登录页面</h1>
    </div>
</div>
<form id="form-login" method="post">
    <p class="error">
    <div class="el-form-item">
        <label class="el-form-item__label" style="width: 80px">用户名</label>
        <div class="el-form-item__content" style="margin-left: 80px;">
            <div class="el-input">
                <input style="width: 300px;" type="text" autocomplete="off" class="el-input__inner" name="username"/>
            </div>
        </div>
    </div>
    <div class="el-form-item">
        <label class="el-form-item__label" style="width: 80px">密码</label>
        <div class="el-form-item__content" style="margin-left: 80px;">
            <div class="el-input">
                <input style="width: 300px;" type="password" autocomplete="off" class="el-input__inner" name="password"/>
            </div>
        </div>
    </div>
    <div class="el-form-item">
        <label class="el-form-item__label" style="width: 80px">验证码</label>
        <div class="el-form-item__content" style="margin-left: 80px;">
            <div class="el-input">
                <input style="width: 300px;" type="text" autocomplete="off" class="el-input__inner" name="captchaCode"/>
            </div>
        </div>
    </div>
    <div class="el-form-item">
        <div class="el-form-item__content" style="margin-left: 80px;">
            <img id="captchaCode" src="/captchaCode" style="cursor: pointer;" title="看不清?换一张"/>
        </div>
    </div>
    <div class="el-form-item">
        <div class="el-form-item__content" style="margin-left: 80px;">
            <button type="button" class="el-button el-button--primary" id="btn-login">
                <span>登录</span>
            </button>
        </div>
    </div>

</form>
<script type="text/javascript" th:src="@{/static/util/javascript/jquery-3.5.1.min.js}"></script>
<script th:inline="javascript">
    const ctx = [[${#httpServletRequest.getContextPath()}]];
    $('#btn-login').bind('click',function () {
        $.ajax({
            url: '/userLogin',
            type: 'post',
            data: $('#form-login').serialize(),
            success: function (obj) {
                debugger
                if (obj.code == 200) {
                    window.location.href = ctx + '/index';
                } else {
                    $('.error').text(obj.msg);
                }
            },
            dataType:'json'
        });
    })
    // 刷新验证码
    $("#captchaCode").bind("click", function () {
        $(this).hide().attr('src', '/captchaCode?random=' + Math.random()).fadeIn();
    });
</script>
</body>
</html>

七、测试效果图

SpringSecuritySpringSecurity
Last Updated 6/14/2024, 3:05:31 AM