JWT与Annotation和AOP的结合使用

JWT与Annotation和AOP的结合使用

    • 引入依赖
    • 码代码
      • JWT工具类
      • 关注点注解类
      • 接口切入AOP类
      • 测试Controller类
    • 测试

这是一个完整的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。

使用到了:

  • Spring AOP
    aspectjweaver
    Lombok

示例代码仓库 》 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>


码代码

JWT工具类

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;
}

接口切入AOP类

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;
        }

    }

测试Controller类

@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参数放到第一个或者最后一个位置,
我这里选择放到最后。

直接访问 test1 接口
JWT与Annotation和AOP的结合使用_第1张图片

随便整个jwt提交

JWT与Annotation和AOP的结合使用_第2张图片
登录拿jwt
登录(拿jwt)的接口可别再用jwt效验了,不然就成一个闭环了。
JWT与Annotation和AOP的结合使用_第3张图片
用登录拿到的jwt访问接口
JWT与Annotation和AOP的结合使用_第4张图片

你可能感兴趣的:(JWT与Annotation和AOP的结合使用)