当前位置: 首页 > 后端技术 > Java

奇怪的登录要求

时间:2023-04-01 19:00:12 Java

@[toc]奇怪的登录要求。这是微信群里朋友的提问。我觉得很有意思:虽然这不是一个典型的需求,但是解决这个问题有助于加深大家对SpringSecurity的理解。所以,宋哥打算写一篇文章,跟大家聊一聊这个话题。1.问题重现。可能有些朋友不太理解这个问题,我先稍微解释一下。当我们登录失败时,可能是用户名或密码错误,但出于安全考虑,服务器一般不会明确提示用户名或密码是否错误,而只会给出一个Obfuscatedusernamesorpasswordsinpasswords。但是,对于很多新手程序员来说,他们可能并不了解这样的“潜规则”,可能会给用户一个明确的提示,明确提示用户名或密码是否错误。为了避免这种情况,SpringSecurity通过封装隐藏了用户名不存在的异常,让开发者在开发时只能获取到BadCredentialsException。该异常表示用户名不存在,用户密码输入错误。SpringSecurity这样做是为了确保我们的系统足够安全。但是,由于种种原因,有时候我们希望能够得到用户不存在的异常和密码输入错误的异常。这个时候我们需要对SpringSecurity做一些简单的定制。2.源码分析首先我们要找到问题的原因和地方。在SpringSecurity中,负责用户验证的类有很多,这里就不一一列举了(有兴趣的朋友可以参考本书《深入浅出Spring Security》),直接说说我们涉及到的关键类AbstractUserDetailsAuthenticationProvider。。这个类将负责验证用户名和密码。具体来说,在authenticate方法中,这个方法很长。我这里只列出与本文相关的代码:@OverridepublicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{try{user=retrieveUser(username,(UsernamePasswordAuthenticationToken)authentication);}catch(UsernameNotFoundExceptionex){if(!this.hideUserNotFoundExceptions){throwex;}thrownewBadCredentialsException(this.messages.getMessage("AbstractUserBadentialsAuthenticationProvider."));}}}retrieveUser方法是根据用户输入的用户名来查找用户。如果未找到,将抛出UsernameNotFoundException。捕获到异常后,会先判断是否隐藏异常。如果不是,则原样抛出原来的异常,如果需要隐藏,则抛出新的BadCredentialsException异常。BadCredentialsException异常从字面上看就是密码输入错误的异常。所以问题的核心就变成了hideUserNotFoundExceptions这个变量。这是一个Boolean类型的属性,默认为true,AbstractUserDetailsAuthenticationProvider也提供了这个属性的set方法:publicvoidsetHideUserNotFoundExceptions(booleanhideUserNotFoundExceptions){this.hideUserNotFoundExceptions=hideUserNotFoundExceptions;}看来修改hideUserNotFoundExceptions属性并不难!只要找到AbstractUserDetailsAuthenticationProvider的实例,调用对应的set方法修改即可。现在问题的核心变成了从哪里获取AbstractUserDetailsAuthenticationProvider的实例?看名字就知道,AbstractUserDetailsAuthenticationProvider是一个抽象类,那么它的实例其实就是它的子类的实例,子类是谁?当然,负责用户密码验证的是DaoAuthenticationProvider。先记住这个知识点,后面会用到。3、登录过程为了理解这个问题,我们还需要了解一个SpringSecurity的通用认证过程,这个过程也很重要。首先大家知道SpringSecurity的认证工作主要是由AuthenticationManager完成的,而AuthenticationManager是一个接口,它的实现类是ProviderManager。总之,ProviderManager#authenticate方法在SpringSecurity中是专门负责验证的。但是,验证工作并不是由ProviderManager直接完成的。ProviderManager管理着若干个AuthenticationProvider,ProviderManager会调用它管理的AuthenticationProvider来完成验证工作,如下图:另一方面,ProviderManager分为全局和局部。当我们登录的时候,本地的ProviderManager首先出来验证用户名和密码。如果验证成功,则用户登录成功。如果验证失败,将调用本地ProviderManager的parent,也就是全局的ProviderManager。完成验证工作,如果全局ProviderManager验证成功,则表示用户登录成功,如果全局ProviderManager验证失败,则表示用户登录失败,如下图:OK,同上知识储备,下面我们来分析一下我们要怎么做才能抛出UsernameNotFoundException。4、思路分析首先,我们的用户验证工作是在本地的ProviderManager中进行的,它管理着若干个AuthenticationProvider,而这若干个AuthenticationProvider中可能包含了我们需要的DaoAuthenticationProvider。那么我们这里是否需要调用DaoAuthenticationProvider的setHideUserNotFoundExceptions方法来完成属性修改呢?宋弟兄的建议是多余的!为什么?因为当用户登录的时候,先到本地的ProviderManager去验证,如果验证成功,当然最好;如果验证失败,不会立即抛出异常,而是抛给全局的ProviderManager继续验证,所以即使我们在本地的ProviderManager中抛出UsernameNotFoundException,也是没有用的,因为到底能不能抛出这个异常依赖全局ProviderManager(如果全局ProviderManager管理的DaoAuthenticationProvider不做任何特殊处理,那么本地ProviderManager中抛出的UsernameNotFoundException最终会被隐藏)。那么,我们要做的就是获取全局的ProviderManager,然后获取全局ProviderManager管理的DaoAuthenticationProvider,然后调用它的setHideUserNotFoundExceptions方法修改对应的属性值。明白了原理,代码就简单了。5、具体做法全局ProviderManager的修改在WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder)类中。这里配置的AuthenticationManagerBuilder最终是用来生成全局ProviderManager的,所以我们的配置如下:daoAuthenticationProvider.setHideUserNotFoundExceptions(false);InMemoryUserDetailsManageruserDetailsS??ervice=newInMemoryUserDetailsManager();userDetailsS??ervice.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());daoAuthenticationProvider.setUserDetailsS??ervice(userDetailsS??ervice);auth.authenticationProvider(daoAuthenticationProvider);}@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.authorizeRequests().anyRequest().authenticated().and().formLogin().failureHandler((请求,响应,异常)->System.out.println(异常)).permitAll();}}codehere很简单:创建一个DaoAuthenticationProvider对象,调用DaoAuthenticationProvider对象的setHideUserNotFoundExceptions方法,修改相应的属性值。为DaoAuthenticationProvider配置用户数据源。将DaoAuthenticationProvider设置为auth对象,auth将用于生成全局ProviderManager。在另一个配置方法中,我们只是配置登录回调。登录失败时,打印异常信息看看。好的。接下来启动项目进行测试。输入错误的用户名,可以看到IDEA控制台会打印出如下信息:可以看到抛出了UsernameNotFoundException异常。6.总结,今天给大家分享下SpringSecurity中如何抛出UsernameNotFoundException。虽然这只是一个小小的需求,但是可以加深大家对SpringSecurity的理解。有兴趣的朋友可以仔细想想。题外话:这个需求还有一个简单的实现方式,就是为用户自定义一个不存在的异常。当在UserDetailsS??ervice中找不到用户时,会抛出自定义异常,自定义异常不会被隐藏,这个比较简单,我就不写代码了,有兴趣的朋友可以试试。