1.后端 

1.1.导入依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>

1.2.编写jwt的拦截器

这个类实现一个HandlerInterceptor接口,这个类主要完成以下几个任务:

  • 从请求头里获取token,没有获取到就抛异常(注意:请求头里原本是没有token的,这个需要我们自己在前端添加一个token)
  • 解码token并从token里获取用户ID,没有获取到就抛异常,表明token里没有数据(注意:这个用户ID是自己在前端添加token时存储的)
  • 通过用户密码来生成一个验证器,解析token(JWT一般含有三个部分,头部,荷载,签名,解析过程中jwtVerifier会检验这三部分能不能正常分离,以及来用验证器来验证签名,以及检查token的过期时间)这一步也是最重要的一步!
package com.kuang.common;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.kuang.exception.ServiceException;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class JwtInterceptor implements HandlerInterceptor {

    @Resource
    private UserMapper userMapper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


        //从请求头Header里接收传来的参数token
        String headerToken = request.getHeader("token");
        //如果传来的token为空,则从url参数中来接收传来的token
        if(StringUtils.isBlank(headerToken)){
            headerToken = request.getParameter("token");
            //如果url里的token为空,则抛异常
        }
        if (StringUtils.isBlank(headerToken)){
            throw new ServiceException("401","请登录");
        }

        //从token中获取userId
        //JWT.decode(headerToken) 解码JTW Token
        String userId;
        try {
            userId = JWT.decode(headerToken).getAudience().get(0);
        } catch (JWTDecodeException e) {
            throw new ServiceException("401","请登录");
        }
        //根据userId查询数据库
        //userId是String类型,这里要转换成int类型
        User user = userMapper.selectUserById(Integer.parseInt(userId));
        //user为空,则抛异常
        if (user == null){
            throw new ServiceException("401","请登录");
        }
        //通过用户密码加密之后生成一个验证器
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            //验证token
            jwtVerifier.verify(headerToken);
        } catch (JWTVerificationException e) {
            throw new ServiceException("401","请登录");
        }
        return true;
    }
}

1.3.编写token的工具类

在这个类中有以下几点任务:

  • 生成token,并且将用户ID放在token的荷载(Payload)中当作受众声明(Audience),以及设置token的过期时间,把用户密码当作密钥,然后给token加一个签名,只有添加了签名,这个token才能被使用,而我们设置的这个密钥就是来验证签名的钥匙
package com.kuang.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;

@Component
public class TokenUtils {

    private static UserMapper staticUserMapper;

    @Autowired
    UserMapper userMapper;

    @PostConstruct
    public void setUserService(){
        staticUserMapper = userMapper;
    }

    /**
     * 创建token
     * @param userId
     * @param sign
     * @return
     */
    public static String createToken(String userId,String sign){

        // 获取当前时间
        Date currentDate = new Date();

        // 创建Calendar实例
        Calendar calendar = Calendar.getInstance();

        // 设置Calendar的时间为currentDate
        calendar.setTime(currentDate);

        // 向前偏移两个小时
        calendar.add(Calendar.HOUR_OF_DAY, 2);

        // 获取偏移后的时间
        Date offsetDate = calendar.getTime();

        return JWT.create().withAudience(userId)//将userId保存到token里
                .withExpiresAt(offsetDate)      //2小时候token过期
                .sign(Algorithm.HMAC256(sign)); //将password作为token密钥

    }

    /**
     * 获取当前登录的用户信息
     * @return
     */
    public static User getCurrentUser(){

        //获取当前请求的HttpServletRequest对象,这样就能在下面访问请求头、参数等
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes())
                .getRequest();

        try {
            //从请求头里获取token
            String token = request.getHeader("token");
            //如果token不为空,则从Audience中获取第一个数据(是用户的id)
            if (StringUtils.isNotBlank(token)){
                String userId = JWT.decode(token).getAudience().get(0);
                return staticUserMapper.selectUserById(Integer.parseInt(userId));
            }
        } catch (Exception e) {
            return null;
        }
        return null;
    }



}

 1.4.扩展springmvc的拦截器

这个类继承WebMvcConfigurationSupport类,主要任务有以下几点:

  • 将在第二步编写的jwt的拦截器注入到spring容器中,并将其添加到spring的拦截器中
  • 设置拦截路径

当用户访问下面设置好的拦截路径时,就会触发我们自己编写的jwt的拦截器,然后进入校验过程(就是第二步中的那一套流程)

package com.kuang.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //配置jwt的拦截器规则,拦截所以请求,除了/user/login,/user/register,/file/upload
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login","/user/register","/file/upload");
        super.addInterceptors(registry);
    }


    @Bean
    public JwtInterceptor jwtInterceptor(){
        return new JwtInterceptor();
    }
}

 1.5.将后端生成的token返回给前端

一般会在用户登录时,将token返回给前端,下面这个类就是实现登录功能的service层的实现类

service层: 

 @Override
    public User selectUserByUsername(User user) {

        User user1 = userMapper.selectUserByUsername(user);
        //生成token,userId用来放在token里,password用来生成token的验证器,来验证token
        String token = TokenUtils.createToken(String.valueOf(user1.getId()), user1.getPassword());
        user1.setToken(token);
        return user1;
    }

controller层: 

    //登陆功能
    @PostMapping("/login")
    public Result login(@RequestBody User user, HttpServletRequest request){
        User user1 = userService.selectUserByUsername(user);

        if (!user.getPassword().equals(user1.getPassword())){
            return Result.error("用户名或密码不正确");
        }
        //将id存入session
        //request.getSession().setAttribute("userId",user1.getId());

        //将含有token的user对象返回给前端
        return Result.success(user1);
    }

2.前端

2.1.导入request.js文件 

这里添加一个通用的request.js文件,这个文件用于以下几点:

  • 可以在请求发送前对请求做一些处理
  • 可以在接口响应后统一处理结果
  • 对请求路径前面的http:localhost做了封装,以便不用每次都写上
  • 对返回的数据做了封装,原本要访问后端返回的数据要这样写res.data.data,封装了之后可以简化为res.data

import axios from 'axios'
import router from "@/router";

const request = axios.create({
    baseURL: 'http://localhost:8082',  // 注意!! 这里是全局统一加上了 '/api' 前缀,也就是说所有接口都会加上'/api'前缀在,页面里面写接口的时候就不要加 '/api'了,否则会出现2个'/api',类似 '/api/api/user'这样的报错,切记!!!
    timeout: 5000
})

// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    //在请求头里添加一个token
    let user = JSON.parse(localStorage.getItem("user") || '{}')
    config.headers['token'] = user.token;  // 设置请求头
    return config
}, error => {
    return Promise.reject(error)
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;
        // 如果是返回的文件
        if (response.config.responseType === 'blob') {
            return res
        }
        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }
        if (res.code === '401'){
            router.push('/login')
        }
        return res;
    },
    error => {
        console.log('err' + error) // for debug
        return Promise.reject(error)
    }
)


export default request

在上面的文件里,我们着重看这几行代码,在前端发送请求时,在请求头里添加一个token,并且从localStorage中获取在登录时存储的用户信息,用来存储在token中,具体程序如下:

在接口响应后,统一处理结果中的如下程序,如果用户没有token,后端就会返回401的错误,在这里就会处理401错误,进行页面跳转

导入这个文件之后,我们可以在main.js文件中注册这个文件的全局对象

2.2.采用request.js文件里提供的请求方式

 这里只提供一个例子,其他的地方都是一样的

3.流程图分析

下面的流程图涉及了后端以下几个类:

  • JwtInterceptor:1.2中的jwt的拦截器
  • TokenUtils:1.3中的token的工具类
  • InterceptorConfig:1.4中的springmvc的扩展类
  • LoginController:处理用户登录的controller方法,这个方法用来返回给前端token
  • selectUserController:在登录之后,处理前端发过来的查询用户的请求

解释上面的过程,用户在登陆之前还没有token,登录之后,通过调用TokenUtils来生成token,并且返回给前端,至此,该用户就有了token,在之后的请求中首先会被Interceptor-Config类拦截下来,然后进入JwtInterceptor类进行token的校验,成功后才会进入controller层,否则就会抛出401的异常(这里的异常是自己手动设置的),然后就会返回给前端,前端就会发生页面跳转,跳转到login页面

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐