JavaWeb登录认证
在Web系统中,如果没有登录功能和登录认证,是可以直接访问到Web系统的后台的。 这是不安全的,所以我们今天的主题就是登录认证。最终要实现的效果是:
- 如果用户名密码错误,不允许登录系统。
- 如果用户名和密码都正确,则登录成功,可以访问系统。
-
登录功能
-
需求
在登录界面中,输入用户的用户名和密码,然后点击 "登录" 按钮请求服务器,服务端判断用户输入的用户名或者密码是否正确。如果正确,则返回登录成功的结果,前端跳转至系统首页。
-
接口描述
我们参照接口文档中的 其他接口
-> 登录接口
-
思路分析
-
怎么样才算登录成功了呢?
-
用户名和密码都输入正确,登录成功
-
否则,登录失败
-
-
登录功能的本质是什么?
-
查询
-
根据用户名和密码查询员工信息
-
-
功能开发
1). 准备实体类 LoginInfo
, 封装登录成功后, 返回给前端的数据 。
/*** 登录成功结果封装类*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {private Integer id; //员工IDprivate String username; //用户名private String name; //姓名private String token; //令牌
}
2). 定义LoginController
@Slf4j
@RestController
public class LoginController {@Autowiredprivate EmpService empService;@PostMapping("/login")public Result login(@RequestBody Emp emp){log.info("员工来登录啦 , {}", emp);LoginInfo loginInfo = empService.login(emp);if(loginInfo != null){return Result.success(loginInfo);}return Result.error("用户名或密码错误~");}}
3). EmpService
接口中增加 login 登录方法
/*** 登录*/
LoginInfo login(Emp emp);
4). EmpServiceImpl
实现login方法
@Override
public LoginInfo login(Emp emp) {Emp empLogin = empMapper.getUsernameAndPassword(emp);if(empLogin != null){LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), null);return loginInfo;}return null;
}
5). EmpMapper
增加接口方法
/*** 根据用户名和密码查询员工信息*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getUsernameAndPassword(Emp emp);
现在登录功能就已经开发完毕,但是当在浏览器中输入地址web系统的功能接口时,发现没有登录仍然可以进入到后端管理系统页面。
而真正的登录功能应该是:登陆后才能访问后端系统页面,不登陆则跳转到登陆页面进行登陆。
为什么会出现这个问题?其实原因很简单,就是因为针对于我们当前所开发的部门管理、员工管理以及文件上传等相关接口来说,我们在服务器端并没有做任何的判断,没有去判断用户是否登录了。所以无论用户是否登录,都可以访问部门管理以及员工管理的相关数据。所以我们目前所开发的登录功能,它只是徒有其表。而我们要想解决这个问题,我们就需要完成一步非常重要的操作:登录校验。
那接下来,我们就进入到今天课程内容的第二章节,登录校验功能的实现。
-
登录校验
所谓登录校验,指的是在服务器端接收到浏览器发送过来的请求之后,首先需要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。
-
思路
了解完什么是登录校验之后,接下来我们分析一下登录校验大概的实现思路。
首先我们在宏观上先有一个认知:
前面在讲解HTTP协议的时候,我们提到HTTP协议是无状态协议。什么又是无状态的协议?
所谓无状态,指的是每一次请求都是独立的,下一次请求并不会携带上一次请求的数据。而浏览器与服务器之间进行交互,基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口,实现了登陆的操作,接下来我们在执行其他业务操作时,服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的,两次请求之间是独立的,所以是无法判断这个员工到底登陆了没有。
那应该怎么来实现登录校验呢?具体的实现思路可以分为两部分:
-
在员工登录成功后,将用户登录成功的信息存起来,记录用户已经登录成功的标记。
-
在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。
想要判断员工是否已经登录,我们需要在员工登录成功之后,存储一个登录成功的标记,接下来在每一个接口方法执行之前,先做一个条件判断,判断一下这个员工到底登录了没有。如果是登录了,就可以执行正常的业务操作,如果没有登录,会直接给前端返回一个错误的信息,前端拿到这个错误信息之后会自动的跳转到登录页面。
我们程序中所开发的查询功能、删除功能、添加功能、修改功能,都需要使用以上套路进行登录校验。此时就会出现:相同代码逻辑,每个功能都需要编写,就会造成代码非常繁琐。
为了简化这块操作,我们可以使用一种技术:统一拦截技术。
通过统一拦截的技术,我们可以来拦截浏览器发送过来的所有的请求,拦截到这个请求之后,就可以通过请求来获取之前所存入的登录标记,在获取到登录标记且标记为登录成功,就说明员工已经登录了。如果已经登录,我们就直接放行(意思就是可以访问正常的业务接口了)。
要完成以上操作,会涉及到web开发中的两个技术:
-
会话技术:用户登录成功之后,在后续的每一次请求中,都可以获取到该标记。
-
统一拦截技术:过滤器Filter、拦截器Interceptor
下面我们先学习会话技术,然后再学习统一拦截技术。
-
会话技术
介绍了登录校验的大概思路之后,我们先来学习下会话技术。
-
介绍
什么是会话?
-
在我们日常生活当中,会话指的就是谈话、交谈。
-
在web开发当中,会话指的是浏览器与服务器之间的一次连接,我们就称为一次会话。
用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连接,此时会话就结束。在一次会话当中,是可以包含多次请求和响应的。
比如:打开了浏览器来访问web服务器上的资源(浏览器不能关闭、服务器不能断开)
第1次:访问的是登录的接口,完成登录操作
第2次:访问的是部门管理接口,查询所有部门数据
第3次:访问的是员工管理接口,查询员工数据
只要浏览器和服务器都没有关闭,以上3次请求都属于一次会话当中完成的。
需要注意的是:会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别出多次请求是否来自于同一浏览器,以便在同一个会话的,多次请求间共享数据。
服务器会接收很多的请求,但是服务器是需要识别出这些请求是不是同一个浏览器发出来的。比如:1和2这两个请求是不是同一个浏览器发出来的,3和5这两个请求不是同一个浏览器发出来的。如果是同一个浏览器发出来的,就说明是同一个会话。如果是不同的浏览器发出来的,就说明是不同的会话。而识别多次请求是否来自于同一浏览器的过程,我们就称为会话跟踪。
使用会话跟踪技术就是要完成在同一个会话中,多个请求之间进行共享数据。
为什么要共享数据呢?
由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据呢?此时就需要在一次会话的多次请求之间进行数据共享
会话跟踪技术有两种:
-
Cookie(客户端会话跟踪技术):数据存储在客户端浏览器当中
-
Session(服务端会话跟踪技术):数据存储在储在服务端
-
令牌技术
-
会话跟踪方案
上面我们介绍了什么是会话,什么是会话跟踪,并且也提到了会话跟踪 3 种常见的技术方案。接下来,我们就来对比一下这 3 种会话跟踪的技术方案,来看一下具体的实现思路,以及它们之间的优缺点。
-
方案一:Cookie
cookie 是客户端会话跟踪技术,它是存储在客户端浏览器中的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。
比如:
- 第一次请求了登录接口,如果登录成功,我们就可以在服务器端设置一个cookie,在 cookie 中就可以将多次请求间需要共享的数据存储在cookie中。(比如存储登录成功的标识,用户相关的一些数据信息,我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。)
- 之后服务器在给客户端响应数据的时候,会自动将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
-
接下来在服务端就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。
我刚才在介绍流程的时候,用了 3 个自动:
-
服务器会 自动 将 cookie 响应给浏览器。
-
浏览器接收到响应回来的数据之后,会 自动 将 cookie 存储在浏览器本地。
-
在后续的请求当中,浏览器会 自动 将 cookie 携带到服务器端。
为什么这一切都是自动化进行的?
因为 cookie 是 HTTP 协议中支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方提供了下面的响应头和请求头:
-
响应头 Set-Cookie :当在服务器端设置Cookie后,服务器会自动在响应头Set-Cookie中将cookie响应给客户端浏览器,而客户端浏览器在响应头中见到Set-Cookie时,会自动将cookie保存在浏览器本地
-
请求头 Cookie:在后续的每一次请求中,都会将当前服务器所涉及到的cookie全部携带到服务器端,通过请求头Cookie携带
代码测试:
@Slf4j
@RestController
public class SessionController {//设置Cookie:服务器给客户端响应cookie@GetMapping("/c1")public Result cookie1(HttpServletResponse response){//设置Cookie/响应Cookieresponse.addCookie(new Cookie("login_username","itheima")); return Result.success();}//获取Cookie:在服务端获取请求中携带过来的cookie@GetMapping("/c2")public Result cookie2(HttpServletRequest request){//获取到传递过来的所有cookieCookie[] cookies = request.getCookies();//循环遍历所有cookiefor (Cookie cookie : cookies) {if(cookie.getName().equals("login_username")){System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie}}return Result.success();}
}
测试:
A. 访问c1接口,设置Cookie,http://localhost:8080/c1
我们可以看到,在服务器端设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器本地。
B. 访问c2接口 http://localhost:8080/c2
,此时浏览器会自动的将Cookie携带到服务端,通过请求头Cookie携带到服务器。
优缺点:
-
优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
-
缺点:
-
移动端APP(Android、IOS)中无法使用Cookie
-
不安全,用户可以自己禁用Cookie
-
Cookie不能跨域
-
-
现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
-
我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
-
然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
-
那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
-
此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域
区分跨域的维度(三个维度有任何一个维度不同,那就是跨域操作):
-
协议
-
IP/协议
-
端口
举例:
-
http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域]
-
http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]
-
http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]
-
http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]
-
方案二:Session
它是服务器端会话跟踪技术,所以它是存储在服务器端的。Session 的底层是基于 Cookie 来实现的,在Cookie中存储的是Session对象的ID(Set-Cookie,Cookie)
-
获取Session
浏览器在第一次请求服务器的时候,可以直接在服务器中获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。
-
响应Cookie (JSESSIONID)
接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。就是在响应头中增加了一个 Set-Cookie 响应头。 cookie 的名是固定是 JSESSIONID 代表服务器端会话对象 Session 的 ID,cookie的值就是Session对象的ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。
-
查找Session
接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
这样我们是不是就可以通过 Session 会话对象在同一次会话的多次请求之间来共享数据了?好,这就是基于 Session 进行会话跟踪的流程。
代码测试
@Slf4j
@RestController
public class SessionController {@GetMapping("/s1")public Result session1(HttpSession session){log.info("HttpSession-s1: {}", session.hashCode());session.setAttribute("loginUser", "tom"); //往session会话对象中存储数据return Result.success();}@GetMapping("/s2")public Result session2(HttpServletRequest request){HttpSession session = request.getSession();log.info("HttpSession-s2: {}", session.hashCode());Object loginUser = session.getAttribute("loginUser"); //从session会话对象中获取数据log.info("loginUser: {}", loginUser);return Result.success(loginUser);}
}
测试:
A. 访问 s1 接口,http://localhost:8080/s1
请求完成之后,在响应头中,就会看到有一个Set-Cookie的响应头,里面响应回来了一个Cookie,就是JSESSIONID,这个就是服务端会话对象 Session 的ID。
B. 访问 s2 接口,http://localhost:8080/s2
接下来,在后续的每次请求时,都会将Cookie的值,携带到服务端,那服务端呢,接收到Cookie之后,会自动的根据JSESSIONID的值,找到对应的会话对象Session。
那经过这两步测试,大家也会看到,在控制台中输出如下日志:
两次请求,获取到的Session会话对象的hashcode是一样的,就说明是同一个会话对象。而且,第一次请求时,往Session会话对象中存储的值,在第二次请求时,也获取到了。 那这样,我们就可以通过Session会话对象,在同一个会话的多次请求之间数据共享了。
优缺点
-
优点:Session是存储在服务端的,安全
-
缺点:
-
服务器集群环境下无法直接使用Session
-
移动端APP(Android、IOS)中无法使用Cookie
-
用户可以自己禁用Cookie
-
Cookie不能跨域
-
PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。
服务器集群环境为何无法使用Session?
-
首先第一点,我们现在所开发的项目,一般都不会只部署在一台服务器上,因为一台服务器会存在一个很大的问题,就是单点故障。所谓单点故障,指的就是一旦这台服务器挂了,整个应用都没法访问了。
-
所以在现在的企业项目开发当中,最终部署的时候都是以集群的形式来进行部署,也就是同一个项目它会部署在多台服务器上。比如这个项目我们现在就部署了 3 份。
-
而用户在访问的时候,到底访问这三台其中的哪一台?其实用户在访问的时候,他会访问一台前置的服务器,我们叫负载均衡服务器,我们在后面项目当中会详细讲解。目前大家先有一个印象负载均衡服务器,它的作用就是将前端发起的请求均匀的分发给后面的这三台服务器。
-
此时假如我们通过 session 来进行会话跟踪,可能就会存在这样一个问题。用户打开浏览器要进行登录操作,此时会发起登录请求。登录请求到达负载均衡服务器,将这个请求转给了第一台 Tomcat 服务器。
Tomcat 服务器接收到请求之后,要获取到会话对象session。获取到会话对象 session 之后,要给浏览器响应数据,最终在给浏览器响应数据的时候,就会携带这么一个 cookie 的名字,就是 JSESSIONID ,下一次再请求的时候,是不是又会将 Cookie 携带到服务端?
好。此时假如又执行了一次查询操作,要查询部门的数据。这次请求到达负载均衡服务器之后,负载均衡服务器将这次请求转给了第二台 Tomcat 服务器,此时他就要到第二台 Tomcat 服务器当中。根据JSESSIONID 也就是对应的 session 的 ID 值,要找对应的 session 会话对象。
我想请问在第二台服务器当中有没有这个ID的会话对象 Session, 是没有的。此时是不是就出现问题了?我同一个浏览器发起了 2 次请求,结果获取到的不是同一个会话对象,这就是Session这种会话跟踪方案它的缺点,在服务器集群环境下无法直接使用Session。
大家会看到上面这两种传统的会话技术,在现在的企业开发当中是不是会存在很多的问题。 为了解决这些问题,在现在的企业开发当中,基本上都会采用第三种方案,通过令牌技术来进行会话跟踪。接下来我们就来介绍一下令牌技术,来看一下令牌技术又是如何跟踪会话的。
-
方案三 - 令牌技术
令牌,其实它就是一个用户身份标识,其本质就是一个字符串。
如果通过令牌技术来跟踪会话,以登录功能为例,当浏览器端发起请求访问登录接口时,如果前端传递的用户名和密码都是正确的,此时成功登录,登录成功后在服务器端就会创建一个令牌,在响应数据的时候,一并将令牌响应给前端。
接下来在前端程序中接收到令牌后,需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。
在后续的每一次请求中,都需要将令牌携带到服务端。携带到服务端之后,就需要校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。
此时,如果是在同一次会话的多次请求之间,想共享数据,就可以将共享的数据存储在令牌中。
优缺点
-
优点:
-
支持PC端、移动端
-
解决集群环境下的认证问题,因为令牌这种方案不需要在服务器端保存任何信息
-
减轻服务器的存储压力(无需在服务器端存储)
-
-
缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
JWT令牌最典型的应用场景就是登录认证:
-
在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。
-
前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
-
服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。
在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:
-
在登录成功之后,要生成令牌。
-
每一次请求当中,要接收令牌并对令牌进行校验。
-
JWT令牌
前面我们介绍了基于令牌技术来实现会话追踪。这里所提到的令牌就是用户身份的标识,其本质就是一个字符串。令牌的形式有很多,我们使用的是功能强大的 JWT令牌。
-
介绍
-
JWT全称 JSON Web Token (官网:https://jwt.io/),定义了一种简洁的、自包含的格式,用于在通信双方以json数据的格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
-
简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
-
自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
-
简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。
-
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
-
第一部分:Header(头), 记录令牌的类型、令牌的签名算法等。 例如:{"alg":"HS256","type":"JWT"}
-
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
-
第三部分:Signature(签名),防止Token被篡改、确保安全性。基于一定的加密算法计算出来的,在进行加密操作时,会将header和payload融入进去,并且还要加入我们指定的密钥,最终基于一定的签名算法生成jwt令牌的签名部分(这个签名算法就是在创建令牌时要指定的签名算法)。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。
JWT是如何将原始的JSON格式数据,转变为字符串的呢?
-
在生成JWT令牌时,会对JSON格式的数据进行base64编码
-
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号
-
需要注意的是Base64是编码方式,而不是加密方式。
-
生成和校验jwt令牌
1). 首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验。工具类:Jwts
2). 生成JWT代码实现:
/*** 生成JWT令牌:* 1、使用Jwts工具类的builder()方法来构建Jwt令牌,* 2、同时链式调用 signWith(签名算法,密钥)方法 ,指定生成签名时使用的签名算法,* 3、再继续链式调用 addClaims(Map集合)方法 给令牌中添加自定义数据* 4、对于生成的jwt令牌还可以链式调用 setExpiration(Date对象)方法 指定有效期* 5、最后调用 compact()方法 生成jwt令牌*/
@Test
public void testGenerateJwt() {//将需要加入令牌中的自定义信息封装在Map<String,Object>集合中Map<String, Object> claims = new HashMap<>();claims.put("id", 10);claims.put("username", "itheima");String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0").addClaims(claims).setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000)).compact();System.out.println(jwt);
}
- 签名算法可以看Jwt官网的下图部分得知:
- 密钥部分可以传一个基于base64编码的字符串进去
- 通过浏览器搜索Base64在线编解码获得
运行测试方法:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk
输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来:
第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。
第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。
由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。
3). 校验JWT令牌(解析生成的令牌):
/*** 解析jwt令牌* 1.使用Jwts调用 parser()方法 解析令牌* 2.链式调用setSigningKey(生成令牌时的密钥)方法指定生成令牌时的密钥* 3.继续链式调用 parseClaimsJws()方法 解析传过来的jwt令牌* 4.续集链式调用 getBody()方法 获取Claims,它是生成令牌时往令牌中添加的自定义信息*/
@Test
public void testParseJwt() {Claims claims = Jwts.parser().setSigningKey("aXRjYXN0").parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko").getBody();System.out.println(claims);
}
运行测试方法:
{id=10, username=itheima, exp=1701909015}
令牌解析后,我们可以看到id和过期时间,如果在解析的过程当中没有报错,就说明解析成功了。
-
登录时下发令牌
JWT令牌的生成和校验的基本操作我们已经学习完了,接下来我们就需要在案例当中通过JWT令牌技术来跟踪会话。具体的思路我们前面已经分析过了,主要就是两步操作:
-
生成令牌
登录成功之后生成一个JWT令牌,并把这个令牌返回给前端 -
校验令牌
拦截前端请求,从请求中获取到令牌,对令牌进行解析校验
首先来完成:登录成功之后生成一个JWT令牌,并把这个令牌返回给前端。
实现步骤:
1.自定义定义JWT令牌操作的工具类:
package com.itheima.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;
import java.util.Map;public class JwtUtils {private static String signKey = "SVRIRUlNQQ==";private static Long expire = 43200000L;/*** 生成JWT令牌* @return*/public static String generateJwt(Map<String,Object> claims){String jwt = Jwts.builder().addClaims(claims).signWith(SignatureAlgorithm.HS256, signKey).setExpiration(new Date(System.currentTimeMillis() + expire)).compact();return jwt;}/*** 解析JWT令牌* @param jwt JWT令牌* @return JWT第二部分负载 payload 中存储的内容*/public static Claims parseJWT(String jwt){Claims claims = Jwts.parser().setSigningKey(signKey).parseClaimsJws(jwt).getBody();return claims;}
}
2.完善 EmpServiceImpl
中的 login
方法逻辑, 登录成功,生成JWT令牌并返回
@Override
public LoginInfo login(Emp emp) {//根据用户名和密码查询用户信息Emp empLogin = empMapper.getUsernameAndPassword(emp);//如果用户信息不为nullif(empLogin != null){//1. 生成JWT令牌Map<String,Object> dataMap = new HashMap<>();dataMap.put("id", empLogin.getId());dataMap.put("username", empLogin.getUsername());String jwt = JwtUtils.generateJwt(dataMap);LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);return loginInfo;}return null;
}
重启服务,打开 Apifox 测试登录接口:
打开浏览器完成前后端联调操作:利用开发者工具,抓取一下网络请求
登录请求完成后,可以看到JWT令牌已经响应给了前端,此时前端就会将JWT令牌存储在浏览器本地。
服务器响应的JWT令牌存储在本地浏览器哪里了呢?
-
在当前案例中,JWT令牌存储在浏览器的本地存储空间
localstorage
中。localstorage
是浏览器的本地存储,在移动端也是支持的。
我们在发起一个查询部门数据的请求,此时我们可以看到在请求头中包含一个token(JWT令牌),在后续的每一次请求中,浏览器都会将这个令牌在请求头中携带到服务端。
-
过滤器Filter
怎么样 统一拦截前端所有的请求来 校验令牌的有效性 ?这里我们会学习两种解决方案:
-
Filter过滤器
-
Interceptor拦截器
-
Filter快速入门
什么是Filter?
-
Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
-
过滤器可以把前端对资源的请求拦截下来,过滤器处理完毕之后,才可以访问对应的资源
-
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
下面我们通过Filter快速入门程序掌握过滤器的基本使用操作:
-
第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。
-
第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
1). 定义过滤器
public class DemoFilter implements Filter {//过滤器的初始化方法:在web服务器启动的时候会自动的创建Filter过滤器对象,//然后自动调用init初始化方法(只会被调用一次)public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init ...");}//每拦截到一次请求之后就会调用一次doFilter()方法(这个方法会被调用多次)public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {System.out.println("拦截到了请求...");chain.doFilter(request, response);}//关闭服务器时,会自动的调用销毁方法destroy(只会被调用一次)public void destroy() {System.out.println("destroy ... ");}
}
2). 配置过滤器
在定义完Filter之后,还需要完成Filter的配置,在Filter类上添加一个注解:@WebFilter
,并指定属性urlPatterns
,通过这个属性指定过滤器要拦截的请求路径。
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {//初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init ...");}//拦截到请求时,调用该方法,可以调用多次public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {System.out.println("拦截到了请求...");chain.doFilter(request, response);}//销毁方法, web服务器关闭时调用, 只调用一次public void destroy() {System.out.println("destroy ... ");}
}
在Filter类上添加@WebFilter注解之后,还需要在启动类上面添加注解@ServletComponentScan
,通过@ServletComponentScan
注解来开启SpringBoot项目对于Servlet组件的支持。
@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {public static void main(String[] args) {SpringApplication.run(TliasManagementApplication.class, args);}
}
重新启动服务,打开浏览器,执行部门管理的请求,可以看到控制台输出了过滤器中的内容:
注意事项:在过滤器Filter中,如果不执行放行操作,将无法访问后面的资源。 放行操作:chain.doFilter(request, response);
-
实现登录校验过滤器
-
分析
使用过滤器Filter来完成案例当中的登录校验功能。
-
对于校验令牌的这一块操作,我们使用登录校验的过滤器,在过滤器当中来校验令牌的有效性。如果令牌是无效的,就响应一个错误的信息,也不会再去放行访问对应的资源了。如果令牌存在,并且它是有效的,此时就会放行去访问对应的web资源,执行相应的业务操作。
思考两个问题:
-
对于所有的请求,拦截到之后,都需要校验令牌吗 ?
-
答案:登录请求不校验
-
-
拦截到请求后,什么情况下才可以放行,执行业务操作 ?
-
答案:有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
-
-
具体流程
要完成登录校验,主要是利用Filter过滤器实现,令牌校验Filter过滤器的实现流程如下:
基于上面的业务流程,我们分析出具体的操作步骤:
-
获取请求的url
-
判断请求的url中是否包含login,如果包含,说明是登录操作,放行
-
获取请求头中的令牌(token)
-
判断令牌是否存在,如果不存在,响应 401
-
解析token,如果解析失败,响应 401
-
放行
-
代码实现
在 com.itheima.filter
包下创建TokenFilter
,具体代码如下:
package com.itheima.filter;import com.itheima.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.util.StringUtils;
import java.io.IOException;/*** 令牌校验过滤器*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) resp;//1. 获取前端发送的请求路径。String url = request.getRequestURL().toString();//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。if(url.contains("login")){ //登录请求log.info("登录请求 , 直接放行");chain.doFilter(request, response);return;}//3. 获取请求头中的令牌(token)。String jwt = request.getHeader("token");//4. 判断令牌是否存在,如果不存在说明没有登录,返回错误结果(未登录)if(!StringUtils.hasLength(jwt)){ //jwt为空log.info("获取到jwt令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return;}//5. 如果token存在,则解析token,如果解析失败,返回错误结果(未登录)try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return;}//6. 如果校验token通过,则放行log.info("令牌合法, 放行");chain.doFilter(request , response);}}
登录校验的过滤器编写完成,接下来就可以重新启动服务来做一个测试:
-
测试1:未登录是否可以访问部门管理页面
首先关闭浏览器,重新打开浏览器,在地址栏中输入:http://localhost:90
由于用户没有登录,登录校验过滤器返回错误信息,前端页面根据返回的错误信息结果,自动跳转到登录页面了
-
测试2:先进行登录操作,再访问部门管理页面
登录校验成功之后,可以正常访问相关业务操作页面
-
Filter详解
主要介绍以下3个方面的细节:
-
过滤器的执行流程
-
过滤器的拦截路径配置
-
过滤器链
-
执行流程
过滤器的执行流程:
在过滤器中拦截到了请求之后,如果希望继续访问后面的web资源,需要执行放行操作(放行操作就是调用 FilterChain对象当中的doFilter()方法),调用doFilter()方法前编写的代码属于放行前的逻辑。
放行之后,访问完 web 资源之后还会回到过滤器中,回到过滤器之后如有需求还可以执行放行后的逻辑,放行后的逻辑写在doFilter()这行代码后。
测试代码:
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {@Override //初始化方法, 只调用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init 初始化方法执行了");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("DemoFilter 放行前逻辑.....");//放行请求filterChain.doFilter(servletRequest,servletResponse);System.out.println("DemoFilter 放行后逻辑.....");}@Override //销毁方法, 只调用一次public void destroy() {System.out.println("destroy 销毁方法执行了");}
}
启动之后运行测试:
-
拦截路径
Filter可以根据需求,配置不同的拦截路径:
拦截路径 | urlPatterns值 | 含义 |
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
下面我们来测试"拦截具体路径":
@WebFilter(urlPatterns = "/login") //拦截/login具体路径
public class DemoFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("DemoFilter 放行前逻辑.....");//放行请求filterChain.doFilter(servletRequest,servletResponse);System.out.println("DemoFilter 放行后逻辑.....");}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic void destroy() {Filter.super.destroy();}
}
-
过滤器链
过滤器链指的是在一个web应用程序中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。
比如:在我们web服务器当中,定义了两个过滤器,这两个过滤器就形成了一个过滤器链。
而这个链上的过滤器在执行的时候会一个一个的执行,会先执行第一个Filter,放行之后再执行第二个Filter,如果执行到最后一个过滤器放行后,才会访问对应的web资源。
访问完web资源之后,按照我们刚才所介绍的过滤器的执行流程,还会回到过滤器当中来执行过滤器放行后的逻辑,而在执行放行后的逻辑的时候,顺序是反着的。
先要执行过滤器2放行后的逻辑,再来执行过滤器1放行后的逻辑,最后在给浏览器响应数据。
过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:
AbcFilter
DemoFilter
这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。
-
拦截器Interceptor
-
快速入门
-
是一种动态拦截方法调用的机制,类似于过滤器。
-
拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。
-
拦截器的作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
-
什么是拦截器?
在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。
下面通过快速入门程序,来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似,也分为两步:
-
定义拦截器
-
注册配置拦截器
1). 自定义拦截器
定义一个类实现HandlerInterceptor接口,并重写其所有方法
- 由于拦截器是Spring框架中提供的,所以要交给Spring IOC容器管理。
//自定义拦截器
@Component
public class DemoInterceptor implements HandlerInterceptor {//目标资源方法执行前执行,最终返回true:放行 返回false:不放行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("preHandle .... ");return true; //true表示放行}//目标资源方法执行后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle ... ");}//视图渲染完毕后执行,最后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion .... ");}
}
2). 注册配置拦截器
创建一个配置类 WebConfig
, 实现 WebMvcConfigurer
接口,并重写其 addInterceptors
方法
- 在
addInterceptor方法中使用InterceptorRegistry对象调用addInterceptor方法注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate DemoInterceptor demoInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象,并设置拦截器拦截的请求路径( /** 表示拦截所有请求)registry.addInterceptor(demoInterceptor).addPathPatterns("/**");}
}
重新启动SpringBoot服务,打开Apifox测试:
可以看到控制台输出的日志:
接下来再来做一个测试:将拦截器中preHandle方法的返回值改为false
使用Apifox,再次发送请求,并没有响应数据,说明请求被拦截了但没有放行
-
令牌校验Interceptor
通过拦截器来完成登录校验功能
登录校验的业务逻辑以及操作步骤和登录校验Filter过滤器当中的逻辑是完全一致的。
1). TokenInterceptor
在 com.itheima.interceptor
包下创建 TokenInterceptor
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 获取请求url。String url = request.getRequestURL().toString();//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。if(url.contains("login")){ //登录请求log.info("登录请求 , 直接放行");return true;}//3. 获取请求头中的令牌(token)。String jwt = request.getHeader("token");//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。if(!StringUtils.hasLength(jwt)){ //jwt为空log.info("获取到jwt令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}//5. 解析token,如果解析失败,返回错误结果(未登录)。try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}//6. 放行。log.info("令牌合法, 放行");return true;}}
2). 配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {//拦截器对象@Autowiredprivate TokenInterceptor tokenInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");}
}
-
Interceptor详解
-
拦截器的拦截路径配置
-
拦截器的执行流程
-
拦截路径
首先看拦截器的拦截路径配置,在注册配置拦截器的时候,我们要指定拦截器的拦截路径,通过addPathPatterns("要拦截路径")
方法,指定要拦截哪些请求路径。
在入门程序中我们配置的是/**
,表示拦截所有资源,而在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns("不拦截路径")
方法,指定哪些请求路径不需要拦截。
@Configuration
public class WebConfig implements WebMvcConfigurer {//拦截器对象@Autowiredprivate DemoInterceptor demoInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(demoInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求).excludePathPatterns("/login");//设置不拦截的请求路径}
}
在拦截器中除了可以设置/**
拦截所有资源外,还有一些常见拦截路径设置:
拦截路径 | 含义 | 举例 |
/* | 匹配一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
/** | 匹配任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
/depts/* | 匹配/depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
/depts/** | 匹配/depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
-
执行流程
接下来再来介绍拦截器的执行流程。通过执行流程,大家就能够清晰的知道过滤器与拦截器的执行时机。
当过滤器和拦截器同时存在时,请求会先被过滤器拦截,然后才会到拦截器,因为拦截器是Spring框架提供的,它只会拦截对Spring中的请求,也就是要来访问我们所定义的controller中的接口方法。
过滤器和拦截器它们之间的区别主要是两点:
-
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
-
拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
相关文章:

JavaWeb登录认证
在Web系统中,如果没有登录功能和登录认证,是可以直接访问到Web系统的后台的。 这是不安全的,所以我们今天的主题就是登录认证。最终要实现的效果是: 如果用户名密码错误,不允许登录系统。如果用户名和密码都正确&…...

半导体制造工艺(二)光刻工艺—掩模版
在上文中我们已经简单概述了光刻工艺的大致流程。接下来将会介绍在光刻工艺中所需用到的必备材料以及设备。例如掩模版、光刻胶、匀胶机、光刻机等等。由于需要保持讲述工艺的完整性以及流畅,每一个都需要涉及,所以每次仅是侧重点不同。此篇主要讲述的是…...

计算机视觉算法实战——高精度分割(主页有源码)
✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ 1. 高精度分割领域简介✨✨ 图像分割是计算机视觉中的核心任务之一,其目标是将图像划分为多个语义区域,并为…...

DeepSeek-R1-Zero:基于基础模型的强化学习
注:此文章内容均节选自充电了么创始人,CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》(人工智能科学与技术丛书)【陈敬雷编著】【清华大学出版社】 文章目录 DeepSeek大模型技术系列四DeepSeek大模型技术系列四》DeepSeek-…...
判断一个文件中以三个#号开头有多少行的shell脚本怎么写
在Linux中,你可以使用grep命令结合正则表达式来统计一个文件中以三个#号开头的行数。以下是一个简单的命令: grep -c ^### filename这里的grep是搜索工具,-c选项表示统计匹配的行数,###是正则表达式,表示行…...
PHP如何与HTML结合使用?
PHP与HTML结合使用的主要方式是通过在HTML文件中嵌入PHP代码,从而实现动态内容的生成和网页的交互性。以下是详细的方法和最佳实践: 1. 嵌入PHP代码到HTML中 PHP代码可以直接嵌入到HTML文件中,通过<?php ?>标签来包裹PHP代码。服务…...
计算机网络之传输层(传输层的功能)
一、数据分段与重组 传输层从会话层接收数据,并将其分割成较小的数据段,以适应网络层的最大传输单元(MTU)限制。在目的端,传输层负责将这些数据段重新组合成原始数据,确保数据的完整性和正确性。 二、端口…...

矩阵碰一碰发视频源码搭建之,支持OEM
引言 阵碰一碰发视频" 技术凭借其便捷的交互方式和高效的传播能力,已成为品牌推广和内容创作的重要工具。为进一步提升视频传播效果,本文将深入探讨如何在矩阵碰一碰系统中集成 AI 文案生成功能,实现 "一碰即传 智能文案" 的…...
DeepSeek 2月27日技术突破:三大核心功能解析与行业影响
DeepSeek 2月27日技术突破:三大核心功能解析与行业影响 一、最新发布功能全景图 1. DualPipe:双向流水线并行革命 DualPipe是一项极具创新性的双向管道并行算法,旨在解决大规模模型训练过程中计算与通信效率低下的关键问题。在传统的模型训…...

【实战 ES】实战 Elasticsearch:快速上手与深度实践-1.2.2倒排索引原理与分词器(Analyzer)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 1.2.2倒排索引原理与分词器(Analyzer)1. 倒排索引:搜索引擎的基石1.1 正排索引 vs 倒排索引示例数据对比: 1.2 倒排索引核心结…...
Vue.js响应式基础
响应式基础 API 参考 本页和后面很多页面中都分别包含了选项式 API 和组合式 API 的示例代码。现在你选择的是 组合式 API。你可以使用左侧侧边栏顶部的“API 风格偏好”开关在 API 风格之间切换。 声明响应式状态 ref() 在组合式 API 中,推荐使用 ref() 函数来声明…...
DeepSeek-OpenSourceWeek-第四天-Optimized Parallelism Strategies
DeepSeek 在 #OpenSourceWeek(开源周) 的第四天推出了两项新工具,旨在让深度学习更快、更高效:**DualPipe** 和 **EPLB**。 DualPipe 定义:DualPipe 是一种用于 V3/R1 训练中计算与通信重叠的双向pipline并行算法。 作用:它通过实现前向和后向计算-通信阶段的完全重叠,减…...
深入浅出:插入排序算法完全解析
1. 什么是插入排序? 插入排序(Insertion Sort)是一种简单的排序算法,其基本思想与我们整理扑克牌的方式非常相似。我们将扑克牌从第二张开始依次与前面已排序的牌进行比较,将其插入到合适的位置,直到所有牌…...

【Keras图像处理入门:图像加载与预处理全解析】
本文将全面讲解如何使用Keras进行图像加载、预处理和数据增强,为深度学习模型准备高质量的图像数据。 一、单张图像处理基础 1. 图像加载与尺寸调整 from keras.preprocessing import image# 加载图像并调整尺寸 img image.load_img(example.jpg, target_size(1…...
企业级AI办公落地实践:基于钉钉/飞书的标准产品解决方案
一、平台化AI的崛起:开箱即用的智能革命 2024年企业AI应用调研数据显示: 73%的中型企业选择平台标准产品而非自研头部SaaS平台AI功能渗透率达89%典型ROI周期从18个月缩短至3-6个月 核心优势对比: 维度自研方案平台标准产品部署周期6-12个…...
对于邮箱地址而言,短中划线(Hyphen, -)和长中划线(Em dash, —)有区别吗
对于邮箱地址而言,**短中划线(Hyphen, -)和长中划线(Em dash, —)**有明确的区别: 短中划线(Hyphen, -): 在邮箱地址中,短中划线是可以使用的,通常…...
C++ STL(三)list
目录 list是什么 构造函数 元素访问 容量操作 修改 迭代器 code实例 实现简单的list forward_list是什么 构造函数 元素访问 容量 修改 迭代器 code实例 实现一个简单的forward_list list是什么 std::list 是 C 标准模板库(STL)中的一个…...
Vue3+TypeScript 封装一个好用的防抖节流自定义指令
一、前言:为什么需要防抖节流? 在前端开发中,高频触发的事件(如滚动、输入、点击等)容易导致性能问题。防抖(debounce) 和 节流(throttle) 是两种常用的优化手段&#x…...
HarmonyOS+Django实现图片上传
话不多说,直接看代码: HarmonyOS部分代码 import { router } from "kit.ArkUI" import PreferencesUtil from "../utils/PreferencesUtil" import { photoAccessHelper } from "kit.MediaLibraryKit" import fs from oh…...
vscode 版本
vscode官网 Visual Studio Code - Code Editing. Redefined 但是官网只提供最新 在之前的版本就要去github找了 https://github.com/microsoft/vscode/releases 获取旧版本vscode安装包的方法_vscode 老版本-CSDN博客...
Admin.Net中的消息通信SignalR解释
定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...

Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅!
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅! 🌱 前言:一棵树的浪漫,从数组开始说起 程序员的世界里,数组是最常见的基本结构之一,几乎每种语言、每种算法都少不了它。可你有没有想过,一组看似“线性排列”的有序数组,竟然可以**“长”成一棵平衡的二…...

【java面试】微服务篇
【java面试】微服务篇 一、总体框架二、Springcloud(一)Springcloud五大组件(二)服务注册和发现1、Eureka2、Nacos (三)负载均衡1、Ribbon负载均衡流程2、Ribbon负载均衡策略3、自定义负载均衡策略4、总结 …...

Qt的学习(二)
1. 创建Hello Word 两种方式,实现helloworld: 1.通过图形化的方式,在界面上创建出一个控件,显示helloworld 2.通过纯代码的方式,通过编写代码,在界面上创建控件, 显示hello world; …...