登录
基础登录功能
登录校验用户名和密码是否正确的本质就是查询表中是否有用户名和数据对应的结果,如果查询到了就登录成功,反之则登录失败。
创建一个新的 controller 类 LoginController:
import com.example.demo.pojo.Emp;
import com.example.demo.responed.Result;
import com.example.demo.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
Emp e = empService.login(emp);
return e != null?Result.success():Result.error("用户名或密码错误");
}
}
在 service 接口 EmpService 中添加关于登录的方法:
Emp login(Emp emp);
在 service 实现类 EmpServiceImpl 中重写方法:
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
在 mapper 接口中写查询用户名和密码的方法:
// 根据用户名和密码查询员工
@Select("select * from emp where username=#{username} and password=#{password}")
Emp getByUsernameAndPassword(Emp emp);
如此一来就实现了基本的登录逻辑
在 Postman 中进行测试:
登录认证校验
概述
目前登录功能存在一个问题,在未登录的情况下,也能直接访问系统的功能,只是徒有其表的,为了解决这个问题,需要进行登录校验的操作。
登录校验的大致流程如下图所示:
角色与交互:
浏览器:用户操作入口,发起请求(如访问系统页面、接口),接收并展示服务端响应。
Web 服务器:处理请求、维护登录状态,包含 “统一拦截”“业务接口(login/depts/emps 等)”“登录标记存储” 。
流程逻辑:
请求发起:浏览器向 Web 服务器发请求(比如访问员工列表
emps
、部门数据depts
,或登录login
)。统一拦截:服务器先通过过滤器(Filter)或拦截器(Interceptor) 统一拦截请求,判断是否需校验登录。
登录标记的 “存” 与 “取”:
- 存:用户登录
login
成功时,服务器生成登录标记(如 Session、Token ,靠 “会话技术” 实现),存到指定存储区。 - 取:访问
depts
、emps
等需登录的接口时,拦截器会 “取” 登录标记,校验是否有效。
- 存:用户登录
响应返回:校验通过则继续处理请求、返回数据;不通过(无标记或标记失效),可能拒绝访问、返回错误,浏览器接收响应后展示结果(如跳转登录页、提示未登录 )。
关键技术点:
统一拦截:用 Filter/Interceptor 提前拦截请求,集中处理登录校验,减少重复代码。
登录标记:靠会话技术(如 HttpSession、JWT )实现,标记用户登录状态,让服务器识别 “该请求是否来自已登录用户”。
会话技术:保障登录标记能在多次请求间传递、存储,维持用户状态(比如登录后,多页面跳转仍保持登录)。
会话技术
在 Web 开发中,会话指浏览器与服务器之间的一次连接,从用户打开浏览器首次访问服务器时建立,直到任一方断开连接时结束。一次会话可包含多次请求和响应,例如打开浏览器后进行登录、查询部门数据、查询员工数据等操作,只要浏览器和服务器未关闭,这些请求都在同一会话中。
会话跟踪是一种维护浏览器状态的方法,服务器通过它识别多次请求是否来自同一浏览器,以便在同一次会话的多次请求间共享数据。
在一次会话中共享数据很重要,例如登录后在后续操作中获取当前登录用户名,以及登录时验证码的生成与验证,都需要在会话的多次请求间共享数据。
浏览器与服务器交互使用的 HTTP 协议是无状态的,即下一次请求不会携带上一次请求的数据,导致同一会话的多次请求无法共享数据,因此需要会话跟踪技术。
会话跟踪技术包括三种:
- 分别是客户端会话跟踪技术 Cookie(存储在客户端浏览器)
- 服务器端会话跟踪技术 Session(存储在服务器)
- 当前企业开发中主流的令牌技术 JWT。
Cookie
Cookie 存储在客户端浏览器,首次请求(如登录接口)时服务器设置 Cookie,存储用户名、用户 ID 等信息,服务器通过响应头 Set-Cookie 将其返回给浏览器,浏览器自动存储,后续请求通过请求头 Cookie 自动携带到服务器,实现同一会话不同请求间的数据共享。
Cookie 是 HTTP 协议支持的技术,浏览器厂商遵循标准,通过响应头 Set-Cookie 和请求头 Cookie 实现自动响应、存储和携带。
以下将通过代码来演示:
import com.example.demo.responed.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
public class SessionController {
// 设置Cookie
@GetMapping("/setCookie")
public Result cookie1(HttpServletResponse response) {
response.addCookie(new Cookie("LoginUserName", "exampleName"));
return Result.success();
}
// 获取Cookie
@GetMapping("/getCookie")
public Result cookie2(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("LoginUserName")) {
return Result.success("LoginUserName: " + cookie.getValue());
}
}
return Result.error("未找到该Cookie");
}
}
在浏览器中访问 http://localhost:8080/setCookie,在浏览器控制台的 Network 选项中找到 Response Headers:
响应头出现 Set-Cookie,Cookie 存储在浏览器本地
在浏览器中访问 http://localhost:8080/getCookie,在浏览器控制台的 Network 选项中找到 Request Headers:
请求头携带 Cookie,服务器控制台成功获取值,验证了 Cookie 在多次请求间共享数据的功能
Cookie 的优缺点:
- 优点:是 HTTP 协议官方支持的技术,Set-Cookie 响应头解析和 Cookie 请求头数据携带由浏览器自动进行,无需手动操作。
- 缺点:
- 移动端 APP 无法使用。
- 存储在客户端浏览器,数据不安全,且用户可禁用浏览器 Cookie 导致方案失效。
- 不能跨域,跨域指协议、IP 地址或域名、端口号任一不同的情况,此时 Cookie 无法使用。
Session
session 是服务器端会话跟踪技术,存储在服务器端,底层基于 cookie 实现。浏览器第一次请求时,服务器创建 session 并生成 session id,通过 cookie 响应给浏览器,后续请求浏览器携带该 cookie,服务器据此找到对应 session 实现数据共享。
演示代码:
import com.example.demo.responed.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SessionController {
// 设置Session
@GetMapping("/setSession")
public Result session1(HttpServletRequest session) {
session.setAttribute("LoginUserName", "exampleName");
return Result.success();
}
// 获取Session
@GetMapping("/getSession")
public Result session2(HttpServletRequest request) {
HttpSession session = request.getSession();
Object loginUserName = session.getAttribute("LoginUserName");
return Result.success(loginUserName);
}
}
Session 的优缺点:
- 优点:数据存储在服务器端,较安全。
- 缺点:在服务器集群环境下无法直接使用,且存在 cookie 的所有缺点。
JWT 令牌
令牌是用户身份标识,本质是字符串。登录成功后服务器生成令牌响应给前端,前端存储令牌,后续请求携带令牌到服务端,服务端校验令牌有效性实现会话跟踪。
JWT 的优缺点:
- 优点:支持 PC 端和移动端,解决集群环境下的认证问题,减轻服务器存储压力。
- 缺点:需要自行实现令牌的生成、存储、携带等操作,且需前端配合
JWT 令牌
介绍
JWT 全称是 JSON Web Token,定义了一种简洁的自包含的数据格式,用于在通信双方以 JSON 数据格式安全传输信息,因数字签名存在而可靠,本质是对原始 JSON 数据的安全封装。
JWT 具有简洁和自包含的特点。简洁指其是简单字符串,可在请求参数或请求头中传递;自包含指能根据需要存储自定义内容,如用户相关信息。
JWT 令牌由三个部分组成,用两个英文点分割:
- 第一部分是头部区域:记录令牌类型和签名算法,数据格式为 JSON,生成时会进行 Base64 编码。
- 第二部分是 payload(有效载荷):可携带自定义信息和默认信息(如令牌签发日期、有效期等),原始数据为 JSON 格式,也经过 Base64 编码。
- 第三部分是 signature(签名):基于头部指定的签名算法,融入 header 和 payload 部分内容及指定密钥计算得出,目的是防止令牌被篡改,确保安全性,并非 Base64 编码。
Base64 编码是一种基于 64 个可打印字符表示二进制数据的编码方式,非加密方式,可解码。64 个字符包括大 A 到 Z、小 a 到 z、0 到 9 及加号、斜杠,等号是补位符号。
JWT 令牌的应用场景最典型的是登录认证。流程为:浏览器发起登录请求,登录成功后服务端生成 JWT 令牌返回给前端,前端存储令牌,后续每次请求都携带令牌,服务端统一拦截请求,判断是否携带令牌及令牌是否有效,有效则放行处理请求。
令牌生成与校验
在项目的 pom.xml 配置文件中引入 JWT 依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
如果是 JDK 8 以上的版本还需要引入以下依赖:
<!-- JAXB API -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- JAXB 实现 -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
<scope>runtime</scope>
</dependency>
<!-- 激活框架(JAXB 依赖) -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
在测试类中编写生成 JWT 的测试代码:
// 生成JWT
@Test
public void testGenerateJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","jack");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256,"Scarletkite")// 签名算法
.setClaims(claims) // 存放在JWT中的数据
.setExpiration(new Date(System.currentTimeMillis()+1000*60*60*24))// 有效期
.compact();
System.out.println(jwt);
}
控制台中就会生成令牌:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzU0NzUxOTY0LCJ1c2VybmFtZSI6ImphY2sifQ.F_ti_qQayc1FZ9GBLFicOcDHPDnCAM1oetTuEd8orOc
然后将 JWT 令牌粘贴到 JWT 官网 就能获取到令牌中的信息:
接下来编写解析 JWT 的测试代码:
// 解析JWT
@Test
public void testParseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("Scarletkite")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9." +
"eyJpZCI6MSwiZXhwIjoxNzU0NzUxOTY0LCJ1c2VybmFtZSI6ImphY2sifQ." +
"F_ti_qQayc1FZ9GBLFicOcDHPDnCAM1oetTuEd8orOc")
.getBody();
System.out.println(claims);
}
运行测试结果为:
{id=1, exp=1754751964, username=jack}
篡改 JWT 令牌的任意位置字符后进行解析,会出现报错,说明 JWT 令牌具有防篡改特性。
校验时密钥需与生成时一致,令牌被篡改或过期都会导致解析报错,报错则说明令牌非法。
登录加入 JWT
引入 JWT 令牌操作工具类:
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 = "Scarletkite";
private static Long expire = 43200000L;
// 生成JWT
public static String generateJwt(Map<String, Object> claims){
return Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
}
// 解析JWT
public static Claims parseJwt(String jwt){
return Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
}
}
修改 LoginController:
import com.example.demo.pojo.Emp;
import com.example.demo.responed.Result;
import com.example.demo.service.EmpService;
import com.example.demo.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
Emp e = empService.login(emp);
// 登录成功,生成token
if (e != null) {
Map<String, Object> claims = Map.of("id", e.getId(),
"name", e.getName(),
"username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
// 登录失败
return Result.error("用户名或密码错误");
}
}
在 Postman 中做测试: