eyePaddles 发布的文章

路由守卫

//index.js
router.beforeEach((to, from, next) => { // to表示前端请求去哪个路径,from表示从哪个网页过来,next表示引导到何处
  if(to.path === '/login') return next() // next()放行
  const token = localStorage.getItem('token') // 从localStorage获取token
  if(!token || token === '') return next('/login') // 若无token,引导到登陆页面
  next()
})

存储token

// token="eyJhbGciOiJIUzI1NiJ9.eyJpbWFnZSI6ImFkbWluLmpwZyIsIm5hbWUiOiJhZG1pbiIsInBlcm1pc3Npb24iOiJyb290IiwiaWQiOjEwMDAwMCwiZW1haWwiOiIxMzUxQHFxLmNvbSIsImV4cCI6MTcwODE1ODA4MH0=.EKs0ywuf6KQJEykiFpA25KIeG4HpcC3VkA7dCKgpjoM"
localStorage.setItem("token", token);

路由跳转

this.$router.push("/path");

解析JWT

paeseToken(){
  const token = localStorage.getItem("token"); // 从localStorage获取JWT
  const payload = token.split('.')[1]; // 取JWT的第二部分payload
  const decodedJson = atob(payload); // atob进行base64解码
  return JSON.parse(decodedJson); // JSON解析字符串,得到对象,JSON.stringify()封装字符串
}

前端请求JWT

axios.post("http://localhost:8080/login", this.object).then((response) => { // POST请求,参数1:URL,参数2:登录账号和密码的对象
  if(response.data.code == 1){ // 响应成功
    var token = response.data.data;
    localStorage.setItem("token", token); // localStorage存储token
    this.$router.push("/home"); // 自动跳转
  } else {
    this.notice(token); // 错误提示
  }
})

前端携带JWT

// axios.post(url, dataObject, config)
axios.post("http://localhost:8080/emp", this.object, {
  headers: { 'token': localStorage.getItem('token') }
}).then((result) => { });

过滤器和拦截器都不能将类转JSON的任务交给Spring,只能使用Servlet的方法返回前端。
示例代码:

if(!StringUtils.hasLength(token)) { // 若token为空
    Result result = Result.error("no_login");
    String jsonResult = JSON.toJSONString(result); // Object转String,需要依赖fastjson
    response.setCharacterEncoding("UTF-8"); // 响应头字符集为UTF-8,防止中文乱码
    response.getWriter().write(jsonResult);
    return false;
}

依赖:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>

Interceptor

用于拦截请求,类似于过滤器,不同于过滤器的是拦截器是Spring框架提供,而过滤器是JaveWeb三大组件之一。
使用拦截器能统一拦截请求,而无需在每一个方法上都加上相同的逻辑,减少了代码的复用。
常见场合:

  1. 统一登录校验,验证JWT令牌合法性
  2. 统一编码解码
  3. 统一敏感字符处理
  4. 其他通用性操作

拦截器:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {

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

    @Override // 目标资源方法执行后执行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override // 视图渲染完毕后执行,最后执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

注册拦截器:

import edu.recipePost.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("/**"); // 注册拦截器路径为请求的所有路径
    }
    
}

指定拦截路径:

// 在配置类中设置
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**") // 注册拦截器路径为请求的所有路径
    .excludePathPatterns("/login"); // 不需要拦截的资源
}
拦截路径含义举例
/*任意一级路径能匹配/a,不能匹配/a/b
/**任意级路径能匹配/a、/b、/a/b
/a/*/a下的一级路径能匹配/a/b,不能匹配/a/b/c和/a
/a/**/a下的任意级路径能匹配/a、/a/b、/a/b/c,不能匹配/b

与过滤器的区别:
过滤器在拦截器的外层拦截,过滤器拦截所有的资源,而拦截器只拦截Spring boot的资源。当有请求到来时,会先被过滤器截获,过滤器放行后会被拦截器拦截。

会话技术

一次会话包括若干次连接,一次连接即一次请求和响应。

1. Cookie

优点:HTTP协议自有的技术,嵌入在请求头(键cookie)和响应头(键set-cookie)中,所有的键值对都存储在浏览器端。
缺点:
a. 移动端app无法使用cookie
b. 不安全,用户可以自行禁止cookie,这样就无法使用cookie保持会话了
c. cookie不能在跨域的场合中使用

示例代码:

import edu.recipePost.pojo.Result;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class SessionController {

    // 设置cookie
    @GetMapping("/get-cookie")
    public Result getCookie(HttpServletResponse response) { // 从HttpServletResponse获取响应头
        Cookie cookie = new Cookie("cookie_key_name", "value"); // 创建cookie
        cookie.setMaxAge(3600*24*7); //cookie设置生命周期,单位是秒
        cookie.setPath("/cookie/age"); //cookie设置路径
        response.addCookie(cookie); //response对象添加cookie
        return Result.success();
    }

    // 获取cookie
    @GetMapping("/other")
    public Result setCookie(HttpServletRequest request) { // 从HttpServletRequest获取请求头
        Cookie[] cookies = request.getCookies(); //获取cookie对象
        for (Cookie cookie:cookies) {
            if(cookie.getName().equals("cookie_key_name")) {
                log.info("cookie_key_name:"+cookie.getValue()); // 数值cookie对应的键值
            }
        }
        return Result.success();
    }

}

2. Session

概述:浏览器的cookie中存储SessionID,请求头的cookie和响应头的set-cookie传输的都是SessionID,所有的键值对都存储在服务器端,服务器通过浏览器传来的SessionID来找到Session对象,Session对象中存储有一次会话的键值对。

优点:数据(键值对)存储在服务器端,比较安全
缺点:
a. 目前大型项目都是服务器集群,服务器集群环境下无法直接使用Session。原因:两次访问会负载均衡到不同的服务器,导致Session对象分布在各个服务器,各服务器之间的Session对象不同步。
b. Session技术依靠HTTP协议的Cookie实现,所以Cookie的缺点也是Session的缺点。

与Cookie区别:
a. Cookie技术键值对存储在浏览器,Session技术键值对存储在服务器;
b. Session技术浏览器的cookie里只存有SessionID,通过SessionID请求服务器,找到服务器的Session对象,从而找到此次会话的键值对。

示例代码:

import edu.recipePost.pojo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class SessionController {

    // 第一次访问时,往HttpSession存储值
    @GetMapping("/set-session")
    public Result getSession(HttpSession session) { // 获取会话对象session,若不存在则创建session;存在就获取当前请求对应的session
        log.info("HttpSession-test1: "+session.hashCode()); // 同次会话HttpSession-test1: 1675666366
        session.setAttribute("loginUser", "tom"); // session存值
        return Result.success();
    }

    // 从HttpSession获取值
    @GetMapping("/get-session")
    public Result setCookie(HttpServletRequest request) { // 通过请求头获取会话对象,也可以通过HttpSession获取会话对象
        HttpSession session = request.getSession(); // 通过请求头获取会话对象
        log.info("HttpSession-test2: "+session.hashCode()); // 同次会话HttpSession-test1: 1675666366
        Object loginUser = session.getAttribute("loginUser");
        log.info("loginUser: "+loginUser); // loginUser: tom
        return Result.success(loginUser);
    }

}

3. JWT令牌

JSON Web Token
JWT_structure.png
结构:
由“.”分成三部分:
第一部分:描述令牌类型、签名算法的Header => 经过JSON封装 => 经过base64编码
第二部分:自定义信息和默认信息的Payload(有效载荷) => 经过JSON封装 => 经过base64编码
第三部分:数字签名,前两部分和指定密钥经过签名算法得到。防止被篡改。

相比于Cookie和Session,有以下优点:
a. 不仅支持Web,还支持PC端、移动端
b. 解决集群服务器环境下的认证问题
c. 减轻服务器端的存储压力
缺点:非HTTP协议天然生成,需要自己实现令牌的生成、存储与传输

Maven依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version></version>
</dependency>

实例代码:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;

import java.util.Date;
import java.util.Map;

public class JWTUtils {

    @Value("${JWT.web.login.sign-key}")
    private static String signKey;

    @Value("${JWT.web.login.expire}")
    private static Long expire;

    public static String generateJWT(Map<String, Object> claims) {
        return Jwts.builder()
                .addClaims(claims) // 添加JSON实体数据
                .signWith(SignatureAlgorithm.HS256, signKey) // 签名算法,签名密钥
                .setExpiration(new Date(System.currentTimeMillis()+expire)) // token过期时间
                .compact();
    }

    public static Claims parseJWT(String jwt) {
        return Jwts.parser() // JWT解析器
                .setSigningKey(signKey) // 添加签名密钥
                .parseClaimsJws(jwt) // 解析JWT令牌
                .getBody();
    }

}

配置信息获取

从application.properties或application.xml中获取配置信息:

@Value("${前缀.键名}") //注解在属性,不能注解在静态属性上
@ConfigurationProperties(prefix="前缀") //注解在类上,配合@Component使用

全局异常处理器

import edu.recipePost.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 e) {
        e.printStackTrace();
        return Result.error("非法操作");
    }
}