spring Cloud Gateway + JWT 实现统一的认证授权

置顶 精帖
0 1
大海无量
大海无量 博主 2019-08-25 22:28
Spring Cloud Gateway + JWT 实现统一的认证授权
项目地址:https://github.com/lizhongxiang12138/myGateway
项目结构说明
主要类说明:
JwtCheck.java --> JwtToken校验注解
JwtCheckAop.java --> JwtToken校验注解AOP
JwtTokenFilter.java -->自定义JWT 过滤器
AuthController.java -->认证测试接口
application.yml -->配置文件
JwtUtil.java --> jwt工具类

│  .gitignore
│  build.sh
│  Dockerfile
│  mvnw
│  mvnw.cmd
│  my-gateway-deployment.yaml
│  my-gateway-service.yaml
│  pom.xml
│  README.md

├─.idea
│  └─libraries
├─.mvn
└─src
    ├─main
    │  ├─java
    │  │  └─com
    │  │      └─lzx
    │  │          └─gateway
    │  │              │  MyGatewayApplication.java
    │  │              │
    │  │              ├─annotation
    │  │              │      ExecuteTime.java
    │  │              │      JwtCheck.java
    │  │              │
    │  │              ├─aop
    │  │              │      JwtCheckAop.java
    │  │              │
    │  │              ├─auth
    │  │              │      JwtTokenFilter.java
    │  │              │
    │  │              ├─config
    │  │              ├─controller
    │  │              │      AuthController.java
    │  │              │
    │  │              ├─dto
    │  │              │      ReturnData.java
    │  │              │      UserDTO.java
    │  │              │
    │  │              └─jwt
    │  │                      JwtModel.java
    │  │                      JwtUtil.java
    │  │
    │  └─resources
    │          application.yml
    │          bootstrap.yml
    │
    └─test
        └─java
            └─com
                └─lzx
                    └─gateway
                        └─demo
                                MyGatewayApplicationTests.java


一、项目添加 Spring Cloud Gateway 依赖
参考:创建spring Cloud Gateway

二、加入jjwt依赖
jjwt是一个Java对jwt的支持库,我们使用这个库来创建、解码token

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
1
2
3
4
5
核心方法
创建jwt token的方法

/**
 * 创建jwt
 * @param id
 * @param issuer
 * @param subject
 * @param ttlMillis
 * @return
 * @throws Exception
 */
public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

    // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成JWT的时间
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);

    // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
    // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
    Map<String, Object> claims = new HashMap<>();
    claims.put("uid", "123456");
    claims.put("user_name", "admin");
    claims.put("nick_name", "X-rapido");

    // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
    // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
    SecretKey key = generalKey();

    // 下面就是在为payload添加各种标准声明和私有声明了
    JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
            .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
            .setId(id)                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
            .setIssuedAt(now)           // iat: jwt的签发时间
            .setIssuer(issuer)          // issuer:jwt签发人
            .setSubject(subject)        // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
            .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥

    // 设置过期时间
    if (ttlMillis >= 0) {
        long expMillis = nowMillis + ttlMillis;
        Date exp = new Date(expMillis);
        builder.setExpiration(exp);
    }
    return builder.compact();
}

解码jwt token的方法

/**
 * 解密jwt
 *
 * @param jwt
 * @return
 * @throws Exception
 */
public static Claims parseJWT(String jwt) throws Exception {
    SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
    Claims claims = Jwts.parser()  //得到DefaultJwtParser
            .setSigningKey(key)                 //设置签名的秘钥
            .parseClaimsJws(jwt).getBody();     //设置需要解析的jwt
    return claims;
}
1

14
最后贴出来一个JWT 的工具类:包含了创建和解码的工具

package com.lzx.gateway.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 描述: jwt 工具类
 *
 * @Auther: lzx
 * @Date: 2019/7/9 17:50
 */
public class JwtUtil {

//密钥 -- 根据实际项目,这里可以做成配置
    public static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c42c231cf43bb8f86998202bd301ec52239a74fc0c9a9aeccce604743367c9646b";

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public static SecretKey generalKey(){
        byte[] encodedKey = Base64.decodeBase64(KEY);
        SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    /**
     * 创建jwt
     * @param id
     * @param issuer
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

        // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("uid", "123456");
        claims.put("user_name", "admin");
        claims.put("nick_name", "X-rapido");

        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
        // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        SecretKey key = generalKey();

        // 下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
                .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(id)                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           // iat: jwt的签发时间
                .setIssuer(issuer)          // issuer:jwt签发人
                .setSubject(subject)        // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥

        // 设置过期时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //设置签名的秘钥
                .parseClaimsJws(jwt).getBody();     //设置需要解析的jwt
        return claims;
    }

}


三、添加token检查的过滤器
getOrder方法中的返回值的数据越小,过滤器的级别越高

package com.lzx.gateway.auth;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lzx.gateway.dto.ReturnData;
import com.lzx.gateway.jwt.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 描述: JwtToken 过滤器
 *
 * @Auther: lzx
 * @Date: 2019/7/9 15:49
 */
@Component
//读取 yml 文件下的 org.my.jwt
@ConfigurationProperties("org.my.jwt")
@Setter
@Getter
@Slf4j
public class JwtTokenFilter implements GlobalFilter,Ordered {

    private String[] skipAuthUrls;

    private ObjectMapper objectMapper;

    public JwtTokenFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳过不需要验证的路径
        if(null != skipAuthUrls&&Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }

        //获取token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        ServerHttpResponse resp = exchange.getResponse();
        if(StringUtils.isBlank(token)){
            //没有token
            return authErro(resp,"请登陆");
        }else{
            //有token
            try {
                JwtUtil.checkToken(token,objectMapper);
                return chain.filter(exchange);
            }catch (ExpiredJwtException e){
                log.error(e.getMessage(),e);
                if(e.getMessage().contains("Allowed clock skew")){
                    return authErro(resp,"认证过期");
                }else{
                    return authErro(resp,"认证失败");
                }
            }catch (Exception e) {
                log.error(e.getMessage(),e);
                return authErro(resp,"认证失败");
            }
        }
    }

    /**
     * 认证错误输出
     * @param resp 响应对象
     * @param mess 错误信息
     * @return
     */
    private Mono<Void> authErro(ServerHttpResponse resp,String mess) {
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
        String returnStr = "";
        try {
            returnStr = objectMapper.writeValueAsString(returnData);
        } catch (JsonProcessingException e) {
            log.error(e.getMessage(),e);
        }
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}



四、添加认证的api接口
这里为了方便测试,认证的接口写在了网关的项目中,实际生产可以把接口设计在专门的认证服务中

 /**
 * 登陆认证接口
 * @param userDTO
 * @return
 */
@PostMapping("/login")
public ReturnData<String> login(@RequestBody UserDTO userDTO) throws Exception {
    ArrayList<String> roleIdList = new ArrayList<>(1);
    roleIdList.add("role_test_1");
    JwtModel jwtModel = new JwtModel("test", roleIdList);
    int effectivTimeInt = Integer.valueOf(effectiveTime.substring(0,effectiveTime.length()-1));
    String effectivTimeUnit = effectiveTime.substring(effectiveTime.length()-1,effectiveTime.length());
    String jwt = null;
    switch (effectivTimeUnit){
        case "s" :{
            //秒
            jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 1000L);
            break;
        }
        case "m" :{
            //分钟
            jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 1000L);
            break;
        }
        case "h" :{
            //小时
            jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 60L * 1000L);
            break;
        }
        case "d" :{
            //小时
            jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 24L * 60L * 60L * 1000L);
            break;
        }
    }
    return new ReturnData<String>(HttpStatus.SC_OK,"认证成功",jwt);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
五、yml配置文件
这里读取了配置中心的文件,大家可以根据自己的需求更改

###################################
#服务启动端口的配置
###################################
server:
  port: ${server-port}

###############################################################
# eureka 的相关配置
# 如果不需要 结合eureka 使用,可以不要这一段配置
###############################################################
eureka:
  client:
    fetch-registry: true
    register-with-eureka: ${register-with-eureka}     # 是否注册到eureka
    service-url:
      defaultZone: ${service-url-defaultZone}
  instance:
    prefer-ip-address: false
    hostname: ${instance-hostname}


spring:
  cloud:
#################################
#   gateway相关配置
#################################
    gateway:
#    路由定义
      routes:

      - id: baidu
        uri: https://www.baidu.com
        predicates:
        - Path=/baidu/**
        filters:
        - StripPrefix=1

      - id: eureka-manage
        uri: lb://eureka-manage
        predicates:
        - Path=/eureka-manage/**
        filters:
        - StripPrefix=1

      - id: sina
        uri: https://www.sina.com.cn/
        predicates:
        - Path=/sina/**
        filters:
        - StripPrefix=1

org:
  my:
    jwt:
      #跳过认证的路由
      skip-auth-urls:
      - /baidu
      ############################################
      #   有效时长
      #     单位:d:天、h:小时、m:分钟、s:秒
      ###########################################
      effective-time: 1m

测试
直接不带认证信息访问一个需要认证的路由:访问一个新浪得路由,提示需要认证
http://localhost:30006/sina

调用认证api获取token

把token加入请求头,再次访问新浪得路由,可以通过认证

尝试token过期后访问,在application.yml中我配置了token一分钟后过期,一分钟后我再次携带token访问新浪得路由,提示认证过期


进阶:制作JwtToken校验注解
定义注解
package com.lzx.gateway.annotation;

import java.lang.annotation.*;

/**
 * 描述: jwt检查注解
 *
 * @Auther: lzx
 * @Date: 2019/6/17 16:24
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JwtCheck {

    String value() default "";
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
定义注解得AOP
package com.lzx.gateway.aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lzx.gateway.dto.ReturnData;
import com.lzx.gateway.jwt.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestHeader;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

/**
 * 描述:添加了 JwtCheck 注解 的Aop
 *
 * @Auther: lzx
 * @Date: 2019/6/18 10:56
 */
@Component
@Aspect
@Slf4j
public class JwtCheckAop {

    @Autowired
    private ObjectMapper objectMapper;

    @Pointcut("@annotation(com.lzx.gateway.annotation.JwtCheck)")
    private void apiAop(){

    }

    /**
     * 方法执行前的aop
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("apiAop()")
    public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //获取参数上得所有注解
        Annotation[][] parameterAnnotationArray = method.getParameterAnnotations();
        Object[] args = point.getArgs();

        String token = null;

        /*
            a -> start
            这个代码片得逻辑:找出有 @RequestHeader("Authorization") 的参数,赋值给 token变量
         */
        for(Annotation[]  annotations : parameterAnnotationArray){
            for(Annotation a:annotations){
                if(a instanceof RequestHeader){
                    RequestHeader requestHeader =  (RequestHeader)a;
                    if("Authorization".equals(requestHeader.value())){
                        token = (String) args[ArrayUtils.indexOf(parameterAnnotationArray,annotations)];
                    }
                }
            }
        }
        /*
            a -> end
         */

        if(StringUtils.isBlank(token)){
            //没有token
            return authErro("请登陆");
        }else{
            //有token
            try {
                JwtUtil.checkToken(token,objectMapper);
                Object proceed = point.proceed();
                return proceed;
            }catch (ExpiredJwtException e){
                log.error(e.getMessage(),e);
                if(e.getMessage().contains("Allowed clock skew")){
                    return authErro("认证过期");
                }else{
                    return authErro("认证失败");
                }
            }catch (Exception e) {
                log.error(e.getMessage(),e);
                return authErro("认证失败");
            }
        }
    }

    /**
     * 认证错误输出
     * @param mess 错误信息
     * @return
     */
    private Object authErro(String mess) {
        ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
        return returnData;
    }

}


注解的使用方法
直接在方法上使用@JwtCheck

/**
     * jwt 检查注解测试 测试
     * @return
     */
    @GetMapping("/testJwtCheck")
    @JwtCheck
    public ReturnData<String> testJwtCheck(@RequestHeader("Authorization")String token,@RequestParam("name")@Valid String name){

        return new ReturnData<String>(HttpStatus.SC_OK,"请求成功咯","请求成功咯"+name);

    }
 ———————————————— 
版权声明:本文为CSDN博主「1024程序猿~嗷~」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/github_35976996/article/details/95170542
回帖
  • 消灭零回复