JWT与Annotation和AOP的结合使用

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

引入依赖

示例项目用到的主要依赖,不是全部。

<!--        JWT 相关依赖 -->
        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
<!--        阿里巴巴的fastJson库,用起来比jdk的Json库好用  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </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提交

在这里插入图片描述
登录拿jwt
登录(拿jwt)的接口可别再用jwt效验了,不然就成一个闭环了。
在这里插入图片描述
用登录拿到的jwt访问接口
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Base64编码和JWT(JSON Web Token)通常结合使用,以便在Web应用程序中进行身份验证和授权。 JWT是一种在客户端和服务器之间安全传输信息的方法,它将信息编码为JSON格式,并使用Base64进行编码。JWT由三个部分组成:头部,负载和签名。 在实现JWT时,Base64编码主要用于将JSON格式的头部和负载转换为Base64编码字符串,并将它们连接起来。然后,使用算法(如HMAC或RSA)将其与密钥一起签名,形成一个安全的JWT令牌,以便在请求中进行传输。 以下是使用Base64.js和JWT结合使用的示例代码: ```javascript // 安装 Base64.js 和 jsonwebtoken 模块 const Base64 = require('Base64'); const jwt = require('jsonwebtoken'); // 设置 JWT 的头部信息 const header = { "alg": "HS256", "typ": "JWT" }; // 设置 JWT 的负载信息 const payload = { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }; // 将头部和负载信息进行 Base64 编码 const encodedHeader = Base64.encode(JSON.stringify(header)); const encodedPayload = Base64.encode(JSON.stringify(payload)); // 使用秘钥对头部和负载信息进行签名 const secret = 'your-256-bit-secret'; const signature = jwt.sign(`${encodedHeader}.${encodedPayload}`, secret); // 将编码后的头部、负载和签名信息连接起来,形成一个 JWT 令牌 const token = `${encodedHeader}.${encodedPayload}.${signature}`; console.log(token); ``` 当服务器收到此JWT令牌时,它将使用与签名时相同的密钥和算法来验证令牌的完整性,并提取出负载信息。如果签名无效或已被篡改,则服务器将拒绝令牌,并返回错误响应。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值