一、准备工作
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());}}))