SpringSecurity+JWT实现登陆验证的思路(有一点点源码分析)

看了几个SpringSecurity+JWT的登陆demo,两个demo在一些细节实现上有一些不同,然后对于各个类和接口的关系比较模糊,就决定整理一下思路。

先简单的借用一下一位UP 三更草堂的图了解一下登陆大概的流程和用到的类,这个他讲的还比较清晰。

前后端分离项目登陆流程。
请添加图片描述

SpringSecurity工作流程(过滤器链)
请添加图片描述
登陆的后端实现:要实现登录验证主要需要实现上图过滤链的橙色方块部分的过滤器(UsernamePasswordAuthenticationFilter),这个过滤器的工作原理如下图。注意一开始受SpringSecurity过滤器链的思想影响会觉得下图的这几个类和接口也是过滤器链上的一个过滤器,实际上稍微他们之间的关系(调用、实现、继承)就知道这些都不是过滤器(废话,看名字就知道了,不过现在一定要非常明确这一点,否则在后续可能会感觉很混乱)。因此从过滤器链来看,下面的这些步骤在过滤器链中全部都属于橙色过滤器(UsernamePasswordAuthenticationFilter)这一步骤。
请添加图片描述
先不用细看这个图,或者先去看看三更草堂的几个视频大概了解一下流程就好。需要注意的是,后续还会设计很多很多乱七八糟的类的名字,这些类的名字都非常长,而且一定程度上有点相似,很容易搞混,因此给大家的建议是看这些名字的时候从后往前看。如上图,我们应该看到xxxxxFilter、xxxxxManager、 xxxxxProvider、 xxxxxService。当然还有一些关键词:Authentication、UserDetails等就ok了。
按照三更草堂的说法,明白流程之后知道哪个部分需要修改再修改就行了,他得出结论只用修改并实现上图的头部和尾部就可以了。实际上就是如此,这是最重要的,但是大家还记得这个图是什么吗?这个图只是一个登陆验证的流程,我们要整合SpringSecurity实际上还有一些步骤,而这些步骤实现的方法不同可能也会影响到登陆的实现方法不同。
上图虽然更美观清晰,但是小白无从下手,下面这个图是手画的实现登陆的一般思路。
请添加图片描述
我知道我画的丑字也丑,但是不急,看完下面的内容你应该很快就知道我画的是什么了。大概看一眼就OK。
那么整合SpringSecurity实现登陆的一般思路是怎么样的呢?
首先既然是前后端分离项目要实现登陆,我们就要写以一个接口对吧(虽然是废话,但是我觉得这很重要,是一切的开始),那我们就从Controller写起。内容就不赘述了,和其他接口一样,注入对应的Service,调用里面实现的login方法。那么问题来了,这个Service的要怎么编写呢?还是看最上面那张前后端登陆的原理图,看后端的部分,可以看总结出两个步骤:
1.验证
2.生成token并返回
这两个步骤分别对应这我们实现用的两个框架,验证我们使用SpringSecurity,负责生成解析token的是JWT。JWT不赘述了。如何使用SpringSecurity验证呢?SpringSecurity默认实现的就是使用上文提到的UsernamePasswordAuthenticationFilter(当整合了SpringSecurity之后就有一个默认的登陆页面,也即是UsernamePasswordAuthenticationFilter,SpringSecurity不仅仅提供了许多过接口,还提供了许多默认实现,我们需要的就是修改或者替换他们)。我们要怎么替换这个过滤器呢?我们只要接口调用自己实现的过滤器就好,那么问题来了,如何实现一个自己的过滤器呢?答案就是看看SpringSecurity默认的实现是怎么实现的我们照猫画虎即可。UsernamePasswordAuthenticationFilter实现登陆验证的流程就是上面那张红色的图,我们可以看到它实现验证使用的authenticate实际上是调用了xxxxManager的authenticate,而 xxxxManager的authenticate实际上是调用的xxxxxProvider的authenticate(实际上前面的xxxx补全了之后ProviderManager和DaoAuthenticationProvider,但是为了避免搞混还是用星号屏蔽一下方便看,而且其实图中画的ProviderManager调用了DaoAuthenticationProvider的authenticate是不严谨的,这个下文再说。)

说白了,在Service里我们只要调用xxxxxManager的authenticate方法就可以实现验证了,是不是很简单?调用了就写完了。想得美,虽然是调用就完事了,但是他要你提供一个参数Authentication。
在这里插入图片描述
这怎么办?这个是啥玩意?要怎么构造?不慌,于是不觉现在括号里打一个new就完事了,看看它有什么方法构造,我们有什么可以给它,万一有个无参构造就是免费的午餐了吗(谐音梗,好冷)。好,打一个new看看。
在这里插入图片描述
不得不说,有了IDE写代码简直不要太方便,一打个new他就自动列出了很多构造方法,而且我什么都没做它就帮我锁定了UsernamePasswordAuthenticationToken 。其实仔细想想也有道理啊,登录验证你当然要给她用户名密码去验证啊,不然拿什么验证呢哈哈哈。看着这个类名眼熟不?是不是好像在哪看过?对上面提到的过很多次的UsernamePasswordAuthenticationFilter长得很像有木有!事实上他们的关系八竿子打不着。都说了,看名字从后往前看,一个是Token,另一个是Filter,不要搞混了啊,不然实现的时候就糊里糊涂的。其实根据这个弹出来的构造方法我们可以知道这个Authentication只是一个接口,而xxxxxxToken 是他的实现类。使用IDEA的关系图功能就使得他们的关系一目了然了。
在这里插入图片描述
话说回来,这个东西要怎么构造呢?他一共有两个构造方法,一个两参数,一个三参数,因为我们看名字知道大概要给她username和password所以我们不难踩到他们对应这principal和credentials,英语好的老哥大概也能知道他们意思差不多,至于为什么不用三参数的构造的话一是因为我们没有authorities,也不知道怎么构造,事实上我们现在只有username和password。(实际上要是英文好一点的话可以看到官方告诉你了,xxxxxToken就用两参数的, 只有xxxxProvider和 xxxxxManger可以用三参数的)
在这里插入图片描述
好了,现在构造好了xxxxxToken之后他返回了一个Authentication对象,这个东西之前提过,是接口,使用了多态。先不管这个东西有什么用啊,反正到此为止我们已经完成了验证了。
那下一步是什么?构造JWT返回给前端呗。怎么构造?这个实际上拿什么来构造是自己决定的,一般就拿可以唯一表示用户的东西来构造,也就是username(也可以叫做userid)之类的东西。在这一步的时候大部分的实现都是通过Authentication的getPrincipal方法来获取用户,然后再拿id去构造的。事实上这个getPrincipal的值应该和之前构造xxxxxToken的时候一样的,理论上也可以直接用前端传过来的值去获取用户(甚至简单的项目中前端传回来的就是一个完整的用户)。不过还是按照大家写法来,这样看起来语义化一些。
至此其实整个登陆就已经完成了。但是还有一些地方没有实现。我们细看登陆流程的后面一部分。
在这里插入图片描述
UserDetailsService的默认实现类是什么?InMemoryUserDetailsManager!稍微翻译一下就是,这个是实现是在内存中管理用户的。回想我们刚真恶化SpringSecurity的时候就能有登录验证,这时候我们数据库里根本没有用户表,他们是怎么实现登陆的呢(默认用户是user,密码在控制台输出),答案就是他们没用数据库,而是就是在内存中生成的。这不符合我们实际项目的需求啊,所以要改!要换!怎么改怎么换?还是像上面一样,我们也照猫画虎的就可以了。在数据库建好表,生成model和mapper。需要注意的是SpringSecurity不是什么model都认的,它只认UserDetails,因此我们需要另外定义一个类来实现一个UserDetails,这个类用我们数据库生成的model来构造。实现的时候完成它所有重载就好。因为我做这个是时候是用于简单课设的登陆,所以很多方法是可以简化的。(也就是全部置为true哈哈哈)
在这里插入图片描述
这个类实现了之后我们就可以沟通了数据库和SpringSecurity了!但是我们还实现他们怎么认证,也就是还没有实现UserDetailsService,这个类只需要实现一个方法
在这里插入图片描述
而到这里的实现的时候我看的两份demo就出现了比较大的分歧了。一份就老老实实的实现了这个类。
在这里插入图片描述
而另一份则没有实现这个接口,而是在SecurityConfig中直接注册了一个UserDetailsService的Bean 到容器中。在这里插入图片描述
两者的效果应该是一样的,但是总是让人有点不放心。毕竟这个方法返回的是Details而声明的是UserDetailService,而且 xxxxxxProvider 调用的是loadUserByUsername方法,而我们也没有实现 。这到底是怎么回事?让人有了一点看源码的欲望。首先看这个UserDetailsService接口:
在这里插入图片描述
也是个函数式接口,定义了loadUserByUsername方法,我们去看看它的实现类DaoAuthenticationProvider。你是这么想的就错了!UserDetailService没有实现类,需要我们用户定义,否则默认用的是InMemoryUserDetailsManager。而DaoAuthenticationProvider是调用了UserDetailService定义的loadUserByUsername方法。还记得这个xxxxxxProvider是啥不?我们验证的时候会调用这个东西的authenticate方法,正好去看看。
跳到DaoAuthenticationProvider,我们照例查看关系图,上图便是xxxxxxProvider的继承实现关系。
在这里插入图片描述
进去直接查找一下哪里用到了UserDetailService 然后就看到下面几个方法:
在这里插入图片描述

在这里插入图片描述
确确实实是调用了loadUserByUsername,但是这个UserDetailService是怎么来的?是外部传入的,这就有点突破我认知了,按照之前的想法应该是注入得到的。还有一件很奇怪的事情就是这个类里面没有authenticate方法!按照三更草堂的图来说这个类应该是有个authenticate方法的!这是为什么?仔细看一下那个关系图,会发现在这个类并不是第一个实现类,其父类同样是一个实现类,而运行的时候用的是最原始的接口类型的多态功能,所以很可能我们要找的答案就在父类AbstractUserDetailsAuthenticationProvider离。
跳进去看找到了authenticate
在这里插入图片描述
里面许多类还挺眼熟的,之前都有提到过,参数需要一个Authentication对象,我们传入的是UsernamePasswordAuthenticationToken ,它在这里还会判断一下它的实例…其中这个retrieveUser是我们在DaoAuthenticationProvider找到UserDetailService的那个方法,但是这里也没有提出UserDetailService是怎么设置的。
在这里插入图片描述
在这里插入图片描述
找到一个包装类看到他是怎么调用我们的实现的,但是对于那种方法为什么可以还是没有找到合理的解释。然而由于所学知识不多,眼看课设也快截止了,所以就不深究下去了,等以后知识储备够了或许就知道了。
不管怎么样,至此我们就几乎实现了登陆验证了。

然而我们里接入整个还需要编写一些SpringSecurity配置(实现WebSecurityConfigurerAdapter)。和实现一些类,并且注入(一般卸载Config里)。

关于加密解密的
PasswordEncoder
在这里插入图片描述
例如关于异常的
AccessDeniedHandler
AuthenticationEntryPoint
在这里插入图片描述

同时以上仅仅实现了第一次登陆的功能,后续登陆使用token的话我们可以在实现一个OncePerRequestFilter接口,放在首次登陆接口的前面。实现起来也不难。需要注意的是SecurityContextHolder的使用。
在这里插入图片描述
实现后全部加到Config类的config方法里
在这里插入图片描述
至此,SpringSecurity+JWT实现登陆验证的思路就清晰很多了,再看回故这幅图。
请添加图片描述
总结一下就是:其中蓝色的圈是我们要实现的,红色的字是service要实现的内容,其余黑色的框框是框架的调用逻辑,我们在实现的时候不需要理会。除此之外我们再配置并实现一个Config类(WebSecurityConfigurerAdapter)、关于异常处理的类、OncePerRequestFilter即可。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值