JSON Web Token(JSON Web令牌)
JWT作用: 授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。它的开销很小并且可以在不同的域中使用。如:单点登录。 信息交换:在各方之间安全地传输信息。JWT可进行签名(如使用公钥/私钥对),因此可确保发件人。由于签名是使用标头和有效负载计算的,因此还可验证内容是否被篡改。
请求流程
- 前端通过Web表单将自己的用户名和密码发送到后端的接口。该过程一般是HTTP的POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage(浏览器本地缓存)或sessionStorage(session缓存)上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP的Header中的Authorization位。(解决XSS和XSRF问题)HEADER
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确﹔检查Token是否过期;检查Token的接收方是否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果
JWT结构
就是令牌token,是一个String字符串,由3部分组成,中间用点隔开
令牌组成:
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
token格式:head.payload.singurater 如:xxxxx.yyyy.zzzz
JWT基本使用
1.引入依赖
<!--引入JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
2.生成token
HashMap<String,Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,20);
String token = JWT.create()
.withHeader(map) //可以不设定,就是使用默认的
.withClaim("userId",20)//payload //自定义用户名
.withClaim("username","zhangsan")
.withExpiresAt(instance.getTime()) //指定令牌过期时间
.sign(Algorithm.HMAC256("fdahuifeuw78921"));//签名
3.根据令牌和签名解析数据
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("fdahuif921")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
decodedJWT.getClaim("userId").asInt();//获取负载里面对应的内容
decodedJWT.getClaim("username").asString();
decodedJWT.getExpiresAt();//获取过期时间
4.常见异常信息
SignatureVerificationException //签名不一致异常
TokenExpiredException //令牌过期异常
AlgorithmMismatchException //算法不匹配异常
InvalidClaimException //失效的payload异常(传给客户端后,token被改动,验证不一致)
SpringBoot整合JWT以及JWT工具类
1.JWT工具类
public class JWTUtils {
private static String SIGNATURE = "token!@#$%^7890";
/**
* 生成token
* @param map //传入payload
* @return 返回token
*/
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(SIGNATURE)).toString();
}
/**
* 验证token
* @param token
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
}
2.登录成功生成token
//controller层接收数据,生成token,并响应
Map<String,Object> map = new HashMap<>();
try{
User userDB = userService.login(user);
Map<String,String> payload = new HashMap<>();
payload.put("id",userDB.getId());
payload.put("name",userDB.getName());
//生成JWT令牌
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);//响应token
} catch (Exception e) {
map.put("state","false");
map.put("msg",e.getMessage());
}
3.声明一个token拦截器
package com.liup.interceptor;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.office.utils.JWTUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* JWT验证拦截器
*/
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
//令牌建议是放在请求头中,获取请求头中令牌
String token = request.getHeader("token");
try{
JWTUtils.verify(token);//验证令牌
return true;//放行请求
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","token算法不一致");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","token失效");
}
map.put("state",false);//设置状态
//将map转化成json,response使用的是Jackson
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
return false;
}
}
4.配置拦截器
package com.liup.config;
import com.office.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/**");
}
}
微服务项目,网关过滤器进行token验证
package com.heima.app.gateway.filter;
import com.heima.app.gateway.utils.AppJwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 网关过滤器
*
* @Author Vsunks.v
* @Date 2022/8/26 16:43
* @Blog blog.guolei.net/996.mba
* @Description: 网关过滤器
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//判断是否是登录请求
if (request.getURI().getPath().contains("login_auth")) {
//是登录请求则放行
return chain.filter(exchange);
}
//不是登录请求则进行下一步,获取token令牌
//从请求头中获取token令牌
String token = request.getHeaders().getFirst("token");
//判断token是否存在,不存在则返回401
if (token == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//判断token是否有效,无效则返回401
try {
JWTUtils.verify(token);//验证令牌
} catch (Exception e) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//获取token令牌有效,放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
因为用户请求发送到网关,再由网关路由到微服务是两个请求,所以先在网关过滤器中解析token,获取用户数据,再将用户数据封装到到微服务的请求头中。最后在微服务中的拦截器中获取用户数据并添加进本地线程中以供使用
网关过滤器配置
import com.heima.wemedia.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//2.判断是否是登录
if (request.getURI().getPath().contains("/login")) {
//放行
return chain.filter(exchange);
}
//3.获取token
String token = request.getHeaders().getFirst("token");
//4.判断token是否存在
if (StringUtils.isBlank(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//5.判断token是否有效
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
//是否是过期
int result = AppJwtUtil.verifyToken(claimsBody);
if (result == 1 || result == 2) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//把解析的token令牌信息中的用户id放入到路由微服务的请求头中
Object id = claimsBody.get("id");
ServerHttpRequest httpRequest = request.mutate().headers(c -> {
c.add("id", id.toString());
}).build();
exchange.mutate().request(httpRequest).build();
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//6.放行
return chain.filter(exchange);
}
/**
* 优先级设置 值越小 优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
微服务拦截器配置
/**
* springmvc配置类
*
* @Author Vsunks.v
* @Date 2022/8/29 10:44
* @Blog blog.guolei.net/996.mba
* @Description: springmvc配置类
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");
}
}
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.thread.WmThreadLocalUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 获取用户ID
*
* @Author Vsunks.v
* @Date 2022/8/29 10:10
* @Blog blog.guolei.net/996.mba
* @Description: 获取用户ID
*/
public class WmTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String id = request.getHeader("id");
if (StringUtils.isNotBlank(id)) {
WmUser wmUser = new WmUser();
wmUser.setId(Integer.valueOf(id));
WmThreadLocalUtil.setUser(wmUser);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
WmThreadLocalUtil.clear();
}
}
用到的线程工具类
import com.heima.model.wemedia.pojos.WmUser;
public class WmThreadLocalUtil {
private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 添加用户
* @param wmUser
*/
public static void setUser(WmUser wmUser){
WM_USER_THREAD_LOCAL.set(wmUser);
}
/**
* 获取用户
*/
public static WmUser getUser(){
return WM_USER_THREAD_LOCAL.get();
}
/**
* 清理用户
*/
public static void clear(){
WM_USER_THREAD_LOCAL.remove();
}
}