tlias智能学习辅助系统——登录功能

        登录功能

        依据前几篇文章,我们已经开发了部门管理和员工管理的功能,那么我们想一下,项目如果上线了的话,是否安全呢?

        答案是一定不安全的,所以今天,我们来优化此案例,开发登录认证的功能。那么什么是认证呢?所谓认证就是根据用户的用户名和密码来进行校验的过程。认证成功之后,我们才可以访问系统中的信息,否则拒绝访问。

        基础登录功能

        首先第一步,我们要先完成基础登录的功能,就是来判断用户输入的用户名或者密码是否正确。我们先来看一下登录功能具体的需求,首先,登录时需要输入员工的用户名和密码,即为员工的信息,同时也说明登录时我们要操作的表即为emp。用户名即为username,密码为password这两个字段。下面我们来考虑一下这个sql语句怎么写?其实就是根据用户名和密码查询员工,如果根据用户名和密码我查询到了员工,就说明用户名和密码是正确的,如果根据用户名和密码没有查询到员工,就说明用户名或密码错误。现在,我们来编写一下sql语句

select * from emp where username = 'zhangwuji' and password = '123456';

        因为我们针对username添加了unique唯一约束,所以在查询的时候不会重复。最终查询的数据只会有一条。现在,我们来看一下接口文档

        

根据接口文档,接下来我们分析一下登录具体的实现思路

        我们需要在controller这个接口中定义一个方法,在这个方法上我们需要加一个注解,也就是@postmapping,且请求的参数是一个json格式的参数,最终在服务端,我们要将其封装到一个对象中,这时我们就需要使用注解@ReuquestBody,我们只需要创建一个logincontroller,而service和mapper我们直接写在员工里面即可,即empservice和empmapper,在empservice中调用mapper接口的方法,根据用户名和密码来查询员工信息,再将查询到的员工信息返回给service,service再返回给controller,controller需要根据返回回来的员工信息进行判断,判断员工是否存在,如果存在,controller再返回给前端一个成功的结果,代表成功,如果不存在,那么直接返回一个登录失败的信息。

        接下来我们在idea来完成登录这个功能,依旧是三层代码实现操作

首先是controller层

package com.ittaotao.controller;

import com.ittaotao.pojo.Emp;
import com.ittaotao.pojo.Result;
import com.ittaotao.service.EmpService;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RestController
public class LoginController {
    @Autowired
    private EmpService empService;

    @PostMapping("/login")
    public Result login(@RequestBody Emp emp){ //注解作用就是将json格式的数据封装到这个实体类中
        log.info("员工登录:{}",emp);
        Emp e = empService.login(emp);
        return e != null?Result.success() :Result.error("用户名或密码错误");
    }
}

        然后是service以及对应的impl实现层

/**
     * 员工登录
     * @param emp
     * @return
     */
    Emp login(Emp emp);

@Override
    public Emp login(Emp emp) {
        return empMapper.getByUsernameAndPassword(emp);
    }

        最后是mapper层接口

/**
     * 根据用户名和密码查询员工
     * @param emp
     * @return
     */
    @Select("select * from emp where username = #{username} and password = #{password};")
    Emp getByUsernameAndPassword(Emp emp);

最后,我们进行postman测试

最后,我们进行前后端联调就完成了基础的登录功能。

        登录校验功能

        第二步,我们就要完成登录校验的操作

        上一小节,我们已经完成了登录的操作,那么我们现在复制一下登录进去的地址,重新进行访问看一看会是什么情况。

        可以发现,我们依旧可以访问进去,这次我们并没有登录。即在未登录情况下,我们也可以直接访问员工管理、部门管理等功能。这是一个异常现象

        正常情况下,我们退出之后,必须重新登录才能访问,这才是一个正常的流程。那么为什么会出现这个问题呢?那是因为我们目前开发的功能都是正常的来操作数据库的数据,完成数据的增删改查,并没有判断用户是否登录。即无论用户是否登录,都可以正常的访问员工管理、部门管理这些功能。

        要想解决这一问题,就要完成我们的第二功能——登录校验。登录校验即为当我们发起一个请求之后,服务端要去判断这个用户是否登录,如果登录则执行正常的业务操作。如果没有登录,则需跳转登录页面,让其完成登录再来访问系统。登录校验也是我们的重点。

        接下来,我们来分析一下登录校验大概的实现思路,首先,我们先来思考一下,http协议是无状态的。所谓无状态,即每一次请求都是独立的,下一次请求不会携带上一次请求的数据。而浏览器与服务器之间进行交互就是通过http协议,那也就是说登录之后我要进行别的功能,浏览器重新请求,服务器无法判断员工是否登录,那我们能想到的如何判断呢?加if条件去一个一个判断?这样太繁琐了。为了简化这一操作,我们就可以通过统一拦截来拦截浏览器发送过来的统一请求。我们拦截到了之后,取回之前存储的登录标记进行对比,如果没有问题,则表示已经登录,那么就放行执行其他功能。如果存在问题,那么我们可以响应一个错误信息给前端,让前端跳转到登录界面。

        执行上面操作,我们要涉及到两个部分,一个是登录标记,登录标记要求登录成功之后,每一次请求中,都可以获取到该标记。这个操作将涉及到web开发的会话技术。另一方面,则是实行统一拦截,即有两种方式,第一种servlet中提供的过滤器filter,还有一种就是spring当中提供的拦截器interceptor。那么我们登录校验就分为四部分来讲,分别是传统的会话技术、当前项目主流的方案JWT令牌技术,最后两种统一拦截的技术,过滤器filter以及拦截器Interceptor

会话技术

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

        我们可以分析到,每一次会话都代表了一个浏览器与服务器进行连接,不过你发起了多少请求,只要是一个浏览器,那就只是一个会话。

        接下来,我们来介绍一下会话跟踪,会话跟踪,即为一种维护浏览器状态的方法,服务器需要识别多次请求是否来自同一浏览器,以便在同一次会话的多次请求间共享数据。如果服务器发现多个请求来自同一浏览器,那么我们即可共享数据。

会话跟踪方案,我们会讲解三种

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

        首先是cookie,cookie是客户端会话跟踪技术,他是存储在客户端(浏览器)当中的,我们使用cookie来跟踪会话,我们就可以存储相关的一些属性信息。首先,服务器会自动的将cookie响应给浏览器,浏览器接收到回来的响应数据之后,会自动的将cookie存储在浏览器本地,在后续的请求当中,浏览器会自动的将cookie携带到服务器端。

cookie:请求头是作为http请求报头包含存储先前通过与所述服务器发送的HTTP cookies,即给服务端传送cookie数据。

set-cookie:响应头:HTTP响应报头被用于从服务器向用户代理发送cookie

接下来我们用代码演示一下,首先,创建一个sessioncontroller

        

package com.ittaotao.controller;

import com.ittaotao.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@RestController
public class SessionController {

    //设置cookie
    @GetMapping("/c1")
    public Result cookie(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","ittaotao"));
        return Result.success();
    }

    //获取cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie:cookies){
            if (cookie.getName().equals("login_username")){ //输出name为login_username的cookie
                System.out.println("login_username:"+ cookie.getValue());
            }
        }
        return Result.success();
    }
}

运行此服务浏览器输入localhost:8080/c1,打开开发者工具,可以发现确实存在一个set-cookie

        接下来我们再次打开一个窗口运行c2,c2只有一个请求的cookie:ittaotao,即访问c2时,通过请求头携带到服务端,同时我们看到控制台也把login_username输出出来了。

最后,我们说一下cookie这种会话技术的优缺点。cookie是http协议中支持的技术,是官方提供的,无序手动操作,但是cookie在移动端无法使用,也就是只有浏览器支持cookie,安卓端和ios端都不支持cookie,其次cookie不是很安全,所以我们不能存放敏感、隐私的数据,其用户可以自己禁用cookie。如果用户禁用了cookie,那么cookie也就无法使用了。最后,cookie不可以跨域,也就是说我们目前主流的前后端分离开发,前后端各占一个服务器,当我们的浏览器访问完前端之后再次访问后端,由于我们当前所处的地址以及我们要请求的地址,三个维度(协议、IP/域名、端口)当中有任何一个不同,那就是跨域操作。如果跨域请求的话,cookie就不能用了。

        session

        上面介绍了我们的cookie操作,接下来我们来介绍第二种会话跟踪方案——session,session是服务器端会话存储技术,即存储在服务器端。而session底层就是基于我们刚才学到的cookie来实现的,如果说我们现在要基于session进行会话跟踪,那浏览器第一次请求服务器,服务器就可以获取到了session,而每一个会话对象都有一个id,即sessionId,那接下来,服务器端响应会将这个sessionId以setcookie的方式响应给浏览器。浏览器接收到sessionId会自动存储到本地,接下来每次请求都会携带过去,服务器拿到之后,会根据sessionId来从众多的session对象中找到当前的请求对象session

接下来我们进行演示


    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession-s1:{}",session.hashCode());

        session.setAttribute("loginUser","tom");  //往session中存储数据
        return Result.success();
    }

    //从HttpSession中获取值
    @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);
    }

        运行服务,我们先访问s1,可以看到一个响应头set-cookie,里面包含一个sessioniD,接着我们看一下application,可以发现里面多了一个值为JSESSIONID。紧接着,我们访问s2,可以发现,我们请求头里包含的cookie有JSESSIONID,那么我们就可以根据这个id找到对应session对象。

        经过测试,我们可以发现它的底层就是基于cookie的,那么最后,我们来说一下这种方案的优缺点,首先,session的数据都存储在服务器端,所以十分安全,然后,我们来说一下它的缺点,在pc互联网以及移动互联网高速发展的今天,session已经不再适用,首先第一点,我们现在所开发的项目一般都不会部署到一台服务器上,因为会出现单点故障的问题(一旦服务器挂掉,整个应用则无法使用),所以现在开发的项目都是以集群部署,也就是说同一个项目会部署多份,让其负载均衡。同时造成的缺点也就是无法找到session。其实,剩余的缺点都是cookie的缺点。

        令牌技术

        因为上面两种方案在企业开发中都会存在一堆问题,因此在现在的企业开发中,都会采用令牌技术的方案来解决会话跟踪。其实令牌技术,其本质就是一个字符串,在浏览器请求登录的时候,如果登录成功,那就生成一个令牌,即此令牌就是该用户的合法身份凭证。接下来响应数据时,就可以把这个令牌响应给前端,在前端程序当中接收到该令牌就可以将该令牌存储起来。接下来,在后续的请求当中,都需要将这个令牌携带到服务端,携带过去之后,我们就需要来校验这个令牌的有效性,如果这个令牌有效,那就说明用户之前已经执行了登录操作,如果这个令牌无效,那就说明用户之前并没有执行登陆这个操作,如果几次请求之间我们想要共享数据,那么我们就可以把想要存储的数据存储到这个令牌当中。

        令牌技术的好处,它既支持pc端,又支持移动端,同时,他可以解决集群环境下的认证问题,因为我在服务器端不需要存储任何数据,这样一来,这也减轻了服务器端的存储压力。令牌技术的缺点就是需要我们自己去实现。

        JWT令牌-介绍

        刚才,我们介绍了通过令牌技术来追踪会话,令牌的形式有很多,我们要讲解的是功能强大的JWT令牌。

        JWT令牌(JSON Web Token),定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息,由于数字签名的存在,这些信息是可靠的。其组成形式如下

  • 第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"}
  • 第二部分:Payload(有效载荷),携带一些自定义信息,默认信息等,例如{"id":"1","username":"TOM"}
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

        当我们生成JWT令牌时,要对上面的三部分json格式的数据进行Base64编码(一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式),也就是说任何数据经过Base64编码都会变成对应的那64个字符,当然还有一个补位符"="。最后,我们来介绍一下JWT令牌的应用场景:登录认证

首先,登陆成功后,生成令牌,其次,后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理。

        JWT令牌-生成和校验

        简单介绍了什么事JWT令牌以及JWT令牌的组成,接下来我们讲解如何通过java代码来生成和校验JWT令牌

        首先,我们来讲解JWT令牌的生成,首先,我们要在pop.xml中引入JWT令牌的依赖,引入依赖完成之后,我们就可以调用这个依赖给我们的api,来完成JWT令牌的生成和校验,而无论生成还是校验,都需要利用他给我们生成的工具类jwts,好了,接下来我们进行代码演示

首先,在pop.xml中进行依赖配置

<!-- JWT令牌的依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

然后,我们在测试中进行jwt令牌的编写

/**
     * 生成JWT
     */
    @Test
    public void testGenJWT(){
        Map<String,Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("name","tom");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "ittaotao") //签名算法
                .setClaims(claims) //自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置JWT令牌的有效期为一个小时
                .compact();//拿到字符串类型的返回值(JWT令牌)
        System.out.println(jwt);
    }

启动该测试,可以发现,jwt令牌已经在控制台输出出来了

我们打开jwt的官网,将上面这段字符串复制上去,他会自动帮我们解析出来

接下来,我们在写一个解析JWT令牌的测试代码

**
     * 解析JWT
     */
    @Test
    public void testParseJWT(){
        Claims claims = Jwts.parser()
                .setSigningKey("ittaotao") //给定签名秘钥
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcwNTA1NDY1OH0.BA5qQ_yMhxx1mwchyvEvbH1DRwrU-Djyxt-myhOWtAM")
                //传递JWT令牌
                .getBody();//获取自定义内容

        System.out.println(claims);
    }

接着,运行该测试,控制台已经把解析出来的json数据打印出来了

我们已经拿到了id,name,令牌的过期时间。

        以上就是我们jwt令牌的生成与解析,也就是校验。那么我们在校验时需要注意:JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的,如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或者失效了,令牌非法。

        JWT令牌-案例实现

        接下来,我们要在案例当中利用JWT令牌技术来跟踪会话。其实思路我们前面都已经分析过了,主要就是两步:令牌生成和令牌校验。在登录成功后,生成JWT令牌,并返回给前端,在请求到达服务端后,对令牌进行统一拦截、校验。接下来我们就再看一下接口文档对于登录这一接口的描述

        

        我们主要看一下响应数据,可以看到,data属性对应的就是一个jwt令牌,所以我们只需要把生成的JWT令牌封装在result中,然后返回给前端即可,此时前端就可以接收到这个jwt令牌。接下来,我们再看一下备注说明

        用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为 token ,值为 登录时下发的JWT令牌。

如果检测到用户未登录,则会返回如下固定错误信息:

{
	"code": 0,
	"msg": "NOT_LOGIN",
	"data": null
}

基于以上信息,我们直接打开idea,完成以上操作,我们直接在utils下面创建一个JWTutils.java

package com.ittaotao.utils;

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 = "ittaotao";
    private static Long expire = 43200000L;

    /**
     * 生成JWT令牌
     * @param claims JWT第二部分负载 payload 中存储的内容
     * @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;
    }
}

        接下来,我们对之前写的logincontroller进行一个更改,加上是否登录的判断,并对其添加JWT令牌

@PostMapping("/login")
    public Result login(@RequestBody Emp emp){ //注解作用就是将json格式的数据封装到这个实体类中
        log.info("员工登录:{}",emp);
        Emp e = empService.login(emp);
        
        //登陆成功,生成令牌,下发令牌
        if (e != null){
            Map<String,Object> claims = new HashMap<>();
            claims.put("id",e.getId());
            claims.put("name",e.getName());
            claims.put("username",e.getUsername());

            String jwt = JwtUtils.generateJwt(claims);//jwt当中包含了当前登录的员工信息
            return Result.success(jwt);
        }
        
        //登陆失败,返回错误信息
        return Result.error("用户名或密码错误");
    }

当我们全部编写完成之后,我们就可以利用postman进行测试了

       

        测试完成之后,我们可以发现data中已经返回给了我们JWT令牌了,我们将其复制到jwt官网上,可以发现这个jwt令牌解析出来就是我们员工登录的登录信息,包括id,name和username。最后我们进行前后端联调,即可完成这次的操作

        拦截令牌

刚刚我们讲解了JWT令牌,并且在员工登录后下发了JWT令牌,JWT令牌就是用户成功登陆后的标记,接下来我们将介绍如何进行统一校验JWT令牌,也就是登录成功的标记,接下来我们会讲解两种比较主流的方案,一种是过滤器filter,另一种是拦截器Interceptor

        filter

        首先,过滤器filter,javaweb三大组件(Servlet、Filter、Listener)之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。同时,过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等等。

        此时,当我们有了过滤器,我们就可以把登录校验的功能定义在过滤器filter当中。在filter中校验用户是否登录。如果用户已经登录则放行,让其去访问那些资源,如果没有登录则在filter当中响应错误信息,即不允许其访问对应的资源。

  •         快速入门

        我们现在可以通过一个快速入门程序来掌握filter对应的开发步骤,主要分为两步:1.定义filter:定义一个类,实现filter接口,并重写其所有方法(init、doFilter、destroy);2.配置filter:filter类上加@WebFilter注解,配置拦截资源的路径。引导类上加@SevletComponentScan开启Sevlet组件支持(因为servlet是javaweb三大组件之一,并不是springboot当中的,所以想要使用必须在启动类上加上上方注解)。

        接下来我们直接进行演示,首先,连包带类一起创建DemoFilter.java

package com.ittaotao.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(urlPatterns = "/*") //表示当前过滤器拦截所有请求,/*表示所有请求
public class DemoFilter implements Filter {
    //因为init和destroy都有默认实现,一般可以不写,这里进行演示所以三个方法都进行了重写。
    @Override //初始化方法,只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init初始化方法执行");
    }

    @Override //拦截到请求之后调用,会调用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求");
    }

    @Override //销毁方法,只调用一次
    public void destroy() {
        System.out.println("destroy销毁方法执行");
    }
}

同时,我们要在启动类上加一个注解表示可以支持servlet组件

        我们运行该服务,可以发现init和destroy都只执行了一次,destroy是在服务结束时运行的,而filter每请求一次都会执行一次,在控制台输出拦截到了请求。但是,我们可以发现,当我们点开部门管理时候,发现里面没东西,其实是因为filter只把请求拦截到了,但没有进行其他的处理,也就是没有做任何事情让请求去访问对应的资源,所以才会导致那些资源不被访问到。此时,我们就需要对其进行放行的操作。可以发现,springboot为我们通过了一个api,我们可以调用chain的方法进行放行,下面我们更改代码

@Override //拦截到请求之后调用,会调用多次
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求");
        //放行
        chain.doFilter(request,response);
    }

        此时,我们打印输出完之后就调用dofilter让其放行去访问请求的资源。此时我们重新启动服务,点击部门管理之后会发现数据已经显示出来,说明过滤器已经放行成功了。

  • 详解(执行流程、拦截路径、过滤器链)

        刚才,我们已经了解了filter的快速入门,接下来我们要详细的介绍一下filter过滤器当中的一些细节,我们主要介绍三个过滤器方面的细节,执行流程、拦截路径、过滤器链。

        首先是执行流程,过滤器在放行之前会执行放行前逻辑,也就是dofilter之前的代码,那在放行之后,请求会去访问对应的资源,之后会在回到过滤器中,这个时候我们还可以执行放行后的逻辑,也就是在dofilter之后编写对应的代码。对此,我们可以总结两点:放行后访问对应资源,资源访问完成后,还会回到filter中;其次,如果回到filter中,会直接执行放行后的逻辑而不是重新执行,以上便是过滤器的执行流程。

        接着,我们可以了解一下拦截路径,即@WebFilter注解,Filter可以根据需求,配置不同的拦截资源路径:首先第一种,即拦截具体路径(/login:只有访问/login路径时,才会被拦截),第二种,拦截目录路径(/emps/*:访问/emps下的所有资源,都会被拦截),第三种,拦截所以(/*:访问所有资源,都会被拦截)

        最后,我们来学习一下过滤器链,什么是过滤器链?所谓过滤器链,是指在web应用当中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。而处于链上的过滤器会一个一个执行,会先执行第一个,第一个放行之后才会执行第二个,以此类推……,同样访问完web资源之后会回来执行放行后逻辑,此时顺序就会反过来,从后往前依次执行,最后再给浏览器响应数据。而其顺序则是以注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序,类名排名越靠前,优先级越高。

  •         登录校验-Filter

        我们上面已经把过滤器的入门以及使用细节学习完了,接下来最后一步,我们需要通过过滤器filter来完成案例中的登录校验功能。

            接下来,我们要分析一下登录校验的具体流程,首先,我们来思考两个问题

  • 所有的请求,拦截到了以后,都需要校验令牌吗?
  • 拦截到请求后,什么情况下才可以放行,执行业务操作

        第一个问题,会有一个例外,即登录操作,在执行登录操作的时候,用户并没有jwt令牌,所以不需要校验。第二个问题,只有有令牌,且令牌校验通过(合法)才可以放行,否则都返回未登录错误信息,即接口文档中的错误信息

{
    "code": 0,
    "msg": "NOT_LOGIN",
    "data": null
}

接下来,我们根据流程图分析一下具体的实现步骤

  1. 获取请求url
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行
  3. 如果不包含,获取请求头中的令牌(token)
  4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  5. 解析token,如果解析失败,返回错误结果(未登录)
  6. 放行

具体的思路和步骤我们已经分析完毕,接下来我们打开idea来进行登录校验的具体实现

        里面的代码我们有一步需要下载一个依赖来满足对象到json格式的手动转换,打开pop.xml注入下面依赖

<!-- 对象——json格式的fastJSON依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.19.graal</version>
        </dependency>

然后,我们编写loginCheckFilter来进行流程的处理

package com.ittaotao.filter;

import com.alibaba.fastjson.JSONObject;
import com.ittaotao.pojo.Result;
import com.ittaotao.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request; //将request进行强转
        HttpServletResponse resp = (HttpServletResponse) response;
        //1.获取请求url
        String url = req.getRequestURL().toString();
        log.info("请求的url:{}",url);

        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if (url.contains("login")){
            log.info("登录操作,放行...");
            chain.doFilter(request,response);
            return;
        }
        //3.获取请求头中的令牌
        String jwt = req.getHeader("token");

        //4.判断令牌是否存在(字符串为null或者为空字符串),如果不存在,返回错误信息(未登录)
        //if (jwt == null || jwt == " ")
        if (!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json ----------> 阿里巴巴fastJson
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }
        //5.解析token,如果解析失败(即解析jwt令牌报错),返回错误信息(未登录)
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) { //出现异常,jwt令牌解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json ----------> 阿里巴巴fastJson
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        //6.放行
        log.info("令牌合法,放行");
        chain.doFilter(request,response);
    }
}

编写完代码之后我们可以通过postman进行测试,测试完成之后打开浏览器进行前后端联调测试

以上我们便可以进行登录filter的登录校验了,接下来我们将来讲解一下拦截器Interceptor

        interceptor

拦截器我们将分为三个部分来讲解(简介&快速入门、详解、登录校验-Interceptor)

  • 简介&快速入门

        拦截器是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法(controller)的执行,其作用即为拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。我们可以发现,拦截器和过滤器非常类似,接下来我们就通过一个快速入门来演示一下拦截器的基本使用。

        拦截器的使用也是分为两部分,首先,定义拦截器,实现HandlerInterceptor接口,并重写其所有方法(preHandle:目标资源方法(controller)执行前执行,返回true:放行,返回false:不放行、postHandle:目标资源方法执行后执行、afterCompletion:视图渲染完毕后执行,最后执行);其次,注册配置拦截器。

        接下来,我们在idea上完成这个操作,首先,在ittaotao下创建一个包interceptor,在创建一个类LoginCheckInterceptor

package com.ittaotao.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //重写HandlerInterceptor中的三个方法

    @Override //在目标资源方法运行前运行,返回true:放行,返回false,不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle......");
        return true;
    }

    @Override //目标资源方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle......");

    }

    @Override //视图渲染完毕后运行,最后才会运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion......");
    }
}

以上即为定义拦截器

        接着,注册配置拦截器,首先创建一个包config用于装入配置类,其次,新建一个类WebConfig开始注册配置拦截器

package com.ittaotao.config;

import com.ittaotao.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration //代表当前类是一个配置类
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor; //通过依赖注入引入刚才创建好的拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**"); //注册添加刚才创建好的拦截器,addPathPatterns表示要拦截的资源(/**表示全部资源)
    }
}

        综上所述,拦截器就配置好了,接下来我们进行测试,在这之前我们先把filter的@webfilter(urlpatterns="/*")注释掉,这样我们运行时候filter过滤器就不会启动了,然后我们启动服务,进行登录这个请求,请求完之后可以看到控制台的输出

        首先执行prehandle,然后放行去执行controller中的方法(登录),登录回来会执行postHandle这个方法,最终执行afterComptetion。

以上便是我们拦截器的快速入门,接下来我们来介绍拦截器的使用细节

  • 详解(拦截路径、执行流程)

首先我们来说一下拦截路径,即可以根据需求,配置不同的拦截路径:

register.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")[代表需要拦截哪些路径].excludePathPatterns("/login")[代表不需要拦截哪些路径]

以上便是拦截器的拦截路径了,介绍完后接下来我们来说一下拦截器的执行流程

        当我们打开浏览器来访问部署到web服务器的web应用时,此时我们所定义的过滤器会拦截到这次请求,当拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作,而由于我们当前是使用springboot开发的,所以放行之后进入到了spring的环境当中,那就要访问我们定义到controller当中的接口方法,但是tomacat服务器并不识别我们controller的程序的,但是他能识别servlet程序,因为tomacat是一个servlet容器,而在springweb环境当中,他提供了一个非常核心的servlet,我们称为前端控制器,这个servlet叫dispatcherservlet,所以请求会先进入到dispatcherservlet,由其将这个请求再转给controller,再去执行对应的接口方法,但是我们现在又定义了拦截器,所以在执行controller的方法之前,先要被拦截器拦截住,拦截器拦截了这次请求之后,接下来就要对这些请求进行处理。在执行controller方法之前,会先执行prehandle,返回true之后,执行放行操作,允许其访问控制器。控制器方法执行完毕之后,才允许其返回执行postHandle以及aftercompletion,然后再返回给diapatcherservlet,最终执行过滤器当中放行后的逻辑,逻辑执行完毕之后,最终给浏览器响应数据,以上便是拥有过滤器和拦截器同时存在的执行过程了。

最后,我们进行代码测试,可以发现其执行顺序就是如上所示

通过以上,我们可以发现filter和interceptor的区别

接口规范不同:过滤器需要实现filter接口,而拦截器需要实现HandleInterceptor接口。

拦截范围不同:过滤器filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。

  • 登录校验-Interceptor

        最后,我们将要完成拦截器的最后一步操作,实现案例的登录校验功能。登录校验的过程以及逻辑步骤和过滤器完全一样,我们不再分析。现在我们只需要将过滤器filter换成拦截器Interceptor即可,接下来我们直接进行代码演示,我们直接对LoginCheckInterceptor进行修改

package com.ittaotao.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.ittaotao.pojo.Result;
import com.ittaotao.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    //重写HandlerInterceptor中的三个方法

    @Override //在目标资源方法运行前运行,返回true:放行,返回false,不放行
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        //1.获取请求url
        String url = req.getRequestURL().toString();
        log.info("请求的url:{}",url);

        //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
        if (url.contains("login")){
            log.info("登录操作,放行...");
            return true;
        }
        //3.获取请求头中的令牌
        String jwt = req.getHeader("token");

        //4.判断令牌是否存在(字符串为null或者为空字符串),如果不存在,返回错误信息(未登录)
        //if (jwt == null || jwt == " ")
        if (!StringUtils.hasLength(jwt)){
            log.info("请求头token为空,返回未登录的信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json ----------> 阿里巴巴fastJson
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return false; //不能放行
        }
        //5.解析token,如果解析失败(即解析jwt令牌报错),返回错误信息(未登录)
        try {
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) { //出现异常,jwt令牌解析失败
            e.printStackTrace();
            log.info("解析令牌失败,返回未登录错误信息");
            Result error = Result.error("NOT_LOGIN");
            //手动转换 对象--json ----------> 阿里巴巴fastJson
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return false;
        }

        //6.放行
        log.info("令牌合法,放行");
        return true;

    }

    @Override //目标资源方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle......");

    }

    @Override //视图渲染完毕后运行,最后才会运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion......");
    }
}

        我们启动服务,利用postman进行测试,首先,进行登录操作,可以在其返回信息上面拿到我们的令牌,接着,我们开始查询部门,可以发现,一开始如果没有携带令牌,会报错误,返回not_login,此时,我们把令牌附上,这时,我们便可以查询部门了,同时拦截器也将我们放行。

最后我们只需要进行前后端联调即可,这里我不再演示

        异常处理

前两个章节讲解完了基础登录和校验登录,接下来,我们就讲解我们的最后一张——异常处理

        此时我们新增一个重复的部门,在界面中我们可以发现什么也没变化,我们打开开发者工具重新运行,可以发现它发送了一个请求,但是请求的响应结果是500,由此可知是服务器端出现的问题,打开代码发现,它会告诉你就业部(添加的部门)重复了,原因就是我们当初添加部门表的时候,为name添加了unique唯一约束。

        我们可以发现其返回的数据为json格式,并不符合开发规范中的统一响应结果result,所以前端并不会解析此结果。那如果之后应用中出现这种异常我们该怎么处理呢?

方案一:在controller的方法中进行try...catch

        可是controller的方法有很多,这样做就以为着我们需要再controller的每一个方法都进行try..catch来捕获异常,十分繁琐,代码臃肿

方案二:全局异常处理器        

        我们可以在整个项目中定义一个全局异常处理器,这样我们就可以来捕获整个项目中所有的异常,这种方式简单、优雅、且比较推荐这样做。有了这个全局异常处理器,mapper1遇到异常不用处理,直接抛给servlice,service遇到异常直接抛给controller,controller也不用处理,最终交给全局异常处理器,全局异常处理器,处理完成之后,再给前端响应标准的统一响应结果result即可。

        接下来我们就打开idea来定义一个全局异常处理器,首先,连包带类创建Exception.GlobalExceptionHandler

package com.ittaotao.exception;

import com.ittaotao.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class) //捕获所有的异常
    public Result ex(Exception ex){
        ex.printStackTrace();
        return Result.error("对不起,操作失败,请联系管理员");
    }
}

        最后,我们再次新增部门,可以发现,前端已经给了我们错误提示,这也就证明了全局异常处理器已经捕获到了异常,我们也可以发现其返回响应的结果是一个标准的result

        以上便是我们本篇博客的所有内容了(基础登录功能、登录校验功能以及异常处理),如有疑问还请评论区多度指教,记得多多点赞,感谢!!!

  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余阳867

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值