Springboot+JWT+Vue实现登录功能

一、前言

最近在写一个Springboot+Vue的前后端分离项目,并且刚学了JWT的功能和原理,正好拿来练练手,在开发过程中也遇到了很多坑点,主要是对vue和springboot不够熟练导致的,因此写篇文章来记录分享。

二、JWT

1.介绍

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

2.请求流程

1. 用户使用账号发出请求; 2. 服务器使用私钥创建一个jwt; 3. 服务器返回这个jwt给浏览器; 4. 浏览器将该jwt串在请求头中像服务器发送请求; 5. 服务器验证该jwt; 6. 返回响应的资源给浏览器。

3.JWT的主要应用场景

身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

4.JWT的结构

JWT包含了三部分

Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)

Payload 负载 (类似于飞机上承载的物品,存放我们指定的信息)

Signature 签名/签证

将这三段信息文本用.连接一起就构成了JWT字符串

就像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

三、Springboot集成JWT

1. Maven添加JWT依赖项

        <!--token-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

2. 封装Token的生成函数

  public String getToken(User user, long time) {
    Date start = new Date();//token起始时间
    long currentTime = System.currentTimeMillis() + time;
    Date end = new Date(currentTime);//token结束时间
    String token = "";
    token = JWT.create()
            .withAudience(user.getLevel().toString()+user.getId().toString()) //存放接收方的信息
            .withIssuedAt(start)//token开始时间
            .withExpiresAt(end)//token存活截止时间
            .sign(Algorithm.HMAC256(user.getPassword()));//加密
    return token;
  }

3. 编写注解类

1.UserLoginToken 需要登录才能进行操作的注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
  boolean required() default true;
}

2.PassToken 用来跳过验证的

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
  boolean required() default true;
}

4. 编写权限拦截器(AuthenticationInterceptor)

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

  @Autowired
  UserService userService;

  @Override
  public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    //通过所有OPTION请求
    if(httpServletRequest.getMethod().toUpperCase().equals("OPTIONS")){
      return true;
    }

    String token = httpServletRequest.getHeader("Authorization");// 从 http 请求头中取出 token
    String refreshToken = httpServletRequest.getHeader("freshToken");// 从 http 请求头中取出 token

    // 如果该请求不是映射到方法直接通过
    if (!(object instanceof HandlerMethod)) {
      return true;
    }
    HandlerMethod handlerMethod = (HandlerMethod) object;
    Method method = handlerMethod.getMethod();
    //检查是否有passtoken注释,有则跳过认证
    if (method.isAnnotationPresent(PassToken.class)) {
      PassToken passToken = method.getAnnotation(PassToken.class);
      if (passToken.required()) {
        return true;
      }
    }

    // 获取 token 中的 用户信息
    String userValue = null;
    try {
      userValue = JWT.decode(token).getAudience().get(0);
     
    } catch (JWTDecodeException j) {
      throw new RuntimeException("401");
    }
    Map<String, Object> map = new HashMap<>();
    map.put("level", (userValue).substring(0,1));
    map.put("id", (userValue).substring(1));

    User user = userService.findUser(map);
    if (user == null) {
      throw new RuntimeException("用户不存在,请重新登录");
    }

    Date oldTime = JWT.decode(token).getExpiresAt();
    Date refreshTime = JWT.decode(refreshToken).getExpiresAt();
    long oldDiff = oldTime.getTime() - new Date().getTime();//这样得到的差值是毫秒级别
    long refreshDiff = refreshTime.getTime() - new Date().getTime();//这样得到的差值是毫秒级别
    if (oldDiff <= 0) {
      if (refreshDiff <= 0) {
        httpServletResponse.sendError(401);
        throw new RuntimeException("401");
      }
    }
    String newToken = userService.getToken(user, 60* 60 * 1000);
    String newRefToken = userService.getToken(user, 24*60*60*1000);
    // 更新token
    httpServletResponse.setHeader("Authorization", newToken);
    httpServletResponse.setHeader("freshToken", newRefToken);

    //检查有没有需要用户权限的注解
    if (method.isAnnotationPresent(UserLoginToken.class)) {  // 是否使用@UserLoginToken注解
      UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
      if (userLoginToken.required()) {
        // 执行认证
        if (token == null) {
          throw new RuntimeException("=== 无token,请重新登录 ===");
        }
        // 利用用户密码,解密验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
          jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
          System.out.println("=== token验证失败 ===");
          httpServletResponse.sendError(401);
          throw new RuntimeException("401");
        }
        return true;
      }
    }
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         Object o, ModelAndView modelAndView) throws Exception {

  }
  @Override
  public void afterCompletion(HttpServletRequest httpServletRequest,
                              HttpServletResponse httpServletResponse,
                              Object o, Exception e) throws Exception {
  }
}

5. 配置跨域请求和权限拦截器

/**
 * Description 解决vue+spring boot跨域问题
 **/
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public CorsFilter corsFilter()
    {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        // 是否允许请求带有验证信息
        config.setAllowCredentials(true);

        // 允许访问的客户端域名
        // (springboot2.4以上的加入这一段可解决 allowedOrigins cannot contain the special value "*"问题)
        List<String> allowedOriginPatterns = new ArrayList<>();
        allowedOriginPatterns.add("*");
        config.setAllowedOriginPatterns(allowedOriginPatterns);

        // 设置访问源地址
        // config.addAllowedOrigin("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
  // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
  @Override
  public void addInterceptors(InterceptorRegistry registry) {

    //添加自定义拦截器和拦截路径,此处对所有请求进行拦截,除了登录界面和登录接口
    registry.addInterceptor(appInterceptor())
        .addPathPatterns("/api/sms/**")//添加拦截路径,拦截所有
        .excludePathPatterns("/login"); // 排除的拦截路径
    WebMvcConfigurer.super.addInterceptors(registry);
  }

  @Bean
  public HandlerInterceptor appInterceptor(){
    return new AuthenticationInterceptor();
  }
}

四、Vue配置JWT

1. 配置axios拦截器

axiosHelper.js

import axios from 'axios';
import {Message} from 'element-ui';

// axios.defaults.timeout = 10000; //超时终止请求
axios.defaults.baseURL = 'http://localhost:8443/'; //配置请求地址


axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
axios.defaults.withCredentials = true;

// var loadingInstance
axios.interceptors.request.use(config => {
    //Ajax请求执行该方法,请求带上token
    var token = localStorage.getItem('userToken');
    const refreshToken = localStorage.getItem('refreshToken');
    if (token !== null && token !== undefined && token !== '') {
        config.headers.Authorization = token;
        var host = window.location.host;
        config.headers['appHost'] = host;
    }
    if (refreshToken !== null && refreshToken !== undefined && refreshToken !== '') {
        config.headers.freshToken = refreshToken;
    }
    //全局配置,get请求加时间戳
    if (config.method.toLowerCase() === "get") {
        config.url += config.url.match(/\\?/) ? "&" : "?";
        config.url += "_dc=" + new Date().getTime();
    }
    return config;
}, error => {  //请求错误处理
    // loadingInstance.close()
    Message.error({
        message: '加载超时'
    });
    return Promise.reject(error);
});
//
var count = 0;
axios.interceptors.response.use(response => {
        return response;
    },
    error => {
        if (error.response.status === 401) {
            if (count === 0) {
                count = count + 1;
            } else if (count > 0) {
                return null;
            }
            // debugger
            Message.error("身份信息超时,请重新登录!", { icon: 1, time: 2000 });
            $cookies.remove('userToken');
            setTimeout(function () {
                window.location.href = '/#/login';
            }, 3000);
            return Promise.reject(error);
        }
    }
    );
export default axios; //暴露axios实例

然后在main.js中配置

import axiosHelper from '../src/axios/axiosHelper'
Vue.prototype.axiosHelper = axiosHelper;

2. axios接收Token, 并放入localStorage中

只需在拿到后端数据data后, 添加以下代码

let obj = {
      username: data.username,
      level: data.level
}

localStorage.setItem('userToken', data.token)
localStorage.setItem('refreshToken', data.refreshToken)

五、总结

至此,我们就配置好了前后端的JWT使用环境,之后还会继续更新我在学习技术时总结的干货,希望大家多多点赞支持哦!

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
Springboot+JWT+Vue实现登录功能
最近在写一个Springboot+Vue的前后端分离项目,并且刚学了JWT的功能和原理,正好拿来练练手,在开发过程中也遇到了很多坑点,主要是对vue和sprin...
<<上一篇
下一篇>>