security整合captcha验证码

7/27/2022 安全框架

# 前述

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 二、新增登录失败处理器

/**
 * 登录失败处理器
 *
 * @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)));
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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

/**
 * 自定义验证代码过滤
 *
 * @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);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 四、修改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;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

# 五、新增验证码接口

/**
 * @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());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 六、登录页修改

<!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"></p>
    <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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

# 七、测试效果图

SpringSecurity

SpringSecurity