JWT——Json web token
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,可实现无状态、分布式的Web应用授权。
JWT包含了使用.分隔的三部分: Header 头部 Payload 负载 Signature 签名
其结构看起来是这样的Header.Payload.Signature
Header
在header中通常包含了两部分:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。
Payload
Token的第二部分是负载,它包含了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格式的输出是以. 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。 下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名。
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息里面。另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
下列场景中使用JSON Web Token是很有用的:
Authorization (授权) :
这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现
在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
Information Exchange (信息交换) :
对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证
传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。
这种基于服务器的身份认证方式存在一些问题:
Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
Scalability : 由于Session是在内存中的,这就带来一些扩展性的问题。
CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用AJAX调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
CSRF : 用户很容易受到CSRF攻击。
相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。
Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。
而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。
Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。
基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。
没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。
虽然这一实现可能会有所不同,但其主要流程如下:
1: 用户携带用户名和密码请求访问。
2: 服务器校验用户凭据。
3: 应用提供一个token给客户端。
4: 客户端存储token,并且在随后的每一次请求中都带着它。
5: 服务器校验token并返回数据。
注意:
1: 每一次请求都需要token。
2: Token应该放在请求header中。
3:我们还需要将服务器设置为接受来自所有域的请求,用Access-Control-Allow-Origin: *。
优点:
1:支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
2:无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
3:更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
4:去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
5:更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
6:CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
7:性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
8:基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。
缺点:
1:JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
2:JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令×××的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
3:为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
1:引入依赖
2:工具类
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<>?N<:{LWPW";
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
//加密,传入一个对象和有效期
public static String sign(T object, long maxAge) {
try {
final JWTSigner signer = new JWTSigner(SECRET);
final Map claims = new HashMap();
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 unsign(String jwt, Class classT) {
final JWTVerifier verifier = new JWTVerifier(SECRET);
try {
final Map 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;
}
}
}
3:登录放在header中返回给前台。
public static Long jwtMaxAge = 30L * 60L * 1000L。半小时。
4:每次请求验证
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);
}
}
前台vue实现,接收token,并放在header中每次请求带着:
import axios from 'axios';
import Qs from 'qs';
import router from '../router';
import {MessageBox} from 'element-ui';
let host = "http://192.168.1.178:7171";
const instance = axios.create({
baseURL: host,
headers: {'content-type': 'application/x-www-form-urlencoded'},
withCredentials: true
});
instance.interceptors.request.use(config => {
// 判断是否存在token,如果存在的话,则每个http header都加上token
let jsessionid = localStorage.getItem("token");
if(jsessionid) {
config.headers.token = jsessionid;
}
return config;
})
instance.interceptors.response.use(res =>{
let ssession = res.headers.token;
if(ssession){
localStorage.setItem("token",ssession);
}
return handleResponse(res);
})
问题:由于前后端分离问题,需要在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();
}
}
}
}