您的位置:首页 > 娱乐 > 八卦 > spring security 自定义图形验证码(web/前后端分离)

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

2025/1/9 3:52:44 来源:https://blog.csdn.net/qq_36260963/article/details/141574415  浏览:    关键词: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*/@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();userDetailsManager.createUser(User.withUsername("test").password("{noop}12345").authorities("admin").build());return userDetailsManager;}/***  自定义authenticationManager 管理器,将自定义的数据源加到其中* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}/***  用自己的认证管理器* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}/***  登录   自定义过滤器* @return*/@Beanpublic 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;}@Overrideprotected 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 {@Overridepublic 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";@Overridepublic 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*/@Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();userDetailsManager.createUser(User.withUsername("test").password("{noop}1234").authorities("admin").build());return userDetailsManager;}/***  配置数据源**/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}/***  显示指定自己的 AuthenticationManager* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic 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);});// 指定自己的authenticationmanagerfilter.setAuthenticationManager(authenticationManagerBean());return filter;}/***  springsecurity 配置* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().mvcMatchers("/comm/kaptcha.jpg").permitAll()// 该路径放行.mvcMatchers("/").permitAll()// 入口页放行.anyRequest().authenticated()// 所有请求都需要认证.and().formLogin()// 表单配置.loginPage("/").and().csrf().disable();//关闭csrf 防护// 定义登录图形过滤器,替换掉UsernamePasswordAuthenticationFilterhttp.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);// 重新生成codelineCaptcha.createCode();// 获取String captchaCode = lineCaptcha.getCode();log.info("获取到验证码为:{}",captchaCode);session.setAttribute(ComConst.SESSION_CAPTCHA,captchaCode);// 转为base64String 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";@Overridepublic 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" propertysetDetails(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());}}))

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com