package com.whyc.manager; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.TokenExpiredException; import com.whyc.config.JwtProperties; import com.whyc.pojo.User; import com.whyc.util.CommonUtil; import com.whyc.util.EncodesUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.*; /** * JWT 管理: * state-base: session,基于状态,用户登录状态 保存于Session中,server-client是关联的 * stateless-base: jwt,基于无状态,用户的状态 未保存在Session中,所以web请求可以由任何Server应答,发送的请求头要求包含jwt信息,以便服务器能理解状态 * * JWT的应用场景: * 优点: * 1.基于jwt的特性:由于不可篡改和有效期固定,非常适合用于一次性验证 * 2.用户禁用cookie时,可以使用 * 3.在分布式会话中,某些场景可以使用jwt,因为是无状态的. * 4.前后端分离,cookie在跨域访问禁止携带,jwt适用于跨域认证 * 缺点: * 1.如果做会话管理: * -无法续签,用于会话管理需要继承其他框架比如spring security auth2 jwt * -做会话管理的话,如果cookie泄露,无法拒绝 jwt验证,因为是计算机计算校验合法性;而session的话清除session可以导致登录验证失败 * -jwt字段长,会造成http请求开销变大 * * 本项目使用jwt,需要继承续签框架 * */ @Component @Slf4j public class JWTManager { @Autowired JwtProperties jwtProperties; /** * 签发令牌: * jwt字符串三部分: * 1.header * -字符串类型,一般为"JWT" * -算法加密类型,一般为"HS256"或其他加密算法 * 2.payload * 常见以下四个标准字段: * st:签发时间 * jti:JWT唯一标识 * signer:签发人,一般为username或者userId * exp:过期时间 * 3.signature * 对1和2进行header中的算法加入签证秘钥,防止令牌被篡改 * @param signer 签发人 * @param ttlMillis 令牌有效时长 * @param sessionId 唯一标识 * @param claims 声明map,可携带其他自定义信息 * @return */ public String issueToken(String signer, long ttlMillis, String sessionId, Map claims){ if(claims==null){ claims = new HashMap<>(); } long mowMillis = System.currentTimeMillis(); //签名秘钥 String hexEncodedSecretKey = EncodesUtil.encodeHex(this.jwtProperties.getHexEncodedSecretKey().getBytes()); JwtBuilder jwtBuilder = Jwts.builder() .setClaims(claims) //唯一标识 .setId(sessionId) //签发时间 .setIssuedAt(new Date(mowMillis)) //签发人 .setSubject(signer) //签证秘钥 .signWith(SignatureAlgorithm.HS256, hexEncodedSecretKey.getBytes()); if(ttlMillis>=0) { //过期时间 long expMillis = mowMillis + ttlMillis; Date exp = new Date(expMillis); jwtBuilder.setExpiration(exp); } return jwtBuilder.compact(); } /** * 解析令牌 * @param jwt 令牌 * @return */ public Claims decodeToken(String jwt){ String encodeHex = EncodesUtil.encodeHex(jwtProperties.getHexEncodedSecretKey().getBytes()); //得到DefaultJwtParser return Jwts.parser() //设置签名的秘钥 .setSigningKey(encodeHex.getBytes()) //可以偏移24h,过期1天内可以获取.超过1天会登录失败 .setAllowedClockSkewSeconds(86400000) //设置需要解析的jwt .parseClaimsJws(jwt) .getBody(); } /** * 判断令牌是否合法 * @param jwt 令牌 * @return */ public boolean verifyToken(String jwt, ServletRequest request){ boolean res = false; String encodeHex = EncodesUtil.encodeHex(jwtProperties.getHexEncodedSecretKey().getBytes()); //这是官方的校验规则,可以自定义 Algorithm algorithm = Algorithm.HMAC256(encodeHex); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); try { //校验不通过会抛出异常 //判断合法的标准:1.header和payload没有被篡改 // 2.没有过期 //增加:首先判断jwt是否有效,在内存中存在,实际上已经将jwt状态化了 List jwts = (List) request.getServletContext().getAttribute("jwts"); if(jwts!=null && jwts.contains(jwt)) { jwtVerifier.verify(jwt); res = true; } }catch (TokenExpiredException expiredException){ log.error("expiredException"); //对于空闲时间小于30分钟的,过期了也可以通过校验,变相续签 Claims claims = decodeToken(jwt); String userJson = (String) claims.get("user"); User user = CommonUtil.toUser(userJson); Date date = new Date(); //System.out.println("当前时间:"+date); Date date1 = new Date((Long) request.getServletContext().getAttribute("exp_" + user.getName())); //System.out.println("最新活跃时间:"+date1); if (System.currentTimeMillis()-(Long) request.getServletContext().getAttribute("exp_" + user.getName()) <= 180000) { //System.out.println("距离最新活跃时间小于3分钟"); res = true; } }catch (Exception e){ log.error("exception"); } return res; } }