Spring Cloud Gateway(四、JWT认证和鉴权)

JWT(JSON WEB TOKEN)是目前最流行的认证解决方案。

一、创建用于登录认证的工程auth-service

1.1 pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.erbadagang.springcloud.gateway</groupId>
    <artifactId>auth-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>auth-service</name>
    <description>sc-auth-service project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.2 JWT工具类

package com.erbadagang.springcloud.gateway.authservice.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;

public class JWTUtil {
    public static final String SECRET_KEY = "erbadagang-123456"; //秘钥
    public static final long TOKEN_EXPIRE_TIME = 5 * 60 * 1000; //token过期时间
    public static final long REFRESH_TOKEN_EXPIRE_TIME = 10 * 60 * 1000; //refreshToken过期时间
    private static final String ISSUER = "issuer"; //签发人

    /**
     * 生成签名
     */
    public static String generateToken(String username, String roles) {
        Date now = new Date();
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法

        String token = JWT.create()
                .withIssuer(ISSUER) //签发人
                .withIssuedAt(now) //签发时间
//                .withSubject()
                .withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRE_TIME)) //过期时间
                .withClaim("username", username) //保存身份标识
                .withClaim("roles", roles) //保存权限标识
                .sign(algorithm);
        return token;
    }

    /**
     * 验证token
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY); //算法
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .build();
            verifier.verify(token);//如果校验有问题会抛出异常。
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 从token获取username
     */
    public static String getUsername(String token) {
        try {
            return JWT.decode(token).getClaim("username").asString();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return "";
    }

    /**
     * 从token获取roles
     */
    public static String getRoles(String token) {
        try {
            return JWT.decode(token).getClaim("roles").asString();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return "";
    }
}

1.3 LoginController类

package com.erbadagang.springcloud.gateway.authservice.controller;

import com.erbadagang.springcloud.gateway.authservice.util.AuthResult;
import com.erbadagang.springcloud.gateway.authservice.util.JWTUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @description 登录后生成token,及刷新token。
 * @ClassName: LoginController
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/31 21:39
 * @Copyright:
 */
@RestController
public class LoginController {
    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 登录认证
     *
     * @param username 用户名
     * @param password 密码
     */
    @GetMapping("/login")
    public AuthResult login(@RequestParam String username, @RequestParam String password) {
        if ("admin".equals(username) && "admin".equals(password)) {
            //查询出用户权限
            String roles = "admin,query";
            //生成token
            String token = JWTUtil.generateToken(username, roles);

            //生成refreshToken
            String refreshToken = UUID.randomUUID().toString().replace("-", "");

            //数据放入redis
            redisTemplate.opsForHash().put(refreshToken, "token", token);
            redisTemplate.opsForHash().put(refreshToken, "username", username);
            redisTemplate.opsForHash().put(refreshToken, "roles", roles);

            //设置token的过期时间
            redisTemplate.expire(refreshToken, JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);

            return new AuthResult(0, "success", token, refreshToken);
        } else {
            return new AuthResult(1001, "username or password error");
        }
    }

    /**
     * 刷新token
     */
    @GetMapping("/refreshToken")
    public AuthResult refreshToken(@RequestParam String refreshToken) {
        String username = (String) redisTemplate.opsForHash().get(refreshToken, "username");
        String roles = (String) redisTemplate.opsForHash().get(refreshToken, "roles");
        if (StringUtils.isEmpty(username)) {
            return new AuthResult(1003, "refreshToken error");
        }

        //生成新的token
        String newToken = JWTUtil.generateToken(username, roles);
        redisTemplate.opsForHash().put(refreshToken, "token", newToken);
        return new AuthResult(0, "success", newToken, refreshToken);
    }

    @GetMapping("/")
    public String index() {
        return "auth-service: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

返回信息包装类AuthResult

package com.erbadagang.springcloud.gateway.authservice.util;

import lombok.Data;

/**
 * AuthResult作用是:
 *
 * @ClassName: AuthResult
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/31 21:04
 * @Copyright:
 */
@Data
public class AuthResult {
    private int code;
    private String msg;
    private String token;
    private String refreshToken;


    public AuthResult(int i, String s) {
        this.code = i;
        this.msg = s;
    }

    public AuthResult(int i, String success, String token, String refreshToken) {
        this.code = i;
        this.msg = success;
        this.token = token;
        this.refreshToken = refreshToken;
    }
}

1.4 application配置信息

spring.application.name=auth-service
# 应用服务 WEB 访问端口
server.port=8080
#redis
spring.redis.database=0
spring.redis.timeout=3000
spring.redis.lettuce.pool.max-active=100
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.pool.max-idle=8
#standalone
spring.redis.host=localhost
spring.redis.port=6379

1.5 启动类

package com.erbadagang.springcloud.gateway.authservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @description 启动类
 * @ClassName: AuthServiceApplication
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/31 21:43
 * @Copyright:
 */
@SpringBootApplication
public class AuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServiceApplication.class, args);
    }

}

1.6 测试

启动redis和Springboot项目。

访问:http://localhost:8080/,返回:auth-service: 2020-07-31 21:45:32.
访问:http://localhost:8080/login?username=admin&password=admin,返回:{"code":0,"msg":"success","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE1OTYyMDM0OTMsImlhdCI6MTU5NjIwMzE5MywidXNlcm5hbWUiOiJhZG1pbiJ9.UR69NUB2jZ9Hco-TYPLwjz0iig-GKqmvl5lWA0lOy6M","refreshToken":"64cfde875ccf457788ee707c1e3d8576"}.
访问:http://localhost:8080/refreshToken?refreshToken=64cfde875ccf457788ee707c1e3d8576,返回:{"code":0,"msg":"success","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE1OTYyMDM2NjIsImlhdCI6MTU5NjIwMzM2MiwidXNlcm5hbWUiOiJhZG1pbiJ9.j2ILPWjBAP72Z-oqMRV_3MhIsXW4cJchpH831sw35TE","refreshToken":"64cfde875ccf457788ee707c1e3d8576"}.

二、改造SpringCloud Gateway工程

2.1 pom.xml文件添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.erbadagang.springcloud.gateway</groupId>
    <artifactId>Gateway-JWT</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Gateway-JWT</name>
    <description>gateway and shiro</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier><!-- jdk版本 -->
            <exclusions>
                <exclusion>
                    <artifactId>commons-lang</artifactId>
                    <groupId>commons-lang</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.2 创建全局过滤器JWTAuthFilter

package com.erbadagang.gateway.auth;

import com.erbadagang.gateway.dto.Response;
import lombok.Data;
import net.sf.json.JSONObject;
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.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @description 过滤器,判断token是否合法,以及取出roles进行逻辑处理。
 * @ClassName: JWTAuthFilter
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/31 23:27
 * @Copyright:
 */
@Component
//读取 yml 文件下的 org.my.jwt
@ConfigurationProperties("org.my.jwt")
@Data
public class JWTAuthFilter implements GlobalFilter, Ordered {

    private String[] skipAuthUrls;

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

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

        //从请求头中取得token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(token)) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.OK);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

            Response res = new Response(401, "401 unauthorized");
            byte[] responseByte = JSONObject.fromObject(res).toString().getBytes(StandardCharsets.UTF_8);

            DataBuffer buffer = response.bufferFactory().wrap(responseByte);
            return response.writeWith(Flux.just(buffer));
        }

        //请求中的token是否有效
        boolean verifyResult = JWTUtil.verify(token);
        if (!verifyResult) {
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.OK);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

            Response res = new Response(1004, "invalid token");
            byte[] responseByte = JSONObject.fromObject(res).toString().getBytes(StandardCharsets.UTF_8);

            DataBuffer buffer = response.bufferFactory().wrap(responseByte);
            return response.writeWith(Flux.just(buffer));
        } else {
            String roles = JWTUtil.getRoles(token);
            if (roles.indexOf("admin") >= 0) {//TODO 根据权限进行判断
                System.out.println("roles = " + roles);
            }
        }

        //如果各种判断都通过,执行chain上的其他业务逻辑
        return chain.filter(exchange);
    }

    /**
     * 判断当前访问的url是否开头URI是在配置的忽略url列表中
     *
     * @param url
     * @return
     */
    public boolean isSkipUrl(String url) {
        for (String skipAuthUrl : skipAuthUrls) {
            if (url.startsWith(skipAuthUrl)) {
                return true;
            }
        }
        return false;
    }
}

Response

package com.erbadagang.gateway.dto;

import lombok.Data;

/**
 * @description 返回数据包装类。
 * @ClassName: Response
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/31 22:03
 * @Copyright:
 */
@Data
public class Response {
    private int code;
    private String message;
    private String data;

    public Response() {

    }

    public Response(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public Response(int code, String message, String data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    //省略getter、setter方法
    //......
}

2.3 application配置信息

###################################
#服务启动端口的配置
###################################
server:
  port: 8888

spring:
  application:
    name: my-gateway

  cloud:
    #################################
    #   gateway相关配置
    #################################
    gateway:
      ################################
      # 配置允许跨域请求
      ################################
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
      #    路由定义
      routes:

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

        # JWT的token生成及验证服务,可以对应到用户子服务(系统)的概念。
        - id: auth-service
          uri: http://localhost:8080/
          predicates:
            - Path=/auth-service/**
          filters:
            - StripPrefix=1

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

org:
  my:
    jwt:
      #跳过认证的路由
      skip-auth-urls:
        - /baidu
        - /auth-service

2.4 测试

  1. 访问不需要认证的baiduhttp://localhost:8888/baidu,正常调整到百度页面。

    image.png

  2. 访问需要认证的sinahttp://localhost:8888/sina,返回{"code":401,"data":"","message":"401 unauthorized"}

  3. 生成token, 访问:http://localhost:8888/auth-service/login?username=admin&password=admin,返回

{
    "code": 0,
    "msg": "success",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6ImFkbWluLHF1ZXJ5IiwiaXNzIjoiaXNzdWVyIiwiZXhwIjoxNTk2MjEwMTE4LCJpYXQiOjE1OTYyMDk4MTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.yiAVGMJE8ILiA3h0YiS0ycxh4T6HMYsY6NwW2QmDrlc",
    "refreshToken": "18335c70036a4f7795c170ab4252f1b6"
}
  1. 再访问需要认证的sinahttp://localhost:8888/sina,本次通过postman,添加header信息Authorization,值为刚才获取的token,可以正常获取到sina首页html。
    带token访问

底线


本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。