JWT(java web token)
JWT(java web token)JWT包含三部分: Header 头部 Payload 负载 Signature 签名其结构看起来是这样的 Header.Payload.Signature。#JWT的组成##Header在header中通常包含了两部分:token类型和采用的加密算法。{ “alg”: “HS256”, “typ”: “JWT”}接下来对这部分内容使用 Base64Url 编
JWT(java web token)
JWT包含三部分: Header 头部 Payload 负载 Signature 签名
其结构看起来是这样的 Header.Payload.Signature。
#JWT的组成
##Header
在header中通常包含了两部分:token类型和采用的加密算法。{ “alg”: “HS256”, “typ”: “JWT”}
接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。
##Payload
它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim:reserved, public 和 private.Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者),exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间)。 Public claims:根据需要定义自己的字段,注意应该避免冲突 Private claims:这些是自定义的字段,可以用来在双方之间交换信息 负载使用的例子:{ “sub”: “1234567890”, “name”: “John Doe”, “admin”: true}
负载需要经过Base64Url编码后作为JWT结构的第二部分
##Signature
建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建: HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret) 签名用于验证消息的发送者以及消息是没有经过篡改的。
#JWT的使用
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息里面。另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
#JWT的使用场景
Authorization (授权) :
这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
Information Exchange (信息交换) :
对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
#JWT和Session的区别
相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
JWT的客户端和服务端的原理
从下图可以看到:
- JWT是由服务端产生的,返回给客户端
- 客户端每次请求服务端的时候都要带上JWT
- 服务端去通过过滤器拦截请求,验证JWT
基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
1: 用户携带用户名和密码请求访问。
2: 服务器校验用户凭据。
3: 应用提供一个token给客户端。
4: 客户端存储token,并且在随后的每一次请求中都带着它。
5: 服务器校验token并返回数据。
注意:
1: 每一次请求都需要token。
2: Token应该放在请求header中。
3:我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *。
4.为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
#JWT的使用
第一步:添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>2.2.0</version>
</dependency>
第二步:创建工具类
import com.auth0.jwt.JWTSigner;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
public class JWTUtils {
private static final String SECRET = "XX#$%()(#*!()!KL<><MQLMNQNQJQK sdfkjsdrow32234545fdf>?N<:{LWPW";
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
//加密,传入一个对象和有效期
public static <T> String sign(T object, long maxAge) {
try {
final JWTSigner signer = new JWTSigner(SECRET);
final Map<String, Object> claims = new HashMap<String, Object>();
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(object);
claims.put(PAYLOAD, jsonString);//传输相关信息
claims.put(EXP, System.currentTimeMillis() + maxAge);//过期时长
return signer.sign(claims);
} catch (Exception e) {
return null;
}
}
//解密,传入一个加密后的token字符串和解密后的类型
public static <T> T unsign(String jwt, Class<T> classT) {
final JWTVerifier verifier = new JWTVerifier(SECRET);
try {
final Map<String, Object> claims = verifier.verify(jwt);
if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
long exp = (Long) claims.get(EXP);
long currentTimeMillis = System.currentTimeMillis();
if (exp > currentTimeMillis) {
String json = (String) claims.get(PAYLOAD);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, classT);
}
}
return null;
} catch (Exception e) {
return null;
}
}
}
第三步:把token放进header中
第四步:每次请求都验证token(通过aop实现拦截)
前端需要把请求头中放入token参数,里面放入最开始后端返回的token
import com.rongji.common.annotation.Log;
import com.rongji.common.config.Constant;
import com.rongji.common.entity.LogEntity;
import com.rongji.common.service.LogService;
import com.rongji.common.utils.*;
import com.rongji.system.entity.JWTentity;
import com.rongji.system.entity.SysUserTokenEntity;
import com.rongji.system.service.SysUserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Autowired
LogService logService;
@Autowired
SysUserService userService;
@Pointcut("execution(* com.*.controller..*(..)))")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) {
//判断token信息
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
String token = request.getHeader("token");
//如果传的值不存在:1:登录 2:过期
String uri = request.getRequestURI();
String method = request.getMethod();
boolean isLogin = StringUtils.equals("/login", uri) && StringUtils.equals("POST", method);
JWTentity user = null;
R r =R.error(Constant.jwtErrorCode, "session失效,请重新登录");
if (StringUtils.isEmpty(token)) {
//进入的是登录界面
if (!isLogin) {
//其他接口:session过期
return r;
}
} else if (!isLogin) {
//校验
user = JWTUtils.unsign(token, JWTentity.class);
if (user == null) {
//返回错误信息
return r;
} else {
Long userId = user.getUserId();
//判断是否在黑名单里面
List tokens = userService.getUserToken(userId);
if (tokens.contains(token)) {
return r;
}
// 执行时长(毫秒)
long time = Constant.jwtMaxAge - user.getLoginDate().getSeconds();
if (0 < time && time <= Constant.jwtDelayMinAge) {
return r;
}
//延时
delayTime(token, user, userId, time);
}
}
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = null;
try {
result = point.proceed();
} catch (Throwable throwable) {
logger.error(throwable.toString());
}
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//异步保存日志
saveLog(point, time, user);
return result;
}
private void delayTime(String token, JWTentity user, Long userId, long time) {
if (time > 0 && time < Constant.jwtDelayMaxAge) { //延时
HttpServletResponse response = HttpContextUtils.getHttpServletResponse();
user.setLoginDate(new Date());
String jwtToken = JWTUtils.sign(user, Constant.jwtMaxAge);
response.setHeader("token", jwtToken);
//保存黑名单
SysUserTokenEntity userToken = new SysUserTokenEntity();
userToken.setUserId(userId);
userToken.setTokenId(token);
userToken.setOperDate(new Date());
userService.saveUserTokenEntity(userToken);
}
}
void saveLog(ProceedingJoinPoint joinPoint, long time, JWTentity user) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogEntity sysLog = new LogEntity();
//Log syslog = method.getAnnotation(Log.class);
//if (syslog != null) {
// 注解上的描述
// sysLog.setOperation(className + "." + methodName + "()");
// }
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的参数
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
Object[] args = joinPoint.getArgs();
// String params1 = JSONUtils.beanToJson(args[0]);
// sysLog.setParams(params1);
// 获取request
// HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
StringBuffer url = request.getRequestURL();
String uri = request.getRequestURI();
// 设置IP地址
String ip = IPUtils.getIpAddr(request);
sysLog.setIp(ip);
if (user != null) {
Long userId = user.getUserId();
if (userId != null) {
sysLog.setUserId(userId);
}
String userName = user.getUserName();
if (StringUtils.isNotEmpty(userName)) {
sysLog.setUsername(userName);
}
}
sysLog.setTime((int) time);
// 系统当前时间
Date date = new Date();
sysLog.setGmtCreate(date);
// 保存系统日志
logService.save(sysLog);
}
}
第五步:由于前后端分离问题,需要在response中设置参数:
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpStatus;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "CORSFilter")
public class CORSFilter implements Filter {
private static final long serialVersionUID = 1L;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request=(HttpServletRequest)servletRequest;
response.setContentType("text/plain;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS");
response.setHeader("Access-Control-Max-Age", "1728000");
response.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, " +
"Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("XDomainRequestAllowed","1");
//添加可以返回自定义header信息
response.setHeader("Access-Control-Expose-Headers","token");
if ("OPTIONS".equals(request.getMethod())){//这里通过判断请求的方法,判断此次是否是预检请求,如果是,立即返回一个204状态吗,标示,允许跨域;预检后,正式请求
response.setStatus(HttpStatus.SC_NO_CONTENT); //HttpStatus.SC_NO_CONTENT = 204
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
/**
* 重新封装request包装类
*/
class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private String url;
public MyHttpServletRequestWrapper(HttpServletRequest request,String url) {
super(request);
this.url=url;
}
@Override
public String getServletPath() {
if(super.getDispatcherType().name().equals("REQUEST")) {
return url;
} else {
return super.getServletPath();
}
}
}
}
更多推荐
所有评论(0)