前言

在Web应用开发中,安全一直是非常重要的一个方面。在庞大的spring生态圈中,权限校验框架也是非常完善的。其中,spring security是非常好用的。今天记录一下在开发中遇到的一个spring-security相关的问题。

问题描述

使用spring security进行授权登录的时候,发现登录接口无法正常捕捉UsernameNotFoundException异常,捕捉到的一直是BadCredentialsException异常。我们的预期是:

  • UsernameNotFoundException -> 用户名错误
  • BadCredentialsException -> 密码错误

贴几个比较重要的代码:

1. 登录业务逻辑

@Service
public class AuthServiceImpl implements AuthService { @Autowired
private UserDetailsService userDetailsService; @Autowired
private AuthenticationManager authenticationManager; @Autowired
private JwtTokenUtil jwtTokenUtil; @Override
public JwtAuthenticationResponse login(String username, String password) {
//构造spring security需要的UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
//调用authenticationManager.authenticate(upToken)方法验证
//该方法将会执行UserDetailsService的loadUserByUsername验证用户名
//以及PasswordEncoder的matches方法验证密码
val authenticate = authenticationManager.authenticate(upToken);
JwtUser userDetails = (JwtUser) authenticate.getPrincipal();
val token = jwtTokenUtil.generateToken(userDetails);
return new JwtAuthenticationResponse(token, userDetails.getId(), userDetails.getUsername());
}
}

2. spring security 的UserDetailsService 实现类

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AbstractUser abstractUser = userRepository.findByUsername(username);
//如果通过用户名找不到用户,则抛出UsernameNotFoundException异常
if (abstractUser == null) {
throw new UsernameNotFoundException(String.format("No abstractUser found with username '%s'.", username));
} else {
return JwtUserFactory.create(abstractUser);
}
}
}

3. 登录接口

try {
final JwtAuthenticationResponse jsonResponse = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());
//存入redis
redisService.setToken(jsonResponse.getToken());
return ok(jsonResponse);
} catch (BadCredentialsException e) {
//捕捉到BadCredentialsException,密码不正确
return forbidden(LOGIN_PASSWORD_ERROR, request);
} catch (UsernameNotFoundException e) {
//捕捉到UsernameNotFoundException,用户名不正确
return forbidden(LOGIN_USERNAME_ERROR, request);
}

在上述代码中,如果用户名错误,应该执行

catch (UsernameNotFoundException e) {
return forbidden(LOGIN_USERNAME_ERROR, request);
}

如果密码错误,应该执行

catch (BadCredentialsException e) {
return forbidden(LOGIN_PASSWORD_ERROR, request);
}

实际上,不管是抛出什么错,最后抓到的都是BadCredentialsException

问题定位

debug大法

断点

跟踪

经过步进法跟踪代码,发现问题所在,位于

AbstractUserDetailsAuthenticationProvider
public Authentication authenticate(Authentication authentication)

结论

  1. loadUserByUsername方法确实抛出了UsernameNotFoundException
  2. 走到AbstractUserDetailsAuthenticationProvider的authenticate方法的时候,如果hideUserNotFoundExceptions = true,直接就覆盖了UsernameNotFoundException异常并抛出BadCredentialsException异常,这也就解释了,为什么总是捕捉到BadCredentialsException异常

问题解决

既然已经找到了是因为hideUserNotFoundExceptions = true导致的问题,那把hideUserNotFoundExceptions = false不就完事了吗?

方案1

参考stackoverflow大神回答

修改WebSecurityConfig配置,添加AuthenticationProvider Bean

@Bean
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}

配置AuthenticationProvider Bean

@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.authenticationProvider(daoAuthenticationProvider());
}

方案2

由于以前项目中也是一样的技术栈,而且代码也差不多,登录这段逻辑可以说是完全相同,不过之前就一直都没有这个问题。反复查看之后发现,在login的代码有些不同

val authenticate = authenticationManager.authenticate(upToken);

前面还有一个

//执行UserDetailsService的loadUserByUsername验证用户名
userDetailsService.loadUserByUsername(authenticationRequest.getUsername());

该方法会直接抛出UsernameNotFoundException,而不走spring security的AbstractUserDetailsAuthenticationProvider,也就不存在被转换为BadCredentialsException了。

但是这个方案有个缺点,

如果验证用户名通过以后,再次调用

val authenticate = authenticationManager.authenticate(upToken);

还会再执行一遍

userDetailsService.loadUserByUsername(authenticationRequest.getUsername());

该操作是冗余的,产生了不必要的数据库查询工作。

推荐使用方案1

Spring-Security无法正常捕捉到UsernameNotFoundException异常的更多相关文章

  1. Spring Security 5.0.x 参考手册 【翻译自官方GIT-2018.06.12】

    源码请移步至:https://github.com/aquariuspj/spring-security/tree/translator/docs/manual/src/docs/asciidoc 版 ...

  2. SpringBoot + Spring Security 学习笔记(三)实现图片验证码认证

    整体实现逻辑 前端在登录页面时,自动从后台获取最新的验证码图片 服务器接收获取生成验证码请求,生成验证码和对应的图片,图片响应回前端,验证码保存一份到服务器的 session 中 前端用户登录时携带当 ...

  3. Spring boot 前后台分离项目 怎么处理spring security 抛出的异常

    最近在开发一个项目 前后台分离的 使用 spring boot + spring security + jwt 实现用户登录权限控制等操作.但是 在用户登录的时候,怎么处理spring  securi ...

  4. Spring Security(05)——异常信息本地化

    Spring Security支持将展现给终端用户看的异常信息本地化,这些信息包括认证失败.访问被拒绝等.而对于展现给开发者看的异常信息和日志信息(如配置错误)则是不能够进行本地化的,它们是以英文硬编 ...

  5. Spring Security中异常上抛机制及对于转型处理的一些感悟

    在使用Spring Security的过程中,我们会发现框架内部按照错误及问题出现的场景,划分出了许许多多的异常,但是在业务调用时一般都会向外抛一个统一的异常出来,为什么要这样做呢,以及对于抛出来的异 ...

  6. Spring 捕捉校验参数异常并统一处理

    使用 @Validated ,@Valid ,@NotBlank 之类的,请自行百度,本文着重与捕捉校验失败信息并封装返回出去 参考: https://mp.weixin.qq.com/s/EaZxY ...

  7. 从源码看Spring Security之采坑笔记(Spring Boot篇)

    一:唠嗑 鼓捣了两天的Spring Security,踩了不少坑.如果你在学Spring Security,恰好又是使用的Spring Boot,那么给我点个赞吧!这篇博客将会让你了解Spring S ...

  8. spring security入门demo

    一.前言 因项目需要引入spring security权限框架,而之前也没接触过这个一门,于是就花了点时间弄了个小demo出来,说实话,刚开始接触这个确实有点懵,看网上资料写的权限大都是静态,即就是在 ...

  9. Spring Security 自定义登录认证(二)

    一.前言 本篇文章将讲述Spring Security自定义登录认证校验用户名.密码,自定义密码加密方式,以及在前后端分离的情况下认证失败或成功处理返回json格式数据 温馨小提示:Spring Se ...

随机推荐

  1. MCMC&Gibbs sampling

    Note of Markov Chain Monte Carlo and Gibbs Sampling :  http://pan.baidu.com/s/1jHpWY1o 序:A major lim ...

  2. Web 开发工具类(3): JsonUtils

    JsonUtils 整合了一些对Json的相关操作: package com.evan.common.utils; import java.util.List; import com.fasterxm ...

  3. .net core 认证与授权(三)

    前言 在写三上是在一的基础上写的,所以有没有看过二是没得关系的,在一中介绍了认证与授权,但是没有去介绍拿到证书后怎样去验证授权. 概念性东西:在这套机制中,把这个权限认证呢,称作为policy.这个p ...

  4. <背包>solution-CF118D_Caesar's Legions

    Caesar's Legions Gaius Julius Caesar, a famous general, loved to line up his soldiers. Overall the a ...

  5. StringBuffer StringBuilder String 区别

    String       字符串常量   不可变  使用字符串拼接时是不同的2个空间 StringBuffer  字符串变量   可变   线程安全  字符串拼接直接在字符串后追加 StringBui ...

  6. ATL的GUI程序设计(3)

    第三章 ATL的窗口类 CWindowImpl.CWindow.CWinTraits,ATL窗口类的奥秘尽在此三者之中.在本章里,李马将为你详细解说它们的使用方法.另外,本章的内容也可以算是本书的核心 ...

  7. Kafka系列2:深入理解Kafka消费者

    Kafka系列2:深入理解Kafka消费者 上篇聊了Kafka概况,包含了Kafka的基本概念.设计原理,以及设计核心.本篇单独聊聊Kafka的消费者,包括如下内容: 生产者是如何生产消息 如何创建生 ...

  8. 开发中常见的common.js--1

    common.js 1.基于代码重用的目的,方便在页面中调用: 2.跟普通的js包含文件并没有其他不一样,主要是一些js经常用到的函数,或者字符串.数组方法的简单扩展: 3.封装的兼容性的方法.[po ...

  9. awk命令入门

    什么是awk? AWK是一个强大的文本处理工具.可以使用awk读取输入文件.为数据排序.处理数据.对输入执行计算以及生成报表,还有无数其他的功能. 使用awk的基本格式 awk [options] ‘ ...

  10. Csp_J2019游记

    Day_-14 学校开始停课集训,还好还有上午~~ Day_-7 马上半期考试+\(Csp\),心态已炸,却还要坚持集训 Day_-1 \(Csp\)前夕,打算临时抱抱佛脚,死磕了一下图论(诸如最小生 ...