SpringBoot3+SpringSecurity6基于若依系统整合自定义登录流程
问题背景
在做项目时遇到了要对接统一认证的需求,但是由于框架的不兼容性(我们项目是springboot3,jdk17,springsecurity6.1.5)等因素,不得不使用抽离服务的方法,将统一认证服务抽出去形成一个中介系统,统一认证服务使用jdk8的版本,这样可以兼容统一认证服务器,认证流程图大致如下:
也就是说,我们的任务变成了这样:中介系统会传入一个工号和一个密钥,主系统需要解密密钥并且将根据工号直接登录。一开始我想直接在这个第三方登录方法中就解密密钥,然后复用正常的登录逻辑,正常的登录逻辑是通过工号和密码,框架才会颁发token,密码可以直接从数据库里查出来,所以直接一股脑塞进去:
但是我们发现框架底层的鉴权流程是:将传入的密码加密,然后查询数据库中存储的密码,而数据库存储的密码本身就是加密后的,相当于匹配这两个字符串是否一致即可(因为这个加密方法是无法逆向解密的)。
这也就是说明我们如果直接从数据库查询密码塞到token里是不行的,因为框架会将你传入的密码再加密一次,就是原来的值了。
在网上看到有说在密码前加入{noop}就可以使得框架不对密码进行加密,但是我尝试后发现没有效果…
没办法,这时候只能自己去实现一个登录流程了。
尝试
在网上查阅了相关文章,找到了SpringSecurity中正常使用账号密码登录走的流程吧:
根据这个认证过程,我们应该可以大致去效仿一下:
首先我们提供一个实现了AbstractAuthenticationProcessingFilter抽象类的过滤器,用来代替UsernamePasswordAuthenticationFilter逻辑,然后提供一个AuthenticationProvider实现类代替AbstractUserDetailsAuthenticationProvider或DaoAuthenticationProvider,最后再提供一个UserDetailsService实现类。
大致流程是这样。但是别忘了,我们这是springboot3,网上大部分文章都是springboot2的,我们很难找到参考…
因为我们这个项目本身是想整合cas登录的,所以后面很多鉴权逻辑类的起名都跟cas有关。
照猫画虎,根据框架的UsernamePasswordAuthenticationToken我们也自定义了CasAuthenticationToken,大致就是把字段改改,剩下就是完全照搬。
public class CasAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
//对应工号
private final Object principal;
//对应验证码
private Object credentials;
public CasAuthenticationToken(String empId, Object credentials){
super(null);
this.principal = empId;
this.credentials = credentials;
setAuthenticated(false);
}
public CasAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object credentials){
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
然后需要写一个Provider,参考SpringSecurity的AbstractUserDetailsAuthenticationProvider,我们也可以照猫画虎一下。注意一下:Provider里面的authenticate方法是鉴权核心代码,比如我们将如何判断密钥合法、根据工号查询用户信息及是否有这个用户等,这里的逻辑需要你们去个性化实现。
@Component
public class CasAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
public CasAuthenticationProvider(UserDetailsService userDetailService) {
this.userDetailsService = userDetailService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CasAuthenticationToken token = (CasAuthenticationToken) authentication;
String empId = (String)token.getPrincipal();
//首先,验证验证码是否正确
String code = (String)token.getCredentials();
//这里应该写一下如何解密的逻辑
-----------------
//然后,查询对应用户
UserDetails user = userDetailsService.loadUserByUsername(empId);
if (Objects.isNull(user)) {
throw new InternalAuthenticationServiceException("根据工号:" + empId + ",无法获取对应的用户信息!");
}
CasAuthenticationToken authenticationResult = new CasAuthenticationToken(user, user.getAuthorities(), token.getCredentials());
authenticationResult.setDetails(token.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return CasAuthenticationToken.class.isAssignableFrom(authentication);
}
}
值得注意的是,返回新的CasAuthenticationToken中第一个参数user信息要是一个对象,这样便于后面解析成LoginUser对象。
看网上大部分文章还需要配置专门的第三方过滤器、专门的登录成功类和登录失败类,一开始我也根据这个创了好多,最后打断点才发现根本都没进到这些代码里来。
大部分文章还要求在SpringSecurity配置类配置下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/error","/login/**","/login/goLogin","/login/doLogin","/login/code","/login/authorization_code").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login/goLogin")
.loginProcessingUrl("/login/doLogin")
.failureUrl("/login/error")
.permitAll()
.successHandler(new QriverAuthenticationSuccessHandler("/index/toIndex"));
//这里我们省略了一些配置 ……
//应用前面定义的配置
http.apply(thirdAuthenticationSecurityConfig);
}
但是我的项目是springboot3+springsecurity6,不能这么写,已经从configure方法变成了filterChain方法,像一个链子一样调用不同的过滤器来实现统一过滤。
看到过滤器链里有这个UsernamePasswordAuthenticationFilter,我也不假思索的觉得应该弄一个CasAuthenticationFilter并且插入进去,但是不知为什么弄完后,填入过滤链项目就无法启动了,报错:The Filter class org.**.ThirdAuthenticationFilter does not have a registered order…网上搜了一下说加@Order的仍然还是一样报错。哎,那还是注掉吧
嗯,那就算了?要不就先这样试试能不能成功
满怀期待的启动项目测试,结果报错:No AuthenticationProvider found for org.***.CasAuthenticationToken
根据资料显示,是由于我定义了一个新的AuthenticationToken,在
authenticationManager.authenticate(authenticationToken);
时候发现authenticationManager的parent属性是null,他不知道如何对我们这个自定义的token进行鉴权。哎不对啊,我前面明明写了个Provider为什么还不行!
看一些可能是SpringBoot2的文章说要弄filter,但是我弄了但是根本不走代码,放到SecurityConfig里又报错无法启动项目。应该是不同版本写法已经不一样了。
其实首要问题就是AuthenticationManager的parent属性是null,应该从这里入手。但是我走错方向了,不停的在SecurityConfig配置上、过滤器上改…
后面我想到了这个AuthenticationManager,偶然在SecurityConfig看到了:
还记得账号密码怎么登录的吗,他用的DaoAuthenticationProvider!这里他给ProviderManager注入了!
点进去看了一下,他可以注入多个Provider!那我们直接把CasAuthenticationProvider注入!
那我们就可以照猫画虎!注入!
最后测试一下,能成功返回token了!
参考文章:
一文理清SpringSecurity中基于用于名密码的登录认证流程-腾讯云开发者社区-腾讯云 (tencent.com)
SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(二)-CSDN博客
SpringSecurity系列 之 集成第三方登录(包括默认的用户名密码、短信验证码和github三种登录方式)_springsecurity第三方登录-CSDN博客
Spring Security authenticationManager()返回null,必须定义authenticationManagerBean的原因分析-CSDN博客