您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

Spring Security自定义认证逻辑实现图片验证码登录

时间:2022-12-12 15:28:07  来源:今日头条  作者:马士兵教育CTO

前言

相信大家在网上冲浪都遇到过登录时输入图片验证码的情况,既然我们已经学习了 Spring Security,也上手实现过几个案例,那不妨来研究一下如何实现这一功能。

首先需要明确的是,登录时输入图片验证码,属于认证功能的一部分,所以本文不涉及授权功能。

认证流程简析

在上文中,我们介绍了认证流程,以及相关的关键类,可知 AuthenticationProvider 定义了 Spring Security 中的验证逻辑,该类的类关系图:

 

我们来看下 AuthenticationProvider 的定义:

public interface AuthenticationProvider {
  Authentication authenticate(Authentication authentication) throws AuthenticationException;

  boolean supports(Class<?> authentication);
}

可以看到,AuthenticationProvider 中就两个方法:

  • authenticate 方法用来做验证,就是验证用户身份。
  • supports 则用来判断当前的 AuthenticationProvider 是否支持对应的 Authentication。

这里又涉及到一个东西,就是 Authentication。Authentication 本身是一个接口,从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表。我们来看下 Authentication 的定义:

package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
  // 获取用户的权限
  Collection<? extends GrantedAuthority> getAuthorities();

  //获取用户凭证,一般是密码,认证之后会移出,来保证安全性
  Object getCredentials();
	//获取用户携带的详细信息,Web应用中一般是访问者的ip地址和sessionId
  Object getDetAIls();
	// 获取当前用户
  Object getPrincipal();
	//判断当前用户是否认证成功
  boolean isAuthenticated();

  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

官方文档里说过,当用户提交登录信息时,会将用户名和密码进行组合成一个实例
UsernamePasswordAuthenticationToken,而这个类是 Authentication 的一个常用的实现类,用来进行用户名和密码的认证,类似的还有 RememberMeAuthenticationToken,它用于记住我功能。

Spring Security 支持多种不同的认证方式,不同的认证方式对应不同的身份类型,每个 AuthenticationProvider 需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时 Spring Security 会生成
UsernamePasswordAuthenticationToken,它是一个 Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个 AuthenticationProvider 来处理它?

我们在 DaoAuthenticationProvider 的基类
AbstractUserDetailsAuthenticationProvider 发现以下代码:

    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

也就是说当web表单提交用户名密码时,Spring Security 由 DaoAuthenticationProvider 处理。

DaoAuthenticationProvider 的父类是
AbstractUserDetailsAuthenticationProvider, 在该类中的 authenticate()方法用于处理认证逻辑,这里就不粘贴代码了,该方法大致逻辑如下:

  1. 首先实例化UserDetails对象,调用了retrieveUser方法获取到了一个user对象,retrieveUser是一个抽象方法。该方法进一步会调用我们自己在登录时候的写的 loadUserByUsername 方法,具体在自定义的 UserDetailsService 或 InMemoryUserDetailsManager 等。
  2. 如果没拿到信息就会抛出异常,如果查到了就会去调用preAuthenticationChecks的check(user)方法去进行预检查。在预检查中进行了三个检查,因为UserDetail类中有四个布尔类型,去检查其中的三个,用户是否锁定用户是否过期用户是否可用
  3. 预检查之后紧接着去调用了additionalAuthenticationChecks方法去进行附加检查,这个方法也是一个抽象方法,检查密码是否匹配,在DaoAuthenticationProvider 的 additionalAuthenticationChecks 方法中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。我们想要校验图片验证码,就可以和密码一起校验,即我们重写 additionalAuthenticationChecks 方法。
  4. 最后在 postAuthenticationChecks.check 方法中检查密码是否过期。
  5. 所有的检查都通过,则认为用户认证是成功的。用户认证成功之后,会将这些认证信息和user传递进去,调用createSuccessAuthentication方法。

DaoAuthenticationProvider 中的
additionalAuthenticationChecks 方法用于比对密码,逻辑比较简单,就是将 password 加密后与事先保存好的密码做比对。代码如下:

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  if (authentication.getCredentials() == null) {
    this.logger.debug("Failed to authenticate since no credentials provided");
    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  } else {
    String presentedPassword = authentication.getCredentials().toString();
    if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
      this.logger.debug("Failed to authenticate since password does not match stored value");
      throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    }
  }
}

实操

自定义认证

我们复用之前的项目
springboot-security-inmemory,通过 postman 进行测试,不需要额外构建 html 页面。

改动内容包括自定义 DaoAuthenticationProvider 实现类,重写
additionalAuthenticationChecks 方法,以及生成图片验证码。

项目增加如下依赖:

<dependency>
  <groupId>com.Github.penggle</groupId>
  <artifactId>kaptcha</artifactId>
  <version>2.3.2</version>
</dependency>
复制代码

创建 VerifyService 获取验证码图片

@Service
public class VerifyService {

  public Producer getProducer() {
    Properties properties = new Properties();
    properties.setProperty("kaptcha.image.width", "150");
    properties.setProperty("kaptcha.image.height", "50");
    properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
    properties.setProperty("kaptcha.textproducer.char.length", "4");
    Config config = new Config(properties);
    DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
    defaultKaptcha.setConfig(config);
    return defaultKaptcha;
  }
}

这段配置很简单,我们就是提供了验证码图片的宽高、字符库以及生成的验证码字符长度。

VerifyCodeController 文件中增加图片返回接口:

@RestController
@Slf4j
public class VerifyCodeController {

  @Autowired
  VerifyService verifyService;

  @GetMApping("/verify-code")
  public void getVerifyCodePng(HttpServletRequest request, HttpServletResponse resp)
      throws IOException {
    resp.setDateHeader("Expires", 0);
    resp.setHeader("Cache-Control",
        "no-store, no-cache, must-revalidate");
    resp.addHeader("Cache-Control", "post-check=0, pre-check=0");
    resp.setHeader("Pragma", "no-cache");
    resp.setContentType("image/jpeg");

    Producer producer = verifyService.getProducer();
    String text = producer.createText();
    HttpSession session = request.getSession();
    session.setAttribute("verify_code", text);
    BufferedImage image = producer.createImage(text);
    try (ServletOutputStream out = resp.getOutputStream()) {
      ImageIO.write(image, "jpg", out);
    }
  }

}

自定义 DaoAuthenticationProvider 实现类

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    // 验证码比对
    HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder
        .getRequestAttributes()).getRequest();
    String code = req.getParameter("code");
    HttpSession session = req.getSession(false);
    String verify_code = (String) session.getAttribute("verify_code");
    if (code == null || verify_code == null || !code.equals(verify_code)) {
      throw new AuthenticationServiceException("验证码错误");
    }
    // 密码比对
    super.additionalAuthenticationChecks(userDetails, authentication);
  }
}

案例比较简单,生成验证码图片时,顺便存放到 session 中,登录验证时从 session 中获取验证码字符串,然后与传来的验证码进行比对。

修改 SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
  PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

  @Override
  @Bean
  protected AuthenticationManager authenticationManager() throws Exception {
    ProviderManager manager = new ProviderManager(Arrays.asList(myAuthenticationProvider()));
    return manager;
  }

  @Bean
  @Override
  protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("hresh").password("123").roles("admin").build());
    return manager;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable()
        .authorizeRequests()
        .antMatchers("/verify-code").permitAll()
        .antMatchers("/code").permitAll()
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .successHandler((req, resp, auth) -> {
          resp.setContentType("application/json;charset=utf-8");
          PrintWriter out = resp.getWriter();
          out.write(new ObjectMapper().writeValueAsString(Result.ok(auth.getPrincipal())));
          out.flush();
          out.close();
        })
        .failureHandler((req, resp, e) -> {
          resp.setContentType("application/json;charset=utf-8");
          PrintWriter out = resp.getWriter();
          out.write(new ObjectMapper().writeValueAsString(Result.failed(e.getMessage())));
          out.flush();
          out.close();
        })
        .permitAll();
  }

  @Bean
  MyAuthenticationProvider myAuthenticationProvider() {
    MyAuthenticationProvider myAuthenticationProvider = new MyAuthenticationProvider();
    myAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    myAuthenticationProvider.setUserDetailsService(userDetailsService());
    return myAuthenticationProvider;
  }

}

测试

首先获取图片验证码

 

输入正确的验证码和错误的密码,进行登录:

 

如果输入错误的验证码

 

问题

使用AirPost测试遇到的问题

controller文件中设置了两个api,一个方法往session中加了一个值,另一个方法从sesion中取值,结果两次操作的sessionId不同。

代码如下所示:

@GetMapping("/verify-code")
public void getVerifyCodePng(HttpServletRequest request) {
  Producer producer = verifyService.getProducer();
  String text = producer.createText();
  HttpSession session = request.getSession();
  session.setAttribute("verify_code", text);
  session.setAttribute("user", "hresh");
  log.info("code is " + text + " session id is " + session.getId());
}

@GetMapping("/code")
public String getVerifyCode(HttpServletRequest request) {
  HttpSession session = request.getSession();
  String verify_code = (String) session.getAttribute("verify_code");
  log.info("input code is " + verify_code + " session id is " + session.getId());
  return verify_code;
}

执行结果:

input code is 8045 session id is 77EBBF046128BC3618C825F62C0A2099
input code is null session id is A69A7D10EAFB0471B5D658489522739D

网上有类似的问题,可以参考这篇文章:blog.csdn.NET/weixin_4164…

相关问题还可以看这篇文章:跨域访问sessionid不一致问题

总结

上面的例子主要是针对认证功能做一点增强,在实际应用中,其他的登录场景也可以考虑这种方案,例如目前广为流行的手机号码动态登录,就可以使用这种方式认证。

后续我们还会自定义认证流程中的密码比对,以及授权流程中的权限比对,使之更佳贴近实际应用场景。



Tags:验证码   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
注意!密码、验证码都没说,钱是怎么被骗走的?
转自:科普中国“我密码和验证码都没有给他,为什么钱还是被骗子骗走了?”骗子是怎么做到的呢?真实案例今年1月,武汉市民张某在家接到一陌生电话,电话自称是某平台的客服,告诉张某购...【详细内容】
2024-04-11  Search: 验证码  点击:(4)  评论:(0)  加入收藏
微软 Edge 浏览器将推出两大新功能:设备端加密和自动验证验证码
8 月 23 日消息,微软正在为其 Edge 浏览器开发新功能,将为用户带来更便捷的体验以及更好的隐私和安全保护。据 Leopeva64 发现,微软正在为 Edge 浏览器的安卓版本开发两个新功...【详细内容】
2023-08-23  Search: 验证码  点击:(294)  评论:(0)  加入收藏
在WordPress登录和注册表中添加验证码
您想将CAPTCHA添加到WordPress网站的登录和注册表单中吗?WordPress登录和用户注册页面通常是黑客,垃圾邮件发送者和暴力攻击的主要目标。避免这些攻击的一种方法是使用CAPTCHA...【详细内容】
2023-08-03  Search: 验证码  点击:(258)  评论:(0)  加入收藏
验证码越来越抽象,我快不能证明自己是人类了。
为了把机器人拦在门外,人类发明了验证码。但不知何时起,验证码开始处处刁难人类。它就像路上的减速带,你过是能过,但总会咯噔一下,而且越急就越难受。黑胖和我说,那夜他突想温习两...【详细内容】
2023-07-03  Search: 验证码  点击:(242)  评论:(0)  加入收藏
验证码竟成骚扰工具,谁在作祟?
本是用于APP注册、网站登录身份验证的手机短信验证码,摇身一变成了骚扰手机用户的帮凶,这是怎么回事儿?近期,不少市民到四川遂宁公安机关报案称:自己手机突然“抽风”,在非本人操...【详细内容】
2023-04-21  Search: 验证码  点击:(254)  评论:(0)  加入收藏
五分钟搞定验证码,你学会了吗?
哈喽,大家好,我是了不起。我们其实很经常看到,登录一些网站其实是需要验证码的。使用验证码是现在很多网站通行的一种方式,因为计算机很难识别验证码,所以可以识别验证码的用户就...【详细内容】
2023-04-04  Search: 验证码  点击:(173)  评论:(0)  加入收藏
KgCaptcha 行为验证码安全策略设置
前言在验证码项目中,都会遇到验证码被恶意大量高频的调用,给服务造成很多无效的注册或登录,占用大量的系统资源。而我在想,有没有哪一款验证码产品可以设置黑/白名单限制IP访问...【详细内容】
2023-03-03  Search: 验证码  点击:(135)  评论:(0)  加入收藏
Spring Security自定义认证逻辑实现图片验证码登录
前言相信大家在网上冲浪都遇到过登录时输入图片验证码的情况,既然我们已经学习了 Spring Security,也上手实现过几个案例,那不妨来研究一下如何实现这一功能。首先需要明确的是...【详细内容】
2022-12-12  Search: 验证码  点击:(252)  评论:(0)  加入收藏
一键去除网页BOM属性「解决乱码,头部空白,验证码不显示问题」
几个常出现的问题:1.网站打开空白2.页面头部出现多余的空白3.网站出现乱码,如“锘�”4.后台登录验证码不显示解决方案:1.选用专业的编辑器,例如notepad++,sublime,editplus这样不会...【详细内容】
2022-11-25  Search: 验证码  点击:(337)  评论:(0)  加入收藏
一个简单的设置,让手机自动填充验证码,省心省力
大家好,我们日常手机登录各种不同的软件APP,除了使用账号密码登录以外,还可以使用手机验证码登录现在很多平台为了安全性,在我们输入密码登录的时候,额外还要求接受短信验证码进...【详细内容】
2022-11-22  Search: 验证码  点击:(219)  评论:(0)  加入收藏
▌简易百科推荐
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(2)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(7)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
站内最新
站内热门
站内头条