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;
}
}