Json Web Token

JWT

Json Web Token

web应用中,能够携带用户信息,带有数字签名的JSON字符串。

通常简称为token。

使用JWT后的访问流程:

用户登录时,访问一个"验证服务",生成一个特殊的字符串给客户端。

这个字符串中,保存了用户信息,还有数字签名。

用户下次登录时,再次访问这个"验证服务",只需按签发时的签名规则解密,就能判断能否访问,同时也能获取保存在其中的信息。

这个特殊的字符串,就成为token,包含三部分,用.隔开:“头.负载.签名”

JWT头				   负载信息					数字签名
aaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccc

优点

  • 无需保存在服务端,减少服务器压力
  • 结构简单,占用字节少,方便传输
  • 可以携带用户信息
  • JSON格式通用,可以跨语言

基本使用

导入依赖

<!-- JWT-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--JDK1.8以上添加才能使用JWT-->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

创建jwt工具类

  • 生成token时的秘钥是一个BASE64编码的字符串,该字符串长度至少为4

    String secret=Base64.getEncoder().encodeToString("hqyj".getBytes());
    

    在util包下建立一个JwtUtil工具类,创建一个token令牌

package com.hqyj.erp_service.util;

import com.hqyj.erp_service.entity.SysUser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;

@Component
public class JwtUtil {

    //生成token时的密钥
    @Value("${secret}")
    String secret;

    /*
     * 创建token的方法
     * */
    public String createToken(SysUser sysUser) {
        //1.定义token的有效期。记录生成时间,设置到期时间
        //当前时间
        long now = System.currentTimeMillis();
        //有效时长1天,计算到期时间
        long exp = now + 1000 * 3600 * 24;
        //2.定义token的负载信息
        HashMap<String, Object> map = new HashMap<>();
        map.put("suId", sysUser.getSuId());
        map.put("suName", sysUser.getSuName());
        map.put("email", sysUser.getEmail());
        map.put("suRole", sysUser.getSuRole());

        //3.创建一个用于生成token的对象
        JwtBuilder builder = Jwts.builder();
        builder.setClaims(map)//设置负载信息
                .setIssuer("com.hqyj")//设置签发者
                .setIssuedAt(new Date())//设置签发时间
                .setExpiration(new Date(exp))//设置到期时间
                .signWith(SignatureAlgorithm.HS256, secret);//设置签名算法和数字签名
        //返回构建好的token字符串
        return builder.compact();
    }
}

修改controller的返回数据

在登陆成功后,调用创建token的方法,给前端返回一个token

@PostMapping("/login")
public ResultData login(SysUser sysUser) {
    sysUser.setSuPassword(DigestUtils.md5DigestAsHex(sysUser.getSuPassword().getBytes()));
    QueryWrapper<SysUser> wrapper = new QueryWrapper<>(sysUser);
    SysUser one = sysUserService.getOne(wrapper);
    if (one == null) {
        return ResultData.error(1, "用户名或密码错误");
    }
    return ResultData.ok(jwtUtil.createToken(one));
}

前端保存返回的token

在前端页面登录成功后,将返回的token保存到localStorage中。

localStorage是一种Web存储机制,可以在用户的浏览器端保存键值对。

不同域名有各自的localStorage,同一个域名下的localStorage是共享的,所以在同一个站点下可以在不同页面中共享数据。

localStrorage的存储空间大小为5MB左右。

前端解析token

  • 安装npm i jwt-decode
  • 导入import {jwtDecode} from ‘jwt-decode’
  • 使用jwtDecode(要解析的token)
const sysUser = reactive({
    suId: '',
    suName: '',
    email: '',
    suRole: ''
})

onMounted(() => {
    var token = localStorage.getItem("token");
    var res = jwtDecode(token);
    sysUser.suId = res.suId;
    sysUser.suName = res.suName;
    sysUser.email = res.email;
    sysUser.suRole = res.suRole;
})

使用拦截器+token实现拦截未登录状态下的访问

创建一个拦截器类和拦截器配置类

拦截器类

/*
 * 自定义权限拦截器
 * 实现HandlerInterceptor接口
 * 重写preHandle方法
 * */
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //该方法如果返回false,不能访问后续内容
        return false;
    }
}

拦截器配置类

/*
 * 拦截器配置类
 * 需要添加@Configuration注解,在项目启动时执行
 * 实现WebMvcConfigurer接口
 * 重写addInterceptors方法
 * 注入自定义的拦截器对象
 * */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    /*
    * 将自定义的拦截器对象注入到Spring容器中
    * */
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    /*
     * 发方法用于设置拦截信息
     * */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry的addInterceptor方法,参数为一个拦截对象
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/**")//设置要拦截的请求  这里表示拦截一切请求
                .excludePathPatterns("/sysUser/login")//放行登录的请求
                .excludePathPatterns("/sysUser/mailLogin");
    }
}

至此,后端只能访问/sysUser/login和/sysUser/mailLogin模块,其余请求访问无结果

在jwt工具类中定义验证token的方法

/*
     * 验证token
     * 根据不同异常返回对应的情况
     * 如果没有任何异常,返回保存在token中的负载信息
     * */
public ResultData validateToken(String token) {
    Claims claims;
    try {
        claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    } catch (ExpiredJwtException e) {
        //token过期时抛出
        return ResultData.error(1,"token过期");
    } catch (SignatureException e) {
        //签名异常
        return ResultData.error(2,"签名异常");
    } catch (MalformedJwtException e) {
        //token异常
        return ResultData.error(3,"token有误");
    }catch (Exception e){
        return ResultData.error(4,"解析token异常");
    }
    return  ResultData.ok(claims);
}

在拦截器中调用验证token的方法,选择是否要放行

在intercepte包下创建AuthInterceptor类

/*
 * 自定义权限拦截器
 * 实现HandlerInterceptor接口
 * 重写preHandle方法
 * */
public class AuthInterceptor implements HandlerInterceptor {

    //创建用于对象和JSON互转的工具
    ObjectMapper jsonTool = new ObjectMapper();
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        String token = request.getHeader("token");
        //判断当前的请求是否属于本项目中的某个controller中的请求
        if (handler instanceof HandlerMethod) {
            //判断当前请求是否携带token
            if (!StringUtils.hasText(token)) {
                //如果没有携带token,返回一个ResultData对象
                //创建一个ResultData对象
                ResultData res = ResultData.error(-1, "请登录(token不存在)");
                //将ResultData对象转换为JSON字符串
                String resultJson = jsonTool.writeValueAsString(res);
                //将JSON字符串写到响应中
                response.getOutputStream().write(resultJson.getBytes("utf-8"));
                response.setContentType("application/json;charset=utf-8");
                return false;
            }
            //如果携带token,解析token
            ResultData resultData = jwtUtil.validateToken(token);
            if(resultData.getCode()!=0){
                //将ResultData对象转换为JSON字符串
                String resultJson = jsonTool.writeValueAsString(resultData);
                //将JSON字符串写到响应中
                response.getOutputStream().write(resultJson.getBytes("utf-8"));
                response.setContentType("application/json;charset=utf-8");
                return  false;
            }
        }
        //该方法如果返回false,不能访问后续内容
        return true;
    }
}

在Vue项目中,使用axios访问某个controller时,携带token

axios.get("路径",
          {
            params:{
                a:x
            },
            headers:{
                token:localStrorage.getItem("token")
            }
		  });

如果Vue项目中,所有的axios请求都需要携带头信息(headers)时,使用axios拦截器

在main.ts中

import axios from 'axios'

// 所有axios请求发送时,都会携带token
axios.interceptors.request.use(config => {
    config.headers.token = localStorage.getItem("token");
    return config;
})

前端页面在使用axios访问后,要对返回值做判断

if (res.data.code == 0) {
    state.count = res.data.count;
    state.tableData = res.data.list;
} else {
    ElMessage.error(res.data.msg);
    router.replace("/")
}

使用拦截器+自定义注解实现权限控制

创建一个自定义权限注解@Auth,设置roles属性值。

在需要权限控制的地方(某个controller或某个controller中的方法)上添加注解,同时设置roles注解属性,表示什么角色能访问。

在拦截器中把要请求的方法所在类获取后,判断是否包含自定义注解,如果有获取登录时的信息,判断注解中roles值是否和登录角色的roles匹配。

当前三种角色:

ADMIN 超级管理员,可以访问任何模块,并且修改其他用户的角色

USER 普通用户,可以访问任何模块

GUEST 游客,只能访问图表模块

自定义注解

在util包下创建Auth注解

/*
* 自定义注解
* 需要添加两个注解
* @Target表示该注解在哪里使用
* @Retention表示该注解的作用域
* */
@Target({ElementType.TYPE,ElementType.METHOD})//当前表示该注解可以用在类或方法上
@Retention(RetentionPolicy.RUNTIME)//表示运行期间注解都生效
public @interface Auth {

    //定义注解的属性,当前表示角色属性。默认为"GUEST"表示游客
    String roles() default "GUEST";
}

在需要控制权限的类或方法上添加自定义注解

@Auth(roles = "ADMIN,USER")
public class WarehouseController {
    
}

在拦截器中判断当前请求是否有权限

在intercepte包下创建AuthInterceptor类

//验证权限

//获取当前的请求操作对象
HandlerMethod handlerMethod = (HandlerMethod) handler;

//获取当前要请求的类
Class<?> beanType = handlerMethod.getBeanType();
//获取当前要请求的方法
Method method = handlerMethod.getMethod();

Auth annotation = null;

//判断要请求的类上是否有自定义注解
if (beanType.isAnnotationPresent(Auth.class)) {
    //获取指定的注解
    annotation = beanType.getAnnotation(Auth.class);
}
//判断要请求的方法上是否有自定义注解
else if (method.isAnnotationPresent(Auth.class)) {
    annotation = method.getAnnotation(Auth.class);
}
//获取注解的某个属性值
String roles = annotation.roles();
//获取当前登录的用户的角色,从已对token解析后的结果中获取
String suRole = ((Claims) resultData.getObj()).get("suRole").toString();
//判断用户的角色是否与注解指定的角色匹配
if (!roles.contains(suRole)) {
    //构造一个无权限的返回值
    ResultData res = ResultData.error(-2, "当前用户无权限");
    String resultJson = jsonTool.writeValueAsString(res);
    response.getOutputStream().write(resultJson.getBytes("utf-8"));
    response.setContentType("application/json;charset=utf-8");
    return false;
}

全部文件

创建token,验证token

在工具包(util)下创建的工具类(JwtUtil)

  • 生成token:创建token的方法(createToken)
    • 1.定义token的有效期。记录生成时间,设置到期时间
    • 2.定义token的负载信息
    • 3.创建一个用于生成token的对象
  • 验证token: 根据不同异常返回对应的情况;如果没有任何异常,返回保存在token中的负载信息(validateToken)
    • token过期时抛出:ExpiredJwtException e
    • 签名异常:SignatureException e
    • token异常:MalformedJwtException e
    • 解析token异常:Exception e
package com.hqyj.erp_service.util;

import com.hqyj.erp_service.entity.SysUser;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;

@Component
public class JwtUtil {

    //生成token时的密钥
    @Value("${secret}")
    String secret;
   // String secret=Base64.getEncoder().encodeToString("hqyj".getBytes());

    /*
     * 创建token的方法
     * */
    public String createToken(SysUser sysUser) {
        //1.定义token的有效期。记录生成时间,设置到期时间
        //当前时间
        long now = System.currentTimeMillis();
        //有效时长1天,计算到期时间
        long exp = now + 1000 * 3600 * 24;
        //2.定义token的负载信息
        HashMap<String, Object> map = new HashMap<>();
        map.put("suId", sysUser.getSuId());
        map.put("suName", sysUser.getSuName());
        map.put("email", sysUser.getEmail());
        map.put("suRole", sysUser.getSuRole());

        //3.创建一个用于生成token的对象
        JwtBuilder builder = Jwts.builder();
        builder.setClaims(map)//设置负载信息
            .setIssuer("com.hqyj")//设置签发者
            .setIssuedAt(new Date())//设置签发时间
            .setExpiration(new Date(exp))//设置到期时间
            .signWith(SignatureAlgorithm.HS256, secret);//设置签名算法和数字签名
        //返回构建好的token字符串
        return builder.compact();
    }


    /*
     * 验证token
     * 根据不同异常返回对应的情况
     * 如果没有任何异常,返回保存在token中的负载信息
     * */
    public ResultData validateToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        } catch (ExpiredJwtException e) {
            //token过期时抛出
            return ResultData.error(1,"token过期");
        } catch (SignatureException e) {
            //签名异常
            return ResultData.error(2,"签名异常");
        } catch (MalformedJwtException e) {
            //token异常
            return ResultData.error(3,"token有误");
        }catch (Exception e){
            return ResultData.error(4,"解析token异常");
        }
        return  ResultData.ok(claims);
    }

}

拦截器配置类

  • 需要添加@Configuration注解,在项目启动时执行
  • 实现WebMvcConfigurer接口
  • 注入自定义的拦截器对象:将自定义的拦截器对象注入到Spring容器中
  • 重写addInterceptors方法:用于设置拦截信息
package com.hqyj.erp_service.config;

import com.hqyj.erp_service.intercepte.AuthInterceptor;
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.WebMvcConfigurer;

/*
 * 拦截器配置类
 * 需要添加@Configuration注解,在项目启动时执行
 * 实现WebMvcConfigurer接口
 * 重写addInterceptors方法
 * 注入自定义的拦截器对象
 * */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    /*
    * 将自定义的拦截器对象注入到Spring容器中
    * */
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    /*
     * 发方法用于设置拦截信息
     * */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry的addInterceptor方法,参数为一个拦截对象
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/**")//设置要拦截的请求  这里表示拦截一切请求
                .excludePathPatterns("/sysUser/login")//放行登录的请求
                .excludePathPatterns("/sysUser/mailLogin");
    }
}

自定义注解,权限控制

在工具包(util)下自定义注解(Auth):用于权限控制

  • @Target表示该注解在哪里使用
  • @Retention表示该注解的作用域
  • 定义注解的属性,当前表示角色属性。默认为"GUEST"表示游客
package com.hqyj.erp_service.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/*
* 自定义注解
* 需要添加两个注解
* @Target表示该注解在哪里使用
* @Retention表示该注解的作用域
* */
@Target({ElementType.TYPE,ElementType.METHOD})//当前表示该注解可以用在类或方法上
@Retention(RetentionPolicy.RUNTIME)//表示运行期间注解都生效
public @interface Auth {

    //定义注解的属性,当前表示角色属性。默认为"GUEST"表示游客
    String roles() default "GUEST";
}

自定义权限拦截器

在包(intercepte)下创建一个类(AuthInterceptor):自定义权限拦截器

  • 实现HandlerInterceptor接口

  • 重写preHandle方法

  • 验证权限

package com.hqyj.erp_service.intercepte;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hqyj.erp_service.util.Auth;
import com.hqyj.erp_service.util.JwtUtil;
import com.hqyj.erp_service.util.ResultData;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

/*
 * 自定义权限拦截器
 * 实现HandlerInterceptor接口
 * 重写preHandle方法
 * */
public class AuthInterceptor implements HandlerInterceptor {

    //创建用于对象和JSON互转的工具
    ObjectMapper jsonTool = new ObjectMapper();
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        //System.out.println(request.getRequestURI());
        //handler表示处理当前请求对象,可以通过判断该对象
        //System.out.println(handler);
        String token = request.getHeader("token");
        //判断当前的请求是否属于本项目中的某个controller中的请求
        if (handler instanceof HandlerMethod) {
            //判断当前请求是否携带token
            if (!StringUtils.hasText(token)) {
                //如果没有携带token,返回一个ResultData对象
                //创建一个ResultData对象
                ResultData res = ResultData.error(-1, "请登录(token不存在)");
                //将ResultData对象转换为JSON字符串
                String resultJson = jsonTool.writeValueAsString(res);
                //将JSON字符串写到响应中
                response.getOutputStream().write(resultJson.getBytes("utf-8"));
                response.setContentType("application/json;charset=utf-8");
                return false;
            }
            //如果携带token,解析token
            ResultData resultData = jwtUtil.validateToken(token);
            if (resultData.getCode() != 0) {
                //将ResultData对象转换为JSON字符串
                String resultJson = jsonTool.writeValueAsString(resultData);
                //将JSON字符串写到响应中
                response.getOutputStream().write(resultJson.getBytes("utf-8"));
                response.setContentType("application/json;charset=utf-8");
                return false;
            }
            //验证权限

            //获取当前的请求操作对象
            HandlerMethod handlerMethod = (HandlerMethod) handler;

            //获取当前要请求的类
            Class<?> beanType = handlerMethod.getBeanType();
            //获取当前要请求的方法
            Method method = handlerMethod.getMethod();

            Auth annotation = null;

            //判断要请求的类上是否有自定义注解
            if (beanType.isAnnotationPresent(Auth.class)) {
                //获取指定的注解
                annotation = beanType.getAnnotation(Auth.class);
            }
            //判断要请求的方法上是否有自定义注解
            else if (method.isAnnotationPresent(Auth.class)) {
                annotation = method.getAnnotation(Auth.class);
            }
            //获取注解的某个属性值
            String roles = annotation.roles();
            //获取当前登录的用户的角色,从已对token解析后的结果中获取
            String suRole = ((Claims) resultData.getObj()).get("suRole").toString();
            //判断用户的角色是否与注解指定的角色匹配
            if (!roles.contains(suRole)) {
                //构造一个无权限的返回值
                ResultData res = ResultData.error(-2, "当前用户无权限");
                String resultJson = jsonTool.writeValueAsString(res);
                response.getOutputStream().write(resultJson.getBytes("utf-8"));
                response.setContentType("application/json;charset=utf-8");
                return false;
            }
        }
        //该方法如果返回false,不能访问后续内容
        return true;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值