这是一个完整的JWT应用实例项目,使用jjwt简化JWT的生成和效验,可使代码更少,程序更稳健。用切面织入需要效验的接口,全面拦截简化重复劳动,使用注解定义关注点,既灵活又方便。
使用JWT可以给我们带来什么:
灵活性提升、减少服务端内存占用,单点登录、使接口调用更加体系化。
首先是JWT Token里存储了一些数据、这些数据往往是会话相关的,这直接减少了服务端存储session数据的内存量。
使用JWT可以使多个session关联到一个用户身份,这个用session共享也能做到,但是我们使用redis管理session的话本身也是个消耗,特别是对于规模小并且上线周期短的项目,毕竟部署、管理redis也是成本。
我们经常可以遇到加签名调用API增加安全性的情况,比如加个sign参数,将接口其它参数排序、拼接、加密,然后接口内部再解密,过程是比较繁琐的,JWT完全可以取缔这种接口签名方案,JWT是个标准实现,还可以携带一些紧凑的信息,明显比各家自己加接口签名更优。
JWT有个特性需要注意,一般我们会把jwt放到客户端的cookie、sessionStorage、localStorage里,当把当前有效的jwt token拿到另一客户端或者IP请求会被认为是“有效请求”,这是个两面性的特性,对有些系统这样会使系统更简单、或者是对于某些业务系统是个必须的特性,但是对于一些系统的应用这个特点会被认为有害的,规避这个特点也很简单;比如将登录时发起请求的客户端IP放到JWT或者其它位置,与当前JWT token绑定,既将有效jwt token用到另一个IP发起请求时系统会效验到请求IP与jwt token的IP不一致,此时可进行处理(比如中止请求)。
生成JWT时,携带信息 claims 和 payload 二选一
使用jjwt的jwtBuild设置jwt附带信息(setIssuer、setId)的话跟claims和payload也有冲突,所以实际上是三种方式选择一种。
使用claims可以使用map比较舒服。
使用payload可以选择用JSON,或者序列化之后的对象,或者直接用String。
使用claims之后会把 setId、setIssuer、setSubject … 覆盖。
claims和payload同时使用会报错。
推荐使用claims。
使用到了:
示例代码仓库 》 ExampleProject
示例项目用到的主要依赖,不是全部。
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.8.2version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.1.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.4version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
<scope>providedscope>
dependency>
package cn.CommonUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Map;
@Slf4j
public class SignatureJwtUtils {
/**
* 生成JWT
* 2019年11月16日
* @throws UnsupportedEncodingException
*/
public static String generateJWT(String secret , SignatureAlgorithm signatureAlgorithm , Long expirationMillisecond , Map headers , Map claims) {
Long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
byte[] encodedKey = new byte[0];
try {
encodedKey = secret.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("生成JWT出错!");
}
SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
/**
* 生成JWT时,携带信息 claims 和 payload 二选一
* 一种是map
* 一种可以选择用JSON,或者序列化之后的对象
* 使用claims之后会把 setId、setIssuer、setSubject .. 覆盖
* 使用payload时,自己需要携带的任何信息都要用payload
*
* 推荐使用claims
*/
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(signatureAlgorithm, secretKey);
if (MapUtils.isNotEmpty(headers)) {
jwtBuilder.setHeader(headers);
}
if (MapUtils.isNotEmpty(claims)) {
jwtBuilder.setClaims(claims);
}
Date expiration = new Date(nowMillis + expirationMillisecond);
jwtBuilder.setExpiration(expiration);
log.info(jwtBuilder.compact());
log.info(jwtBuilder.toString());
return jwtBuilder.compact();
}
/**
* 解析JWT
* 2019年11月16日
* @throws UnsupportedEncodingException
*/
public static Claims translateJWT(String jwt , String secret , SignatureAlgorithm signatureAlgorithm ,Long expirationMillisecond ) {
Long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
byte[] encodedKey = new byte[0];
try {
encodedKey = secret.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
log.error("解析JWT时,encode密钥时出错!");
}
SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
try {
String signature = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getSignature();
io.jsonwebtoken.Header header = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getHeader();
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
log.info("############## signature ##########################################################");
log.info(signature);
log.info("################# header #################################################");
header.keySet().forEach(k->{
log.info(k+ ":" +header.get(k).toString());
});
log.info("############ claims ############################################################");
claims.keySet().forEach(k->{
log.info(k+ ":" + claims.get(k).toString());
});
log.info("########################## expiration #######################################");
log.info(claims.getExpiration().toLocaleString());
log.info("currentTimeMillis : "+ System.currentTimeMillis());
log.info("JWT expiration TimeMillis : "+ claims.getExpiration().getTime());
if (System.currentTimeMillis() > claims.getExpiration().getTime()) {
log.warn("JWT expirationTime had been over !");
}else{
return claims;
}
} catch (io.jsonwebtoken.ExpiredJwtException eje) {
eje.getStackTrace();
log.error(eje.getLocalizedMessage());
log.info("############## signature ##########################################################");
// log.info(signature);
log.info("################# header #################################################");
eje.getHeader().keySet().forEach(k->{
log.info(k+ ":" +eje.getHeader().get(k).toString());
});
log.info("############ claims ############################################################");
eje.getClaims().keySet().forEach(k->{
log.info(k+ ":" + eje.getClaims().get(k).toString());
});
}
catch (io.jsonwebtoken.MalformedJwtException mje) {
mje.getStackTrace();
log.error(mje.getLocalizedMessage());
}
return null;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignatureJwt {
public enum Status {Dead,Start}
ToDo.Status status() default ToDo.Status.Dead;
}
package cn.Aop;
import cn.CommonUtils.SignatureJwtUtils;
import cn.MetaData.Annotation.SignatureJwt;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Aspect
@Slf4j
public class FrontendInterfaceSignatureAop {
/**
* 注解切点
* 带参
*/
@Pointcut(value = "@annotation(cn.MetaData.Annotation.SignatureJwt) && args(..)" )
public void PointcutAnnotationWithParameter(){}
/**
* 带参 关注点对象 处理JWT
* 需要把jwt放在最后一个参数
* @param proceedingJoinPoint
* @return
*/
@Around(value = "PointcutAnnotationWithParameter()")
public Object oneParamBeforeHandlerByAnnoWithParam2(ProceedingJoinPoint proceedingJoinPoint ){
log.info("$$$$$$$$$$$$$$$$$$$$$$ 注解切点 抓取关注点参数 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$");
log.info("Come in Aop Procduce : "+ "powerd by annotation" + " JoinPoint");
String jwtSignature = (String) proceedingJoinPoint.getArgs()[proceedingJoinPoint.getArgs().length - 1 ];
if (StringUtils.isEmpty(jwtSignature)) {
JSONObject result = new JSONObject();
result.put("status", "invail login");
return result;
}
Claims claims = SignatureJwtUtils.translateJWT(jwtSignature, "secret", SignatureAlgorithm.HS384, 30 * 60 * 1000L);
if (claims == null) {
JSONObject result = new JSONObject();
result.put("status", "invail login claims is null");
return result;
}
log.info("$$$$$$$$$$$$$$$$$$$$$$ 注解切点 抓取关注点参数 $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$");
try {
Object result = proceedingJoinPoint.proceed();
return result;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
@Slf4j
@Controller
public class FourthController {
@RequestMapping("/fourth/test1")
@ResponseBody
@SignatureJwt
public JSONObject test1Proc(String id, String name , HttpServletRequest request , HttpServletResponse response , String jwt) {
JSONObject result = new JSONObject();
result.put("status", "success");
log.info("############ this is fourth test procduce ... ");
return result;
}
@RequestMapping("/fourth/login")
@ResponseBody
public JSONObject test1Proc( String name , String pwd , HttpServletRequest request , HttpServletResponse response ) {
JSONObject result = new JSONObject();
if ("tom".equals(name) && "123".equals(pwd)) {
String jwt = SignatureJwtUtils.generateJWT("secret", SignatureAlgorithm.HS384, 30 * 60 * 1000L, null, null);
result.put("status", "success");
result.put("jwt", jwt);
return result;
}
result.put("status", "fail");
log.info("############ this is fourth test procduce ... ");
return result;
}
/**
* 在使用jwt时,用@PathVariable风格接收参数要注意 jwt token 会以 . 被分为三段
* @param id
* @param name
* @param request
* @param response
* @param jwt
* @return
*/
@RequestMapping("/fourth2/{jwt}")
@ResponseBody
@SignatureJwt
public JSONObject test1Proc2(String id, String name , HttpServletRequest request , HttpServletResponse response , @PathVariable(name = "jwt") String jwt) {
JSONObject result = new JSONObject();
result.put("status", "success");
log.info("############ this is fourth test procduce . 2 .. ");
return result;
}
}
在做接口jwt签名时,最好把jwt参数放到第一个或者最后一个位置,
我这里选择放到最后。
随便整个jwt提交