【JavaWeb | 第十二篇】项目实战——登录功能

目录

一、登录功能的实现

[1.1 请求和响应数据](#1.1 请求和响应数据)

[1.2 代码实现](#1.2 代码实现)

二、登录校验

[2.1 会话技术](#2.1 会话技术)

[2.2 会话跟踪技术------Cookie](#2.2 会话跟踪技术——Cookie)

[2.3 会话跟踪技术------Session](#2.3 会话跟踪技术——Session)

[2.4 会话跟踪技术------令牌](#2.4 会话跟踪技术——令牌)

三、JWT令牌技术

[3.1 JWT的核心结构](#3.1 JWT的核心结构)

四、令牌校验

[4.1 Filter(过滤器)令牌校验](#4.1 Filter(过滤器)令牌校验)

[4.1.1 场景实现](#4.1.1 场景实现)

[4.2 Interceptor(拦截器)令牌校验](#4.2 Interceptor(拦截器)令牌校验)

[4.2.1 拦截器的三个核心工作时机](#4.2.1 拦截器的三个核心工作时机)

[4.2.2 流程](#4.2.2 流程)

五、Filter与Interceptor对比


一、登录功能的实现

1.1 请求和响应数据

请求数据:

响应数据:

1.2 代码实现

Controller层:

对于登录功能映射到了/login地址,因此我们新创建一个LoginController类

Controller层的逻辑:

将前端的请求数据发给Service层,Service层返回info结果,如果该用户存在则登录成功,如果用户不存在那么Service层返回的info为空,那么登录失败。

java 复制代码
@Slf4j
@RestController
public class LoginController {

    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){
        log.info("登录员工: {}", emp);
        LoginInfo info = empService.login(emp);
        if(info != null){
            return Result.success(info);
        }else
            return Result.error("用户名或密码错误!");
    }
}

Service层:

java 复制代码
    @Override
    public LoginInfo login(Emp emp) {
        Emp e = empMapper.selectByUsernameAndPassword(emp);
        if(e != null){
            return new LoginInfo(e.getId(), e.getUsername(), e.getName(), "");
        }
        return null;
    }

Mapper层:

Mapper层主要是进行数据库的操作,因此起名时要见名知意。

java 复制代码
    @Select("select id, username, name from tilas.emp where username = #{username} and password = #{password}")
    Emp selectByUsernameAndPassword(Emp emp);

实现上述代码后,我们发现将登录成功后的网址复制到一个新的页面上,无需登录就可以进行数据表的修改,我们只在前端页面上实现了该登录逻辑,但是并没有真正的实现登录的校验功能。

二、登录校验

登录校验的思路:

在用户登录时,存入一个登录标记,在执行各种操作时判断这个用户的登录标记是否存在;但是每个接口都要判断一次登录标记,在工程上是很复杂的,因此引入一个统一拦截器,浏览器发送请求后由统一拦截器拦截,取出登录标记进行判断。如果成功取出,那么就将这个请求放行,去访问对应的接口。

登录标记:用户登录成功之后,在后续的每一次请求中,都可以获取到该标记。[会话技术】

统一拦截:过滤器Filter、拦截器Interceptor

2.1 会话技术

会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

会话跟踪方案:

  • 客户端会话跟踪技术:Cookie
  • 服务端会话跟踪技术:Session
  • 令牌技术

Cookie 是最早出现的会话跟踪技术,它的核心思想是"把凭证发给客户端,让客户端每次请求都带着"。

  • 工作原理:

    1. 客户端发起第一次请求(比如登录)。

    2. 服务器响应时,在 HTTP 响应头中添加 Set-Cookie 字段,把一些数据(如用户标识)发送给浏览器。

    3. 浏览器接收到 Cookie 后,会自动将其保存在本地。

    4. 之后,浏览器每次向同一个域名发送请求时,都会自动在 HTTP 请求头中带上 Cookie 字段,服务器通过解析 Cookie 就能认出你是谁。

  • 优点:

    • 实现简单,浏览器自动管理。

    • 服务器不需要存储状态数据,减轻了服务器内存压力。

  • 缺点:

    • 不安全: 数据明文保存在客户端,容易被截获或篡改(容易遭受 XSS 和 CSRF 攻击)。

    • 大小受限: 单个 Cookie 的大小通常被限制在 4KB 左右。

    • 性能损耗: 每次请求都会携带所在域名下的所有 Cookie,增加网络带宽消耗。

2.3 会话跟踪技术------Session

为了解决 Cookie 不安全的问题,Session 技术诞生了。它的核心思想是"数据存在服务器,只给客户端发一把钥匙"。

  • 工作原理:

    1. 用户登录成功后,服务器会在内存中创建一个 Session 对象,用来存储该用户的敏感数据,并生成一个唯一的 Session ID

    2. 服务器将这个 Session ID 通过 Cookie(通常叫 JSESSIONIDPHPSESSID)发送给浏览器。

    3. 浏览器下次请求时,带着包含 Session ID 的 Cookie 过来。

    4. 服务器拿到 Session ID 后,去内存里找到对应的 Session 对象,从而获取用户状态。

  • 优点:

    • 安全性高: 敏感数据存储在服务器端,客户端只保存一个无意义的 ID。

    • 没有大小限制: 可以存储复杂的对象。

  • 缺点:

    • 服务器压力大: 如果用户量非常大,服务器内存会被大量的 Session 对象占满。

    • 分布式扩展困难: 在多台服务器集群的环境下,如果用户的第一次请求打在 A 机器(Session 在 A 上),第二次请求打在 B 机器,B 机器上没有该用户的 Session,用户就会被迫重新登录。解决这个问题需要引入"Session 共享"(如使用 Redis),增加了系统架构的复杂度。

2.4 会话跟踪技术------令牌

随着移动端 App 的兴起和微服务/分布式架构的普及,Session 的局限性暴露无遗(App 不支持原生 Cookie,分布式 Session 共享复杂)。于是,Token(最常见的是 JWT - JSON Web Token)成为了主流。

它的核心思想是"服务器不保存状态,全靠密码学签名防篡改"。

  • 工作原理:

    1. 用户登录成功后,服务器根据用户信息(如 UserID、角色等)生成一个 JSON 字符串,并使用服务器侧的"密钥"对其进行加密签名,最终生成一串长字符串(这就是 Token)。

    2. 服务器把 Token 发给客户端(不再强制要求存在 Cookie 里,客户端可以存放在 LocalStorage 甚至内存里)。

    3. 客户端后续发请求时,通常将 Token 放在 HTTP 请求头的 Authorization 字段中。

    4. 服务器收到请求后,不需要去内存里查数据,只需要用自己的"密钥"对 Token 的签名进行验证。如果签名合法且没过期,就信任该 Token 携带的用户信息。

  • 优点:

    • 彻底无状态: 服务器不需要存储任何 Session,极大地节省了服务器资源,天生支持分布式集群。

    • 跨端支持好: 不依赖 Cookie,可以完美支持手机 App、小程序等跨平台应用。

    • 避免 CSRF 攻击: 因为不需要浏览器自动发送 Cookie,防御了跨站请求伪造攻击。

  • 缺点:

    • 一旦签发,极难废弃: Token 在过期前始终有效,如果用户中途修改密码或被封号,很难立刻让旧 Token 失效(除非服务器引入黑名单机制,但这又变回了有状态)。

    • 体积较大: 包含了用户信息和较长的签名,每次请求携带会消耗一定带宽。

三、JWT令牌技术

JSON Web Token (JWT) 是一种目前非常流行、用于跨域和无状态身份验证的开放标准

3.1 JWT的核心结构

如果你在浏览器控制台或网络请求里抓过包,你看到的 JWT 往往是一长串毫无规律的乱码,例如: xxxxxx.yyyyyy.zzzzzz

仔细看,它由两个英文句号 . 分隔成了三个部分:

  • 第一部分:Header(头部) 记录了令牌的类型(通常是 JWT)以及签名使用的加密算法(如 HS256RS256)。这部分会被 Base64Url 编码。

  • 第二部分:Payload(负载/载荷) 这是 JWT 的核心,用来存放实际需要传递的数据(称为 Claims 声明)。比如用户的 ID、角色、签发时间(iat)、过期时间(exp)等。这部分同样会被 Base64Url 编码。 (注意:Payload 是明文编码,可以被任何人解码,因此千万不要在里面存放密码等敏感信息!)

  • 第三部分:Signature(签名) 这是防止令牌被篡改的关键。服务器会将编码后的 Header、编码后的 Payload,加上一个只有服务器自己知道的密钥(Secret),使用 Header 中指定的算法进行加密运算,生成签名。

加密代码实现:

java 复制代码
    public void testGenerateJwt(){
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("id", 1);
        dataMap.put("username", "admin");
        String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "c3BybmVmdA==")//指定加密算法,密钥
                .addClaims(dataMap)//添加自定义信息
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))//设置过期时间
                .compact();//生成令牌
        System.out.println(jwt);
    }

得到的jwt令牌:

java 复制代码
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc3OTAwMTg1M30.abcewY65PS145Y_naxlNkadh-fNyNoENsyfClUWlprM

解密代码实现:

java 复制代码
    public void testParseJwt(){
        String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc3OTAwMTg1M30.abcewY65PS145Y_naxlNkadh-fNyNoENsyfClUWlprM";
        Claims claims = Jwts.parser()
                .setSigningKey("c3BybmVmdA==")//指定密钥
                .parseClaimsJws(jwt)//解析JWT
                .getBody();//获取自定义信息
        System.out.println(claims);
    }

解密后得到的body数据:

四、令牌校验

在上一章节中,我们实现了如何生成用于验证身份的jwt令牌,接下来我们要实现拦截令牌并验证令牌的有效性。

4.1 Filter(过滤器)令牌校验

Filter 是 Java Web(Servlet 规范)提供的一种技术。

当客户端发起 HTTP 请求到达服务器,但在真正到达你的 Controller(业务代码)之前,会先经过 Filter。同样的,当 Controller 处理完准备返回响应给客户端时,也会再次经过 Filter。

利用这个特性,我们可以把校验令牌(Token)的逻辑写在 Filter 里,实现统一拦截

快速入门程序:

1.定义Filter:定义一个类,实现 Filter 接口,并实现其所有方法。

2.配置Filter:Filter类上加@WebFilter注解,配置拦截路径。引导类上加 @ServletComponentScan开启Servlet组件支持。

实现代码:

java 复制代码
@Slf4j
@WebFilter(urlPatterns = "/*") // 拦截所有请求
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("init 初始化方法");
    }

    //拦截到请求执行,会执行多次
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("doFilter 拦截到请求执行");
    }

    @Override
    public void destroy() {
        log.info("destroy 销毁方法");
    }
}

引导类注解:

每一次请求都会被Filter拦截:

拦截后要进行放行操作:

4.1.1 场景实现

java 复制代码
@WebFilter(urlPatterns = "/*")
@Slf4j
public class JwtFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //1.获取请求路径,uri
        String uri = request.getRequestURI();

        //2.判断请求路径是否需要处理,如果路径包含/login,则直接放行
        if(uri.contains("/login")){
            log.info("登录请求,放行");
            chain.doFilter(request, response);
            return;
        }
        //3.获取请求头中的令牌
        String token = request.getHeader("token");

        //4.判断令牌是否存在,不存在说明用户没有登录,返回401状态码
        if(token == null){
            log.info("令牌不存在,返回401状态码");
            response.setStatus(401);
            return;
        }
        //5.判断令牌是否有效,如果无效,则返回401状态码
        try {
            JwtUtils.parseJwt(token);
        }catch (Exception e){
            log.info("令牌无效,返回401状态码");
            response.setStatus(401);
            return;
        }
        log.info("令牌有效,放行");
        chain.doFilter(request, response);
    }
}

4.2 Interceptor(拦截器)令牌校验

Interceptor 是 Spring MVC 框架 自带的技术。它专门用来拦截发送到 Controller(控制器) 的请求。

4.2.1 拦截器的三个核心工作时机

实现一个拦截器,通常需要实现 HandlerInterceptor 接口。这个接口里面有三个非常重要的方法:

  1. preHandle(前置处理): 最常用,在请求到达 Controller 之前执行。

    • 作用: 校验 JWT 令牌、检查权限。

    • 返回值: 返回 true 表示放行;返回 false 表示拦截,请求直接打回,不会走到 Controller。

  2. postHandle(后置处理): 在 Controller 执行完毕,但在视图渲染之前执行。(在目前前后端分离的 JSON 接口开发中,较少使用)。

  3. afterCompletion(完成之后): 整个请求处理完毕(连数据都返回给前端了)之后执行。

    • 作用: 清理资源、记录极其详细的操作日志、处理异常。

4.2.2 流程

1.定义拦截器,实现HandlerInterceptor接口,并实现其所有方法。

java 复制代码
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion");
    }
}

2.注册拦截器

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private DemoInterceptor demoInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
    }
}

五、Filter与Interceptor对比

维度 Filter(过滤器) Interceptor(拦截器)
技术出身 Servlet 规范(Java EE 标准,Tomcat 提供支持) Spring 框架(Spring MVC 特有组件)
拦截范围 大。 拦截所有的进入 Tomcat 的请求(包括静态资源 html、css、图片等)。 小。 只能拦截发往 Spring Controller 的请求。
上下文感知 弱。 它只知道 HTTP 请求和响应,不知道 Spring 内部的事。 强(核心优势)。 它的 preHandle 方法里有个参数叫 Object handler,这个对象其实就是你要访问的 Controller 方法。你可以通过反射拿到这个方法上的自定义注解 (比如 @RequireAdmin)。
触发时机 在请求进入 Servlet 前触发。 在请求进入 Servlet 后,到达 Controller 前触发。
相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
如何保持nginx配置与前端打包dist的路径保持一致、解决页面刷新白屏以及页面跳转问题
运维·前端·nginx
想唱rap1 小时前
五种IO模型和非阻塞IO
linux·运维·服务器·网络·数据库·tcp/ip
m0_733565461 小时前
如何指定PHP版本运行phpMyAdmin_多版本共存配置
jvm·数据库·python
喜欢小苹果的码农1 小时前
Java动态定时任务
java
xcLeigh2 小时前
IoTDB JDBC 完整使用教程:连接、查询、批处理与字符集配置
开发语言·数据库·qt·iotdb·查询·批处理·连接
chunyublog2 小时前
数据挖掘环境搭建
数据库
haiyangyiba2 小时前
修改jar包中class的包路径
java·jar·修改class·修改class中包路径
阿洛学长2 小时前
CSDN、掘金、简书博客文章如何转为Markdown?
运维·数据库·架构·php·持续部署
zuozewei2 小时前
国产化之达梦数据库性能优化方案
数据库·性能优化