首先讲一下,这篇文章的目的主要是记录一下我是如何由一个知识点慢慢往下挖的,或者说是怎样将知识由点连称线的。涉及到的内容包括web后端中的登录认证功能-拦截器与过滤器-Web容器、Servlet容器与Spring容器的关系。由于主要目的是关于思考、学习流程,而不是知识点本身,所以相关技术知识会比较简略,但是我会相应给出一些链接,可以去原博主那里学习。
本人目前是在校研究生,之前的主要研究内容一直是图像算法之类的。读研两年以后,发现自己对算法实在是有些心有余而力不足了,所以在现在的研三阶段签了一个后端开发的岗位。目前处于小论文发表了,大论文写得差不多了,学校不让离开所在城市没法去已经签了的公司实习的状态,所以这段时间打算顺着具体的项目,来好好学习、整理一下后端开发的思路、流程、习惯什么的。这是我的第一篇文章,也可能是最后一篇(如果我后续懒得写了的话),希望能帮到跟我一样不知道为什么就无法理解问题,但是又非常不擅长读源码的选手。
我目前找到的,认为值得去读一读的工程是EL-ADMIN,有源码,有相应的说明文档,也可以预览,个人认为非常适合拉出一个具体的功能去学。
我首先去看的就是说明文档里的权限控制的内容。在数据交互一块,文档中给出的简单流程是用户登录->后端验证登录返回token
->前端带上token
请求后端数据->后端返回数据。整个流程实际是由token
串联起来,因此需要去关注的问题就变成了:
首先是问题1,结合学过的计网的知识,配合对于数据交互的理解,我便能得到一个比较模糊的概念:token是一个身份验证的方式,类似于令牌一样的存在,在用户使用正确的用户名、密码登陆后,后台分配了一个token,返回给客户端,那么客户端在持有该token以后,后续在请求后端数据时,便不再需要进行用户名、密码对比验证了。
清楚上面流程,实际上问题3也就有了一个基础的理解,token是后续客户端带着来给服务端,作为一个免登录的令牌使用。举个例子,我最近打九价疫苗,第一次去的时候医院要求我出示了身份证、预约的号、登记了身份,然后给了我一张卡片,说第二针和第三针你带着卡就行了,不用身份证了。那这张卡就像是token一样的存在啦。
问题2是我最关注的,也是最复杂的一个点,碰到这种情况,最理想的方式就是阅读源码。
在分析代码前,我首先了解了一下用户认证登录的标准流程,此处参考的是:
接着是带着这个基本流程的概念去阅读源码。由于是在输入账号密码,能够获取授权的token,代码应该定位到用户登录授权的部分(AuthorizationController.java中的login函数)。
@ApiOperation("登录授权")
@AnonymousPostMapping(value = "/login")
public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUser, HttpServletRequest request) throws Exception {
// 密码解密
String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUser.getPassword());
// 查询验证码
String code = (String) redisUtils.get(authUser.getUuid());
// 清除验证码
redisUtils.del(authUser.getUuid());
if (StringUtils.isBlank(code)) {
throw new BadRequestException("验证码不存在或已过期");
}
if (StringUtils.isBlank(authUser.getCode()) || !authUser.getCode().equalsIgnoreCase(code)) {
throw new BadRequestException("验证码错误");
}
// 将name和password封装成token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
// 将token传递给Authentication进行验证
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 获建立安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌与第三方系统获取令牌方式
// UserDetails userDetails = userDetailsService.loadUserByUsername(userInfo.getUsername());
// Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // SecurityContextHolder.getContext().setAuthentication(authentication); String token = tokenProvider.createToken(authentication);
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 保存在线信息
onlineUserService.save(jwtUserDto, token, request);
// 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", properties.getTokenStartWith() + token);
put("user", jwtUserDto);
}};
if (loginProperties.isSingleLogin()) {
//踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUser.getUsername(), token);
}
return ResponseEntity.ok(authInfo);
}
EL-ADMIN使用了Spring Security + Jwt Token
的安全框架,两个部分居然刚好都是我不了解的内容,不过还是查了一堆资料(authentication过程),同样,可以配合提取出来的流程阅读,会简单很多:
在读了EL-ADMIN的源码以后,我对于登录认证有了一个笼统的流程概念,但是因为很多基础知识没学过,问题更多了。比如我不知道这个所谓的上下文信息是什么,存在哪里等等。于是我又翻了很久的关于登录认证的博客资料。非常幸运的找到了一份写得非常好的手撸登录认证的文章。同样的,是我非常喜欢的能够配合代码使用的类型rude-java。
(实际上,这里我的阅读流程是倒置的,因为显然先看这部分,再去看SpringSecurity会是一个更加自然的过程,但是靠自己的学习过程这种情况可太常见了,所i有碰到无法理解的不要慌,继续找一找前置知识点,也许一下子就整个通了。)
文章介绍了目前两种流行的登录认证方式:Session
和JWT
。具体的过程我会简略带过,因为大佬的文章写得非常清楚,我简单讲一下在阅读过程中自己是怎么去理解和延伸问题的。
Session是常见的一种登录认证的方式。用户登录通过,服务端存储相应的用户信息,然后生成session Id给前端,后续前端的请求会携带这个session id,然后服务端就能够通过session id来检查用户的登陆状态。读完文章对应部分+代码对应部分,整个过程就非常好理解了。包括一些小细节,比如用户信息是存在HttpSession中的,第一次访问时,服务器会在响应头的Set-Cookie标识里把Session Id返回给浏览器等等。
Jwt是另一种流行方式,区别是服务器返回的是一串加密的字符串,然后客户端下一次带着这串字符串来校验确认登录态。整体流程和Session非常接近,区别是Jwt是无状态的,也就是没有在服务端保存用户信息。同样读完作者的文章也就都能理解了。
到这里,我发现了一个让我有些别扭的点。Session实现时,作者用了过滤器来判断登录,而在Jwt方法的时候,作者用的拦截器。看代码,两者的功能和实现都过于接近,根据我之前零散看到过知识点,也无法分辨出两者的区别,于是我又把问题转向了:拦截器和过滤器的区别
拦截器和过滤器常见的应用场景包括登录验证、权限验证、日志记录等等。为了相对详细得了解一下两者,拦截器我去看了HandlerInterceptor的相关代码,而过滤器去大概研究了一下OncePerRequestFilter,可以看看这篇:Springboot下使用拦截器和过滤器。
文章中得到的重点信息包括两点:
区别:
共存:在共同使用时,请求会优先进入Filter,最后离开的也是Filter。
分析上述信息以后,我认为的核心在于Filter是属于Servlet规范的一部分,而Interceptor是属于一个spring组件。请求经过两者的顺序应该也是跟这个条件是直接联系的。到这里,我想起来当初在自己半个月后端面试速成的时候,看到过的Web容器、Servlet容器、Spring容器、SpringMVC容器之间的关系。
Tomcat、Servlet、Spring三个部分的内容都很重要,但是在这里我们关注的是他们之间的相对关系。这一篇可以去看看——servlet容器、web容器、spring容器、springmvc容器、dubbo容器之间区别
首先是最外层,也就是Tomcat部分。Tomcat是一个Web容器,与客户端直接打交道的部分。从客户端的角度来看,请求丢给Tomcat,然后Tomcat做了一系列操作后,把请求结果丢回给了我。
从Tomcat的角度来说,它要做的事情是(理清Servlet、Tomcat、web服务器关系):
Tomcat
监听端口servlet
去处理servlet
的service
方法,service
方法返回一个response
对象tomcat
再把这个response
返回给客户端。Web容器管理了servlet(通过Servlet容器)、监听器和过滤器Filter。这些部分是不在spring和springmvc的掌控范围内的。
Servlet容器是管理Servlet对象的。在请求到来时,Servlet容器会调配调用哪个Servlet,并返回结果。从Servlet的角度来说,它做的事情是:
Servlet从最本质上来说,其实就是个接口,定义了一整套处理网络请求的规范(关于Servlet,知乎上这篇Servlet本质的第一个回复我个人认为值得一看。)
Spring容器管理service和dao,SpringMVC容器管理controller,Spring容器是SpringMVC的父容器。到这个部分,其实已经是特别熟悉的开发中关注的业务逻辑实现的部分了,也就不展开了。
到这个部分,再回头去思考过滤器Filter和拦截器Interceptor在共存时的执行顺序。Filter是由Web容器直接管理的、依赖于Servlet容器的,不归属于Spring容器管理,而拦截器Interceptor是属于一个Spring组件。显然,在任意一个请求过来时,都是先经过了Filter,而后才有可能到达Interceptor,在任意一个请求离开时,则都是先离开了Interceptor,才会从Filter离开,整个过程就非常好理解了。
顺着登录功能一路看到了Web容器,其实是越看越偏了,当然原因主要还是没有系统地去学习过,因为没有基础知识的支撑,导致知识点是以零散的点的形式在大脑中被记忆下来,知道看到几个容器之间的关系,才算是勉强连成了一条知识线。当然,其实很多内容不需要深究就能够顺利地开发各种功能,但是隐患也就很多了。
实际上,我在大二的时候,有接触过一些后端开发的内容,现在还模糊记得当初自己写好了代码需要打包成jar包,然后发到服务器上,去服务器上部署好,启动tomcat什么的。直到去年暑假去实习,是第二次接触后端开发,框架变成了springboot,启动一个工程的方式跟记忆中的流程也差了好多步骤的样子,粗糙地查了查,哦哦,内置了这个内置了那个,留下了这样的印象。所有的事情都好像是变简单了,但是理解问题对我来说复杂起来了,包装地实在太好了!我只需要天天按按钮,输入我要个怎样怎样的A,然后拿到我相应的物品,即使完全不知道A的加工流程也没关系。直到某一天,我要求的红色的、甜口的、圆形的A物品变成了椭圆形的A,事情就大条了。为了避免工作以后碰到这样的事,凡事还是往下挖一挖,有备无患嘛!