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

news2024/11/24 17:50:22

前言

相信大家在网上冲浪都遇到过登录时输入图片验证码的情况,既然我们已经学习了 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 方法,具体在自定义的 UserDetailsServiceInMemoryUserDetailsManager 等。
  2. 如果没拿到信息就会抛出异常,如果查到了就会去调用preAuthenticationCheckscheck(user)方法去进行预检查。在预检查中进行了三个检查,因为UserDetail类中有四个布尔类型,去检查其中的三个,用户是否锁定用户是否过期用户是否可用
  3. 预检查之后紧接着去调用了additionalAuthenticationChecks方法去进行附加检查,这个方法也是一个抽象方法,检查密码是否匹配,在DaoAuthenticationProvideradditionalAuthenticationChecks 方法中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。我们想要校验图片验证码,就可以和密码一起校验,即我们重写 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不一致问题

总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/90856.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【录用案例】计算机电子类SCI,仅1个月15天录用

【期刊简介】IF&#xff1a;1.0-2.0&#xff0c;JCR4区&#xff0c;中科院4区 【检索情况】SCI在检&#xff0c;正刊 【征稿领域】自主传感器网络的高级接口电路及其应用 【参考周期】2-3个月左右 重要时间节点&#xff1a; 2022.12.15 | Accepted 2022.11.22 | 提交返修稿 20…

设计模式之建造者模式

builder desigin pattern 建造者模式的概念、建造者模式的结构、建造者模式的优缺点、建造者模式的使用场景、建造者模式的实现示例、建造者模式的源码分析 1、建造者模式的概念 将一个复杂对象的构建和表示分离&#xff0c;使得同样的创建过程可以得到不同的表示。其主要特点…

一种高复用的组件式安装包制作系统

目录 整体设计 流程描述 文件目录结构设计 产品资源的配置与更新 安装包制作流程 安装包执行流程 整体设计 如下展示了安装包系统的整体结构&#xff1a; 将集群布署可能用到的docker资源&#xff0c;按最小的单元进行整理&#xff0c;以压缩包的形式放于资源库&#xf…

欧姆龙电气元器件要点14讲

对于一个电气工程师来说&#xff0c;不仅仅只是要会PLC、伺服控制、变频器参数调优和总线控制、触摸屏这些大的元件的使用&#xff0c;还有很多种类齐全、功能各异的电气元器件&#xff0c;它们的作用、原理、维护方法、安全要点都要熟记于心&#xff0c;牢牢掌握。 第一章 电气…

艾美捷CpG ODN——ODN 1720 (TLRGRADE)说明书

艾美捷CpG ODN系列——ODN 1720 (TLRGRADE)&#xff1a;具有硫代磷酸酯骨架的GpC寡脱氧核苷酸。 艾美捷CpG ODN 丨ODN 1720 (TLRGRADE)化学性质&#xff1a; 序列&#xff1a;5-tccatgagcttcctgatgct-3&#xff08;小写字母表示硫代磷酸酯键&#xff09;。 MW&#xff1a;638…

Java中的运算符

算术运算符&#xff1a;&#xff0c; -&#xff0c;*&#xff0c;/&#xff0c;%&#xff0c;&#xff0c;--关系运算符&#xff1a;&#xff0c;!&#xff0c;<&#xff0c;>&#xff0c;<&#xff0c;>逻辑运算符&#xff1a;&&&#xff0c;||&#xff0…

基于STM32的温度控制系统

提示&#xff1a;记录毕设 文章目录前言一、任务书1.1设计(研究)目标:1.2设计(研究)内容:二、代码思路三、硬件四、联系我们五、设计六、框图代码等资料喜欢请点赞哦&#xff01;前言 基于STM32的温度控制系统&#xff0c;主控使用STM32F103ZET6&#xff0c;在正点原子的精英板…

ubuntu18.0 调节显卡GPU涡轮风扇转速

前言&#xff1a; 在炼丹的时候发现涡轮显卡的温度已经很高了85摄氏度&#xff0c;但是涡轮的风扇转速还不到65%&#xff0c;此时显卡计算频率明显已经下降了&#xff0c;所以需要手动调节风扇的转速&#xff0c;让噪音和计算速度处于均衡状态。 一、准备工作 》》安装显卡驱…

html:自定义网页右键菜单

<div id"menu"><divclass"menu-item"data-id"1">功能1</div><divclass"menu-item"data-id"2">功能2</div><divclass"menu-item"data-id"3">功能3</div><…

股票购买接口委托下单c++代码

炒股并非是运气可以驱使的&#xff0c;买股票不是赌博&#xff0c;是一种有风险的经济投资。在股市投资生涯中&#xff0c;掌握一门实战买卖技巧是我们必备的武器&#xff0c;这也是我们能长久在股市投资中得以生存的技法。 其实做股票投资是非常讲究买入和卖出的时机的。一只…

图像风格迁移-DSTN

样式传输的目的是从参考图像中再现具有样式的内容图像。现有的通用风格转换方法成功地以艺术或照片逼真的方式将任意风格传递给原始图像。然而&#xff0c;现有作品所定义的“任意风格”的范围由于其结构限制而在特定领域内受到限制。具体而言&#xff0c;根据预定义的目标域来…

用vscode配置C++3种编译器及多文件编译

末尾附上最终的模板 Vscode开发环境配置 C有很多种编译器&#xff0c;最重要的有三种 GNU的GCC(推荐)微软的MSVCClang/LLVM C的最新标准是C23,各个编译器对C各个标准的支持情况是不同的&#xff1a; C compiler support - cppreference.com 注意主要看C20的支持情况 用Vsco…

校园进销存网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 功能模块包括&#xff1a;员工模块、手机类型模块、供应商模块、采购模块、客户模块、销售模块、统计模块、库存模块 (1…

2022年的最后一个Win11 Dev预览版本

今日凌晨&#xff0c;微软向广大Win11 Dev用户推送了今年的最后一个版本更新&#xff0c;版本号为25267。根据官方的变化&#xff0c;引入了改变任务栏中可用搜索框样式的设置。此外&#xff0c;此版本还对任务栏、文件资源管理器、设置等进行了各种增强。 该公司还表示&#x…

nacos使用教程及原理简介

一、什么是 Nacos Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&#xff0c;帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos的关键特性包括&#xff1a; 服务发现和服务健康监测动态配置服务动态 DNS 服务服务及其元…

java排序算法

目录 一 冒泡排序 二 选择排序 三 插入排序 四 希尔排序 五 快速排序 5.1 单边循环快速排序 5.2 双边循环快速排序 六 二分查找 七 总结 一 冒泡排序 依次比较数组中相邻的两个元素&#xff0c;若 arr[i] > arr[i1]&#xff0c;则交换两个元素&#xff0c;两两都比…

RabbitMQ原理剖析

常见的消息队列很多&#xff0c;主要包括 RabbitMQ、Kafka、RocketMQ 和 ActiveMQ&#xff0c;本篇文章只讲 RabbitMQ&#xff0c;先讲原理&#xff0c;后搞实战。 直接上思维导图&#xff1a; 1. 消息队列 1.1 消息队列模式 消息队列目前主要 2 种模式&#xff0c;分别为“…

【AI with ML】第 6 章 :使用嵌入使情绪可编程

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

Web安全研究(二)

TChecker: Precise Static Inter-Procedural Analysis for Detecting Taint-Style Vulnerabilities in PHP Applications 香港中文大学&#xff0c;CCS2022 Abstract 由于php语言的高度复杂性&#xff0c;现有的污点分析解决方案由于其不全面的程序间分析和各种实现问题&#…

DIV简单个人静态HTML网页设计作品 WEB静态个人介绍网页模板代码 DW个人网站制作成品 期末网页制作与实现

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…