验证码实际上和谜语有点像,分为谜面和谜底。谜面通常是图片,谜底通常为文字。谜面用于展现,谜底用于校验。
总之,不管什么形式的谜面,最后用户的输入内容要和谜底进行验证。
图中蓝色为服务端、澄粉色为客户端。
这是一种最典型的验证码实现方式,实现方式也比较简单。
这种实现方式的优点就是比较简单,缺点就是:因为一套应用部署一个session,当我们把应用部署多套如:A、B、C,他们各自有一个session并且不共享。导致的结果就是验证码和图片由A生成,但是验证请求发送到了B,这样就不可能验证通过。
分布式应用验证码的实现,实际上不是验证码的问题,而是如何保证session唯一性或共享性的问题。主要的解决方案有两种:
可能出于主机资源的考虑,可能出于系统架构的考量,有些应用是无状态的
那么对于这些无状态的应用,我们就无法使用session,或者换个说法从团队开发规范上就不让使用session。那么我们的验证码该怎么做?
这种做法的缺陷是显而易见的:实际上就是将验证码文字在客户端服务端之间走了一遍。虽然是加密后的验证码文字,但是有加密就必须有解密,否则无法验证。所以更为稳妥的做法是为每一个用户生成密钥,并将密钥保存到数据库里面,在对应的阶段内调用密钥进行加密或者解密。
从密码学的角度讲,没有一种对称的加密算法是绝对安全的。所以更重要的是保护好你的密钥。正如没有一把锁头是绝对安全的,更重要的是保护好你的钥匙。
本节基于google开源的验证码实现类库kaptcha,作为验证码工具实现验证码功能开发。验证码工具类通常要具有以下三种功能方法:
本节基于google开源的验证码实现类库kaptcha,作为验证码工具实现验证码功能开发。验证码工具类通常要具有以下三种功能方法:
这种验证码类库有很多,但是都是基于以上逻辑。我们本节使用kaptcha。
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<artifactId>javax.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100
kaptcha.image.height=45
kaptcha.session.key=code
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=35
kaptcha.textproducer.char.length=4
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑
下面的代码加载了配置文件中的kaptcha配置(参考Spring Boot的配置加载),如果是独立的properties文件,需加上PropertySource注解说明。
另外,我们通过加载完成的配置,初始化captchaProducer的Spring Bean,用于生成验证码。
@Component
@PropertySource(value = {"classpath:kaptcha.properties"})
public class CaptchaConfig {
@Value("${kaptcha.border}")
private String border;
@Value("${kaptcha.border.color}")
private String borderColor;
@Value("${kaptcha.textproducer.font.color}")
private String fontColor;
@Value("${kaptcha.image.width}")
private String imageWidth;
@Value("${kaptcha.image.height}")
private String imageHeight;
@Value("${kaptcha.textproducer.char.length}")
private String charLength;
@Value("${kaptcha.textproducer.font.names}")
private String fontNames;
@Value("${kaptcha.textproducer.font.size}")
private String fontSize;
@Value("${kaptcha.session.key}")
private String sessionKey;
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", border);
properties.setProperty("kaptcha.border.color", borderColor);
properties.setProperty("kaptcha.textproducer.font.color", fontColor);
properties.setProperty("kaptcha.image.width", imageWidth);
properties.setProperty("kaptcha.image.height", imageHeight);
properties.setProperty("kaptcha.session.key", sessionKey);
properties.setProperty("kaptcha.textproducer.char.length", charLength);
properties.setProperty("kaptcha.textproducer.font.names", fontNames);
properties.setProperty("kaptcha.textproducer.font.size",fontSize);
//kapcha的配置类
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
至此,Kaptcha开源验证码软件的配置我们就完成了,如果发现IDEA环境下配置文件读取中文乱码,修改如下配置。
生成验证码的Controller。同时需要开放路径"/kaptcha"的访问权限,配置成不需登录也无需任何权限即可访问的路径。
@Controller
public class CodeController {
//kapcha验证码生成
@Resource
DefaultKaptcha captchaProducer;
@RequestMapping("/kaptcha")
public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
// 禁止服务器缓存
response.setDateHeader("Expires",0);
// 设置标准的 HTTP/1.1 no-cache headers.
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
// 设置IE扩展 HTTP/1.1 no-cache headers (use addHeader).
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
// 设置标准 HTTP/1.0 不缓存图片
response.setHeader("Pragma", "no-cache");
// 返回一个 jpeg图片, 默认是text/html
response.setContentType("image/jpeg");
// 生成验证码
String capText = captchaProducer.createText(); // 为图片创建文本
//创建验证码对象----验证码,过期时间
CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
//将验证码存到session
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, captchaImageVO);
//将图片返回给前端
try(ServletOutputStream out = response.getOutputStream();) {
BufferedImage bi = captchaProducer.createImage(capText);
ImageIO.write(bi, "jpg", out);
out.flush();
}//使用try-with-resources不用手动关闭流
}
}
我们要把CaptchaImageVO保存到session里面。所以该类中不要加图片,只保存验证码文字和失效时间,用于后续验证即可。把验证码图片保存起来既没有用处,又浪费内存。
@Data
public class CaptchaImageVO {
//验证码文字
private String code;
//验证码失效时间
private LocalDateTime expireTime;
public CaptchaImageVO(String code, int expireAfterSeconds){
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
}
//验证码是否失效
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
public String getCode() {
return code;
}
}
把如下代码加入到登录页面合适的位置,注意图片img标签放到登录表单中。
<img src="/kaptcha" id="kaptcha" width="110px" height="40px"/>
<script>
window.onload=function(){
var kaptchaImg = document.getElementById("kaptcha");
kaptchaImg.onclick = function(){
kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
}
}
</script>
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.antMatchers("/kaptcha").permitAll()//放行验证码的显示请求,不需要认证
.anyRequest().authenticated()
....
import com.google.code.kaptcha.Constants;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;
@Component
public class CaptchaCodeFilter extends OncePerRequestFilter {
@Resource
MyFailHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 必须是登录的post请求才能进行验证,其他的直接放行
if("/login".equals(request.getRequestURI())
&&request.getMethod().equalsIgnoreCase("post")){
try{
//1.验证谜底与用户输入是否匹配
validate(new ServletWebRequest(request));
}catch(AuthenticationException e){
//2.捕获步骤1中校验出现异常,交给失败处理类进行进行处理
myAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
//通过校验,就放行
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
HttpSession session = request.getRequest().getSession();
//获取用户登录界面输入的code
String codeInRequest = ServletRequestUtils.getStringParameter(
request.getRequest(),"code");
if(codeInRequest.isEmpty()){
throw new SessionAuthenticationException("验证码不能为空");
}
// 获取session池中的验证码谜底
CaptchaImageVO codeInSession = (CaptchaImageVO)
session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(Objects.isNull(codeInSession)) {
throw new SessionAuthenticationException("您输入的验证码不存在");
}
// 校验服务器session池中的验证码是否过期
if(codeInSession.isExpried()) {
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
throw new SessionAuthenticationException("验证码已经过期");
}
// 请求验证码校验
if(!codeInSession.getCode().equals(codeInRequest)) {
throw new SessionAuthenticationException("验证码不匹配");
}
}
}
@Component
//继承该类,是因为其默认的实现,可以简化我们的代码
public class MyFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
String errorMsg="用户名或密码错误";
//验证码错误
//如果是验证码错误,响应JSON数据给前端
if(e instanceof SessionAuthenticationException)
{
errorMsg=e.getMessage();
httpServletResponse.setContentType("text/plain;charset=UTF-8");
httpServletResponse.getWriter().write(errorMsg);
return;
}
//如果是用户名密码错误,调用父类的方法,默认跳转到登录页面
super.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
}
}
最后将CaptchaCodeFilter过滤器放到用户名密码登录过滤器之前执行。login.html登录请求中要传递参数:code
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private ObjectMapper objectMapper=new ObjectMapper();
@Resource
private CaptchaCodeFilter captchaCodeFilter;
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}
//数据源注入
@Autowired
DataSource dataSource;
//持久化令牌配置
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
//用户配置
@Override
@Bean
protected UserDetailsService userDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
if (!manager.userExists("dhy")) {
manager.createUser(User.withUsername("dhy").password("123").roles("admin").build());
}
if (!manager.userExists("大忽悠")) {
manager.createUser(User.withUsername("大忽悠").password("123").roles("user").build());
}
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.//处理需要认证的请求
authorizeRequests()
//放行请求,前提:是对应的角色才行
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
//无需登录凭证,即可放行
.antMatchers("/kaptcha").permitAll()//放行验证码的显示请求
//剩余的请求都需要认证才可以放行
.anyRequest().authenticated()
.and()
//表单形式登录的个性化配置
.formLogin()
.loginPage("/login.html").permitAll()
.loginProcessingUrl("/login").permitAll()
.defaultSuccessUrl("/main.html")//可以记住上一次的请求路径
//登录失败的处理器
.failureHandler(new MyFailHandler())
.and()
//退出登录相关设置
.logout()
//退出登录的请求,是再没退出前发出的,因此此时还有登录凭证
//可以访问
.logoutUrl("/logout")
//此时已经退出了登录,登录凭证没了
//那么想要访问非登录页面的请求,就必须保证这个请求无需凭证即可访问
.logoutSuccessUrl("/logout.html").permitAll()
//退出登录的时候,删除对应的cookie
.deleteCookies("JSESSIONID")
.and()
//记住我相关设置
.rememberMe()
//预定义key相关设置,默认是一串uuid
.key("dhy")
//令牌的持久化
.tokenRepository(jdbcTokenRepository())
.and()
//将CaptchaCodeFilter过滤器放到用户名密码登录过滤器之前执行
.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class)
//csrf关闭
.csrf().disable();
}
//角色继承
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<body>
用户名:<input type="text" name="username" id="name"/><br/>
密码:<input type="password" name="password" id="pwd"/><br/>
验证码: <input type="text" id="code"/>
<img src="/kaptcha" id="kaptcha" width="110px" height="40px"/></br>
记住我: <input type="checkbox" name="remember-me" id="remember-me"/><br/>
<input type="submit" id="loginBtn" value="登录" />
</body>
</html>
<script>
window.onload=function()
{
//获取验证码的dom对象
var kaptchaImg = document.getElementById("kaptcha");
//图片被点击
kaptchaImg.onclick = function(){
//重新请求设置一遍路径,路径后面跟上随机数,防止从浏览器缓存中获取数据
kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
}
}
$("#loginBtn").click(function ()
{
$.ajax({
url:'login',
type:'post',
data: {
'username':$("#name").val(),'password':$("#pwd").val(),
'code':$("#code").val(),'remember-me':$("#remember-me").val()
}
,success: function (res)
{
alert(res)
}
})
})
</script>
不输入验证码:
验证码输入错误:
验证码过期:
验证码输入成功,但是用户名密码密码错误,失败处理器调用父类方法调回到登录页面:
验证码输入成功,用户名密码输入成功,跳转到主页
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121427549
内容来源于网络,如有侵权,请联系作者删除!