文章会比较长,这个问题困扰了我接近一周的时间,前前后后搜过无数的资料文档,几乎翻遍了Security的源码部分,这四五天的时间可以说Security那迷宫一样的初始化机制就是我挥之不去的梦魇,所以我想从问题的发现开始,记录下我排查问题的每一步,希望能帮到以后的人。

为什么这么感慨呢,我相信,当你认认真真看完整篇文章之后,会发现一个很惊人的事实:网上关于SpringSecurity的使用方法99%都是错误的,我尝试过的几个高分开源项目(例如ruoyi)关于SpringSecurity的使用也完全是错误的。 为什么没有被发现呢,因为只是用来做简单系统单登录方式的话根本不会用到这一块功能,如果要扩展多用户体系多登陆模式,网上的方法虽然行得通,但却极大违背了SpringSecurity的设计初衷,是根本思想上的错误。

注意本文使用的是不继承WebSecurityConfigurerAdapter的方式,因为此方法在5.7版本开始就被废弃了,虽然实现方式不同但是运行机制是一样的。

起因

介绍一下版本情况

SpringBoot 2.6.9
SpringSecurity 5.6.6

最近项目上有个需求,需要支持用户名密码模式、手机验证码模式和OAuth授权码模式三种登录,项目本身采用SpringSecurity作为安全框架,之前的模式和网上众多教程别无二致,无非是/login接口作为白名单放行,业务侧颁发jwt token。新需求一来就有点不够看了,要针对多种方式做单独的登录处理,都挤在一个业务处理类中实在是有点太邋遢,于是自然而言就想到了利用SpringSecurityAuthenticationProvider来帮我们实现这一功能。


运行的大体流程是这样的:

  1. 构建一个特定的Token类,例如这里的PasswordAuthenticationToken,这个类需要继承AbstractAuthenticationToken,在你需要做认证的地方把他new出来;
  2. 把上面new出来的对象传递给authenticationManager.authenticate()方法,他会根据AuthenticationManager中维护的AuthenticationProvider列表逐个调用其supports()方法,若提供的token类与当前AuthenticationProvider所匹配则交由该provider执行;
  3. authenticate()方法若认证成功,则返回一个完全构建好的Authentication对象告知Security认证已完成,不需要再往下走认证器链了,若认证不成功返回null或抛出相应的异常(注意异常父类必须是AuthenticationException),Security会继续向下寻找Provider直至走完整个过滤器链,返回认证失败。

看起来很简单是不是,三下五除二改造完成,先看一下此时的项目目录结构

为方便理解这里以带图形验证码的用户名密码登录为例贴一下相关代码(已隐去import部分),核心逻辑都加了注释,注意阅读。

/*** 基于用户名(手机号)、密码、验证码登录的认证实体*/
public class PasswordAuthenticationToken extends AbstractAuthenticationToken {private final String loginId;private final String captchaId;private final String captchaValue;private final LoginUserPojo principal;private final String credentials;/*** 登录验证** @param loginId      用户名或手机号* @param credentials  MD5+SM3密码* @param captchaId    图形验证码id* @param captchaValue 输入的图形验证码值*/public PasswordAuthenticationToken(String loginId, String credentials, String captchaId, String captchaValue) {super(null);this.loginId = loginId;this.credentials = credentials;this.captchaId = captchaId;this.captchaValue = captchaValue;this.principal = null;this.setAuthenticated(false);}/*** 授权信息** @param principal   LoginUserPojo* @param credentials token* @param authorities 角色清单*/public PasswordAuthenticationToken(LoginUserPojo principal, String credentials, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;this.loginId = null;this.captchaId = null;this.captchaValue = null;this.setAuthenticated(true);}public String getLoginId() {return loginId;}public String getCaptchaId() {return captchaId;}public String getCaptchaValue() {return captchaValue;}@Overridepublic LoginUserPojo getPrincipal() {return principal;}@Overridepublic String getCredentials() {return credentials;}
}
/*** 基于用户名(手机号)、密码、验证码的认证处理器*/
@Component
public class PasswordAuthenticationProvider implements AuthenticationProvider {private static final String IMG_CAPTCHA_REDIS_PREFIX = "SK:CAPTCHA:IMG:";@Autowiredprivate UserDetailServiceImpl userDetailService;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate RedisCacheUtil redisCacheUtil;/*** 验证主逻辑*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {PasswordAuthenticationToken authenticationToken = (PasswordAuthenticationToken) authentication;// 验证码校验if (!checkImgCaptcha(authenticationToken.getCaptchaId(), authenticationToken.getCaptchaValue())) {throw new BadCaptchaException("验证码有误或已过期,请重新输入");}// 密码校验LoginUserPojo userDetails = (LoginUserPojo) userDetailService.loadUserByUsername(authenticationToken.getLoginId());if (!passwordEncoder.matches(authenticationToken.getCredentials(), userDetails.getPassword())) {throw new BadCredentialsException("用户名或密码错误,请重新输入");}// 用户状态校验if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() || !userDetails.isAccountNonExpired()) {throw new LockedException("用户已禁用,请联系管理员启用");}return new PasswordAuthenticationToken(userDetails, authenticationToken.getCredentials(), userDetails.getAuthorities());}/*** 当类型为PasswordAuthenticationToken的认证实体进入时才走此Provider*/@Overridepublic boolean supports(Class<?> authentication) {return PasswordAuthenticationToken.class.isAssignableFrom(authentication);}/*** 校验验证码正确与否,验证完成后删除当前码值** @param id    验证码对应的id* @param value 用户输入的验证码结果* @return true or false*/private boolean checkImgCaptcha(String id, String value) {if (StringUtils.isBlank(id) || StringUtils.isBlank(value)) {return false;}CaptchaCodePojo captchaCode = redisCacheUtil.getObject(IMG_CAPTCHA_REDIS_PREFIX + id);redisCacheUtil.deleteObject(IMG_CAPTCHA_REDIS_PREFIX + id);return !Objects.isNull(captchaCode) && value.equals(captchaCode.getResult());}
}

以下是登录Service

/*** 登录*/
@Service
public class LoginServiceImpl implements ILoginService {private static final String IMG_CAPTCHA_REDIS_PREFIX = "SK:CAPTCHA:IMG:";@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCacheUtil redisCacheUtil;@Autowiredprivate SysUserMapper userMapper;@Overridepublic String login(Map<String, String> params) {// 实际业务执行在PasswordAuthenticationProvider中Authentication authentication = new PasswordAuthenticationToken(params.get("loginKey"), params.get("password"), params.get("id"), params.get("value"));Authentication authenticate = authenticationManager.authenticate(authentication);LoginUserPojo loginUserPojo = (LoginUserPojo) authenticate.getPrincipal();// 更新登录时间updateLoginTime(loginUserPojo.getUserId());return buildToken(loginUserPojo);}@Overridepublic String oAuthLogin(String code) {// 实际业务执行在OAuthAuthenticationProvider中Authentication authentication = new OAuthAuthenticationToken(code);Authentication authenticate = authenticationManager.authenticate(authentication);LoginUserPojo loginUserPojo = (LoginUserPojo) authenticate.getPrincipal();// 更新登录时间updateLoginTime(loginUserPojo.getUserId());return buildToken(loginUserPojo);}/*** 根据用户信息构造token并写入redis** @param loginUserPojo LoginUserPojo* @return token*/private String buildToken(LoginUserPojo loginUserPojo) {JSONObject user = new JSONObject();user.put("userId", loginUserPojo.getUserId());user.put("userName", loginUserPojo.getUserName());user.put("roleCode", loginUserPojo.getAuthorities().stream().map(UserGrantedAuthority::getRoleCode).collect(Collectors.joining(",")));// 生成tokenString token = JwtTokenUtil.createJwtToken(user);redisCacheUtil.setObject(TokenConstant.TOKEN_REDIS_PREFIX + token, loginUserPojo, TokenConstant.TOKEN_EXPIRE_TIME, TokenConstant.TOKEN_EXPIRE_TIME_UNIT);return token;}@Overridepublic Map<String, String> generateImageCaptcha() throws IOException {CaptchaCodePojo captchaCode = new MathCaptchaGenerator(1).generate();SimpleCaptchaRender captchaRender = new SimpleCaptchaRender(90, 30, captchaCode.getCode(), 2, new Font(Font.SANS_SERIF, Font.BOLD, (int) (30 * 0.75)));Map<String, String> result = new HashMap<>(2);result.put("id", UUID.randomUUID().toString());result.put("pic", captchaRender.getImageBase64());// 将生成的验证码及结果存入redis,有效期两分钟redisCacheUtil.setObject(IMG_CAPTCHA_REDIS_PREFIX + result.get("id"), captchaCode, 2, TimeUnit.MINUTES);return result;}/*** 更新登陆时间** @param userId 用户id*/private void updateLoginTime(String userId) {SysUserEntity userEntity = new SysUserEntity();userEntity.setUserId(userId);userEntity.setLastLogin(DateTimeUtil.getCurrentDate("yyyy-MM-dd HH:mm:ss"));userMapper.updateByUserId(userEntity);}
}

补充一个用来构造相关Bean的类

/*** SpringSecurity相关Bean构造*/
@Component
public class SpringSecurityBeans {@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

以及核心的SpringSecurity Config

/*** SpringSecurity配置类*/
@EnableWebSecurity
@PropertySource("classpath:authfilter.properties")
public class SpringSecurityConfig {@Value("${exclude_urls}")private String excludeUrls;/*** token认证过滤器*/@Autowiredprivate AuthenticationTokenFilter authenticationTokenFilter;/*** 认证失败处理器*/@Autowiredprivate AuthenticationFailHandler authenticationFailHandler;/*** 注销处理器*/@Autowiredprivate AuthenticationLogoutHandler logoutHandler;/*** 密码认证处理器*/@Autowiredprivate PasswordAuthenticationProvider passwordAuthenticationProvider;/*** OAuth认证处理器*/@Autowiredprivate OAuthAuthenticationProvider oAuthAuthenticationProvider;@Beanpublic SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {return httpSecurity.csrf().disable().formLogin().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeHttpRequests().antMatchers(StringUtils.split(excludeUrls, ",")).permitAll().anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(authenticationFailHandler).and().logout().logoutUrl("/logout").logoutSuccessHandler(logoutHandler).and()// 将自定义的Provider添加到Security中.authenticationProvider(passwordAuthenticationProvider).authenticationProvider(oAuthAuthenticationProvider).addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class).build();}
}

看起来很简单,代码也很简单,几个小时就改好了,编译启动都正常,出于迷之自信也没有做测试直接丢到开发环境,启动运行,本以为今天就要愉快的结束了,然后结结实实的一盆冷水拍到脸上。

我明明已经把Provider注入到SecurityFilterChain链中了,为什么他会告诉我找不到?此时是8月23日七点半,此时我的噩梦算是正式开始了。

SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(一)相关推荐

  1. Oracle EBS FA创建资产时提示错误不能获得摊派到日期信息解决方案

    Oracle EBS FA 创建资产时提示错误 不能获得摊派到日期信息解决方案 英文错误: Error: Unable to get prorate date information Cause: Y ...

  2. 将折旧表分配至公司代码时提示公司代码分录不完全解决方案

    [将折旧表分配至公司代码时提示公司代码分录不完全解决方案] 给公司代码维护折旧表,保存时候提示:"XXXX的公司代码分录不完全-参见长文本". 这个问题的原因其实是非应税税码未分配 ...

  3. 此程序将从您的计算机删除adobe,电脑中安装Adobe软件时提示该程序已安装的最佳解决方案...

    很多用户都喜欢在电脑中安装这款Adobe软件来使用,不过有时候会碰到一些故障,就有用户在电脑中安装Adobe软件的时候,却提示该程序已安装或者无法安装的现象,这是怎么回事呢,经过分析一般是之前安装过没 ...

  4. centos挂载硬盘时提示mount:unknown filesystem type 'LVM2_member'的解决方案

    我写博文主要是为了记录自己学习中遇到的问题 centos挂载硬盘时提示mount:unknown filesystem type 'LVM2_member'的解决方案 我的是因为重新挂载了LV后面再挂 ...

  5. scp时提示【Read-only file system】的解决方案

    scp时提示Read-only file system这是由于没有权限读写目标目录,解决方案如下: # 在远程终端以root权限执行如下命令: mount -o remount rw / 参考链接: ...

  6. 笔记本重置找不到恢复环境_重置Windows10系统时提示“找不到恢复环境”的解决方案...

    如果我们在使用win10系统过程中,遇到了一些无法解决的问题的话,就可以开启系统自带的重置功能来处理.不过,部分用户在使用Win10重置功能时,却碰到了"找不到恢复环境"的情况,这 ...

  7. 十八、启动jmeter时提示findstr不是内部命令的解决方案

    1.jmeter环境配置后,点击jmeter.bat,提示以下错误 2.先cmd检查,输入命令 findstr /? 说明C:\Windows\System32\findstr.exe未生效,因安装极 ...

  8. WordPress更新时提示无法连接到FTP服务器的解决方案

    这几天在搭建主站的时候,更新wordpress时无法连接到FTP原因服务器 解决方法如下: 在WordPress目录下找到wp-config.php文件并编辑,在最后一行加上: define('FS_ ...

  9. 安装OpenCV时提示缺少boostdesc_bgm.i文件的问题解决方案

    安装OpenCV时,会遇到下面的错误 /home/zhang/slam/opencv-3.4.5/opencv_contrib/modules/xfeatures2d/src/boostdesc.cp ...

  10. 3. SpringSecurity 自定义手机号登录

    距离上一次更新,不知不觉已经过去了半个月了,人真的是不能放松,一放松就肆意妄为了.希望这个月内可以把 SpringSecurity 系列更新完毕吧,加油!. OK,言归正传上一章我们利用 Spring ...

最新文章

  1. 自监督学习简介以及在三大领域中现状
  2. cdrx4自动排版步骤_现在的大学生,都不会论文排版了
  3. leetcode 698. Partition to K Equal Sum Subsets | 698. 划分为k个相等的子集(回溯法)
  4. 如何删除Android系统中的内置应用
  5. black.lst 丢失或被破坏,怎么解决
  6. redis hash field过期时间_Redis系列-Redis数据类型
  7. 开源版权 项目 字体
  8. MathType2022最新版详细教程及怎么安装到word里?
  9. 基于三菱PLC的全自动洗衣机控制系统设计
  10. 信号与系统 第二版pdf 作者:奥本海姆 翻译:刘树棠
  11. 电脑自动跳出计算机管理员登陆界面,解决运行wegame总是弹出用户账户控制界面的方法...
  12. linux查看历史命令history
  13. blink usb无线网卡驱动 linux,blink随身wifi驱动
  14. 东拉西扯01世界的沧海桑田
  15. 11款学习编程好玩的浏览器游戏
  16. 桌面便利贴软件下载 电脑桌面便签小工具软件下载
  17. 【翻译】 Intel(R) 800 Series序列网卡 ice 驱动安装
  18. ubuntu设置时间为utc标准时间
  19. Kafka触发Rebalance的场景分析
  20. 如何学C语言,新手必看!

热门文章

  1. Python爬虫系列之某了么h5签名sign算法
  2. 数据分析报告怎么写(一)
  3. maven的jar包引入成功却仍然爆红
  4. Java 获取月初时间
  5. 基于Android的天气预报APP设计与实现
  6. 肾囊肿有什么症状呢?
  7. SCI检索与EI检索
  8. SQL读取Excel数据
  9. 发布APP到腾讯应用宝
  10. 移除bable打包的use strict模式(vue2)