文章目录

  • 前言
  • 一、jwt后端配置
  • 1.导入依赖
  • 2.jwt方法类
  • 3.token生成
  • 4.jwt拦截器
  • 5.jwt拦截器配置
  • 二、jwt前端配置
  • 1.获取token
  • 2.封装axios
  • 三、踩坑记录
  • 1.静态资源被拦截(一、5)
  • 2./error被拦截(一、5)
  • 3.yml项目路径(一、5)



前言

通过jwt实现单点登录,后端项目基于spring boot实现,前端使用vue.js示范。
jwt是什么?原理是什么?诸如此类的问题这里不提,本文记录jwt登录验证的前后端实现过程及踩的坑


一、jwt后端配置

1.导入依赖

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.25</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

2.jwt方法类

包含token相关参数,生成token、验证token、解析token方法

package com.ymzhao.website.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @date 2019/4/25 11:46
 * @atuther net source
 */
public class JwtUtil {

    //密钥
    public static final String SECRET = "sgfdsopljkhsl;o437632";
    //过期时间:秒
    public static final int EXPIRE = 60 * 1;

    /**
     * 生成Token
     * @param username
     * @return
     * @throws Exception
     */
    public static String createToken(String username) throws Exception {
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.SECOND, EXPIRE);
        Date expireDate = nowTime.getTime();

        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");

        String token = JWT.create()
                .withHeader(map)//头
                .withClaim("username", username)
                .withSubject("测试")//
                .withIssuedAt(new Date())//签名时间
                .withExpiresAt(expireDate)//过期时间
                .sign(Algorithm.HMAC256(SECRET));//签名
        return token;
    }

    /**
     * 验证Token
     * @param token
     * @return
     * @throws Exception
     */
    public static Map<String, Claim> verifyToken(String token)throws Exception{
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
        DecodedJWT jwt = null;
        try {
            jwt = verifier.verify(token);
        }catch (Exception e){
            throw new RuntimeException("凭证已过期,请重新登录");
        }
        return jwt.getClaims();
    }

    /**
     * 解析Token
     * @param token
     * @return
     */
    public static Map<String, Claim> parseToken(String token){
        DecodedJWT decodedJWT = JWT.decode(token);
        return decodedJWT.getClaims();
    }

}

验证方法返回类为Map<String, Claim>,Claim定义如下:

ingress rewrite前后端分离 前后端分离 jwt_vue


有需要的话,使用它提供的格式转换方法

3.token生成

demo 如下,直接调用上一节中的token生成方法:

@Service
public class LoginServiceImpl implements LoginService {

    @Override
    public String logIn(String username, String password) {
    	// 可封装下返回类,避免直接抛异常,结构为{code: 0, data: "xxx", message: "成功"}
        if("admin".equals(username) && "123abc".equals(password)) {
            String token;
            try {
                token = JwtUtil.createToken(username);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("生成token失败");
            }
            return token;
        }
        throw new RuntimeException("用户名/密码错误");
    }
}

返回给前端后,需同前端约定将token置于请求头中,以实现用户登录验证

4.jwt拦截器

拦截http请求,检查token的有效与否

package com.ymzhao.website.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.interfaces.Claim;
import com.ymzhao.website.utils.JwtUtil;
import org.apache.commons.lang3.StringUtils;
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;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * Create By ymzhao on 2021/3/11
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {

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

        String uri = request.getRequestURI();

        System.out.println("uri: " + uri);

        String headToken = request.getHeader("Authorization");
        if(StringUtils.isEmpty(headToken)) {
            Map<String, Object> map = new HashMap<>();
            map.put("code", 20001);
            map.put("message", "Missing or invalid Authorization header");
            ErrorResponse(response, map);
            return false;
        }
        try {
            Map<String, Claim> map = JwtUtil.verifyToken(headToken);
        } catch (Exception e) {
            e.printStackTrace();
            Map<String, Object> map = new HashMap<>();
            map.put("code", 20002);
            map.put("message", "Invalid Authorization header " + e.getLocalizedMessage());
            ErrorResponse(response, map);
            return false;
        }
        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 {
    }

    // 被拦截的请求响应
    private void ErrorResponse(HttpServletResponse response, Map<String, Object> result){
        OutputStream out = null;

        JSONObject object = new JSONObject();
        object.put("result", result);

        try{
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/json");
            out = response.getOutputStream();
            out.write(object.toString().getBytes());
            out.flush();
        }catch (Exception e){
            e.printStackTrace();
        }finally{
            try {
                if (out != null) {
                    out.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

5.jwt拦截器配置

配置拦截模式
此处踩了三个坑:静态资源也被拦截了(三、1);项目/error路径也会被拦截(三、2);yml中设置了项目路径时,拦截路径中需要加上该路径(三、3)

package com.ymzhao.website.config;

import com.ymzhao.website.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.annotation.Resource;

/**
 * Create By ymzhao on 2021/3/11
 */
@Configuration
public class JwtInterceptorConfig extends WebMvcConfigurationSupport {

    @Resource
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,要声明拦截器对象和要拦截的请求
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**") //所有路径都被拦截
                .excludePathPatterns("/login/**") // 排除用户登录请求
                .excludePathPatterns("/register/**") // 排除用户注册请求
                .excludePathPatterns("/error");
        super.addInterceptors(registry);
    }

    /**
     * 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }
}

二、jwt前端配置

同后端的约定,登录后,返回的token存放在之后的请求头中,属性名为Authoritization

1.获取token

使用封装的axios方法(见下一节)发送请求

this.fetchPost("/zymwb/login/log_in", {username: 'admin', password: '123abc'}).then(res => {
		console.log(res)
		const result = res.data // 示例中返回的result结构为{code: 0, data: 'xxx', message: '成功'}
		if(result != null && result.code == 0) { // 登录成功
			localStorage.setItem('token', result.data)
		} else { // 登录失败
		}
	});

2.封装axios

import Vue from 'vue';
import axios from 'axios';
import qs from 'qs';

const host = 'http://192.168.0.179';
const baseURL = host + ':13145';

axios.defaults.withCredentials = false;
axios.defaults.timeout = 2500;

// 请求拦截
axios.interceptors.request.use(
  config => {
	const token = localStorage.getItem("token")
	config.headers.Authoritization = token
	return config;
  },
  err => {
    return Promise.reject(err); 
  }
)
// 响应拦截
axios.interceptors.response.use(
	res => {
    return res;
  },
  err => {
    return Promise.reject(err);
  }
)
const fetch = (url, method, data) => {
  data = data ? data : {};

  let httpDefaultOpts = { //http默认配置
    method: method,
    url: baseURL + url,
    params:data,
    data:qs.stringify(data),
    headers: method=='get'?{
      "Accept": "application/json",
      "Content-Type": "application/json; charset=UTF-8"
    }:{
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
    }
  }
 
  if(method =='get'){
    delete httpDefaultOpts.data;
  }else{
    delete httpDefaultOpts.params;
  }
    
  let promise = new Promise(function(resolve, reject) {
    axios(httpDefaultOpts).then(
      res => {
        resolve(res);
      }
    ).catch(
      res => {
        reject(res);
      }
    )
  })
  return promise;
}

const fetchGet = (url, data) => {
	return fetch(url, "get", data);
}
const fetchPost = (url, data) => {
	return fetch(url, "post", data);
}

Vue.prototype.fetchGet = fetchGet;
Vue.prototype.fetchPost = fetchPost;

三、踩坑记录

1.静态资源被拦截(一、5)

BUG记录:SpringBoot项目配置拦截器,部署后出现无法调用接口的问题

2./error被拦截(一、5)

springboot的错误处理url为/error也会被拦截,需要添加排除

3.yml项目路径(一、5)

yml中设置了项目路径时,拦截路径中需要加上该路径(三、3)

server:
  port: 13145
  servlet:
    context-path: /zymwb
registry.addInterceptor(jwtInterceptor)
		.addPathPatterns("/zymwb/**") //所有路径都被拦截
		.excludePathPatterns("/zymwb/login/**") // 排除用户登录请求