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