目录

1.JWT简介

2.登录鉴权流程

3.springcloud gateway简单使用

4.创建Token

5.实现登录鉴权

6.刷新令牌


1.JWT简介

JWT:全称是JSON Web Token,是token的一种实现方法。通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。

JWT结构:JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_后端

header中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。

payload部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_分布式_02

当然,用户也可以根据自己业务使用需要自行更改添加字段。

Signature部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,使用指定秘钥,通过指定的签名算法生成哈希,以确保数据不会被篡改。该密码仅仅为保存在服务器中,并且不能向用户公开。

JWT优势:

  • 跨语言:因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
  • 适合单点登录:不需要在服务端保存会话信息,也就是说不依赖于cookie和session,适用于分布式微服务
  • 适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要 Cookie 保存 SessionId),所以不适合移动端
     

2.登录鉴权流程

项目以springcloud框架实现,其总体框架如下:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring cloud_03

 cloud-auth-server 为登录认证微服务,实现用户登录登出功能(以shiro实现,此处不进行展开,具体可参考:springboot集成shiro实现用户登录认证)

cloud-gateway为统一网关,实现路由管理和token鉴权

cloud-jwt-manage为JWT生成存放微服务,存放生成jwt token的相关工具类。

其他微服务实现用户登录鉴权未使用到,不进行展开。

springcloud gateway实现登录鉴权流程如下;

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring cloud_04

简单来说就是用户需要先进行登录操作,验证用户名密码,获得jwt令牌。再携带jwt令牌发送请求,通过网关转发,访问具体的业务接口;若没有携带jwt令牌,或者令牌已过期,则无法访问对应的业务接口,需要重新登录。

3.springcloud gateway简单使用

首选,需要导入springcloud gateway相关依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

需要注意的是,gateway相关依赖和 springboot web相关依赖会冲突,如果在其他包中使用了,需要将其排除:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-webflux</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 编写配置文件:

application.yml:

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  # redis配置
  redis:
    database: 0
    host: 127.0.0.1
    #redis默认端口
    port: 6379
    password:
    timeout: 5000ms
  profiles:
    include: routers    #使得 application-routers.yml配置文件生效

auth:
  jwt:
    enabled: true   # 是否开启JWT登录认证功能
    secret: passjava  # JWT 私钥,用于校验JWT令牌的合法性
    expiration: 3600000 # JWT 令牌的有效期,用于校验JWT令牌的合法性,一个小时
    header: Authorization # HTTP 请求的 Header 名称,该 Header作为参数传递 JWT 令牌
    userParamName: userId  # 用户登录认证用户名参数名称
    pwdParamName: password  # 用户登录认证密码参数名称
    useDefaultController: true # 是否使用默认的JwtAuthController
    skipValidUrl: /auth/login

需注意,auth.jwt中相关配置信息cloud-auth-server配置文件中也要有一样的配置,避免cloud-auth-server微服务生成token时属性空值。

application-routers.yml:

spring:
  cloud:
    gateway:
      routes:
        - id: route_auth # 认证微服务路由规则
          uri: lb://cloud-auth-server # 负载均衡,将请求转发到注册中心注册的 auth 服务进行认证
          predicates: # 断言
            - Path=/api/auth/** # 如果前端请求路径包含 api/auth,则应用这条路由规则
          filters: #过滤器
            - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空

然后,我们就可以通过http://localhost:9527/api/auth/login 地址进行登录

4.创建Token

编写cloud-jwt-manage相关代码:

导入依赖:

<dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>compile</scope>
        </dependency>
        <!-- redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- jwt 工具类 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
            <scope>compile</scope>
        </dependency>
        <!-- 自定义配置项 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

 jwt相关属性类:

@Data
@ConfigurationProperties(prefix = "auth.jwt")
@Component
public class AuthJwtProperties {


    //是否开启JWT,即注入相关的类对象
    private Boolean enabled = true;

    //JWT 密钥
    private String secret;

    //accessToken 有效时间
    private Long expiration;

    //header名称
    private String header;

    /**
     * 用户登录-用户名参数名称
     */
    private String userParamName = "userId";
    /**
     * 用户登录-密码参数名称
     */
    private String pwdParamName = "password";

    //是否使用默认的JWTAuthController
    private Boolean useDefaultController = false;
    //跳过认证的路由
    private String skipValidUrl;

}
public class TokenConstants
{
    /**
     * 令牌自定义标识
     */
    public static final String AUTHENTICATION = "Authorization";

    /**
     * 令牌前缀
     */
    public static final String PREFIX = "Bearer ";

}

 jwt 令牌生成、验证工具类:

import com.seven.springcloud.config.AuthJwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;


@Component
public class JwtTokenUtil {

    private static final String JWT_CACHE_KEY = "jwt:userId:";
    private static final String USER_ID = "userId";
    private static final String USER_NAME = "username";
    private static final String ACCESS_TOKEN = "access_token";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String EXPIRE_IN = "expire_in";

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private AuthJwtProperties jwtProperties;

    /**
     * 生成 token 令牌主方法
     * @param userId 用户Id或用户名
     * @return 令token牌
     */

    public Map<String, Object> generateTokenAndRefreshToken(String userId, String username) {
        //生成令牌及刷新令牌
        Map<String, Object> tokenMap = buildToken(userId, username);
        //redis缓存结果
        cacheToken(userId, tokenMap);
        return tokenMap;
    }

    //将token缓存进redis
    private void cacheToken(String userId, Map<String, Object> tokenMap) {
        stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, ACCESS_TOKEN, tokenMap.get(ACCESS_TOKEN));
        stringRedisTemplate.opsForHash().put(JWT_CACHE_KEY + userId, REFRESH_TOKEN, tokenMap.get(REFRESH_TOKEN));
        stringRedisTemplate.expire(userId, jwtProperties.getExpiration() * 2, TimeUnit.MILLISECONDS);
    }
    //生成令牌
    private Map<String, Object> buildToken(String userId, String username) {
        //生成token令牌
        String accessToken = generateToken(userId, username, null);
        //生成刷新令牌
        String refreshToken = generateRefreshToken(userId, username, null);
        //存储两个令牌及过期时间,返回结果
        HashMap<String, Object> tokenMap = new HashMap<>(2);
        tokenMap.put(ACCESS_TOKEN, accessToken);
        tokenMap.put(REFRESH_TOKEN, refreshToken);
        tokenMap.put(EXPIRE_IN, jwtProperties.getExpiration());
        return tokenMap;
    }
    /**
     * 生成 token 令牌 及 refresh token 令牌
     * @param payloads 令牌中携带的附加信息
     * @return 令牌
     */
    public String generateToken(String userId, String username,
                                Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);;

        return generateToken(claims);
    }
    public String generateRefreshToken(String userId, String username, Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);

        return generateRefreshToken(claims);
    }
    //构建map存储令牌需携带的信息
    private Map<String, Object> buildClaims(String userId, String username, Map<String, String> payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();

        Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
        claims.put("sub", userId);
        claims.put("username", username);
        claims.put("created", new Date());
        //claims.put("roles", "admin");

        if(payloadSizes > 0){
            claims.putAll(payloads);
        }

        return claims;
    }


    /**
     * 刷新令牌并生成新令牌
     * 并将新结果缓存进redis
     */
    public Map<String, Object> refreshTokenAndGenerateToken(String userId, String username) {
        Map<String, Object> tokenMap = buildToken(userId, username);
        stringRedisTemplate.delete(JWT_CACHE_KEY + userId);
        cacheToken(userId, tokenMap);
        return tokenMap;
    }

    /**
     * 从request获取userid
     * @param request http请求
     * @return request.getHeader
     */
    public String getUserIdFromRequest(HttpServletRequest request) {
        return request.getHeader(USER_ID);
    }

    //缓存中删除token
    public boolean removeToken(String userId) {
        return Boolean.TRUE.equals(stringRedisTemplate.delete(JWT_CACHE_KEY + userId));
    }


    /**
     * 从令牌中获取用户id
     *
     * @param token 令牌
     * @return 用户id
     */
    public String getUserIdFromToken(String token) {
        String userId;
        try {
            Claims claims = getClaimsFromToken(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            userId = null;
        }
        return userId;
    }
    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = (String) claims.get(USER_NAME);
        } catch (Exception e) {
            username = null;
        }
        return username;
    }


    /**
     * 判断令牌是否不存在 redis 中
     *
     * @param token 刷新令牌
     * @return true=不存在,false=存在
     */
    public Boolean isRefreshTokenNotExistCache(String token) {
        String userId = getUserIdFromToken(token);
        String refreshToken = (String)stringRedisTemplate.opsForHash().get(JWT_CACHE_KEY + userId, REFRESH_TOKEN);
        return refreshToken == null || !refreshToken.equals(token);
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return true=已过期,false=未过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            //验证 JWT 签名失败等同于令牌过期
            return true;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userId  用户Id用户名
     * @return 是否有效
     */
    public Boolean validateToken(String token, String userId) {

        String username = getUserIdFromToken(token);
        return (username.equals(userId) && !isTokenExpired(token));
    }


    /**
     * 生成令牌
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis()
                + jwtProperties.getExpiration());
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512,
                        jwtProperties.getSecret())
                .compact();
    }
    /**
     * 生成刷新令牌 refreshToken,有效期是令牌的 2 倍
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateRefreshToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 2);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }

    /**
     * 从令牌中获取数据声明,验证 JWT 签名
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
}

上述代码中:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring_05

此方法中存放业务所需存放的信息,比如用户名,id等,生成的jwt解码后,payload中可查得以上信息。

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_分布式_06

方法将生产的token缓存进redis中,进行保存。

 然后,我们需要将cloud-jwt-manage进行clean、install,以便其他微服务进行使用,其导入方式如下:

<!--自定义包-->
        <dependency>
            <groupId>springcloud2022</groupId>
            <artifactId>cloud-jwt-manage</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.alibaba</groupId>
                    <artifactId>druid-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

因网关中web依赖和gateway依赖会冲突,以及网关中未导入数据库连接相关配置,所以需要排除以上依赖再讲自定义jwt包导入。cloud-auth-server微服务中直接导入即可。

5.实现登录鉴权

下面进行登录授权,首先在cloud-auth-manage登录控制类方法中,进行登录验证,通过后,调用以下代码生成token返回结果:

// 通过 jwtTokenUtil 生成 JWT 令牌和刷新令牌
        Map<String, Object> tokenMap = jwtTokenUtil
                .generateTokenAndRefreshToken(String.valueOf(account.getId()), username);

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_gateway_07

 然后在cloud-gateway微服务中,编写过滤器:

//过滤器,使得JWTToken过滤器生效
@Component
public class GlobalLoginFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
import com.alibaba.fastjson.JSON;
import com.seven.springcloud.config.AuthJwtProperties;
import com.seven.springcloud.constants.TokenConstants;
import com.seven.springcloud.entities.CommonResult;
import com.seven.springcloud.entities.enums.ResponseCodeEnum;
import com.seven.springcloud.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Slf4j
@Configuration
public class JwtAuthCheckFilter {
    private static final String AUTH_TOKEN_URL = "/auth/login";
    private static final String REFRESH_TOKEN_URL = "/auth/token/refresh";
    public static final String USER_ID = "userId";
    public static final String USER_NAME = "username";
    public static final String FROM_SOURCE = "from-source";

    @Resource
    private AuthJwtProperties authJwtProperties;
    @Resource
    private JwtTokenUtil jwtTokenUtil;


    @Bean
    @Order(-101)
    public GlobalFilter jwtAuthGlobalFilter() {

        return (exchange, chain) -> {

            ServerHttpRequest serverHttpRequest = exchange.getRequest();
            ServerHttpResponse serverHttpResponse = exchange.getResponse();
            ServerHttpRequest.Builder mutate = serverHttpRequest.mutate();
            String requestUrl = serverHttpRequest.getURI().getPath();

            // 跳过对登录请求的 token 检查。因为登录请求是没有 token 的,是来申请 token 的。
            if(AUTH_TOKEN_URL.equals(requestUrl)) {
                log.info("登录url,放行");
                return chain.filter(exchange);
            }

            // 从 HTTP 请求头中获取 JWT 令牌
            String token = getToken(serverHttpRequest);
            if (StringUtils.isEmpty(token)) {
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_MISSION);
            }

            // 对Token解签名,并验证Token是否过期
            boolean isJwtNotValid = jwtTokenUtil.isTokenExpired(token);
            if(isJwtNotValid){
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
            }
            // 验证 token 里面的 userId 是否为空
            String userId = jwtTokenUtil.getUserIdFromToken(token);
            String username = jwtTokenUtil.getUserNameFromToken(token);
            if (StringUtils.isEmpty(userId)) {
                return unauthorizedResponse(exchange, serverHttpResponse, ResponseCodeEnum.TOKEN_INVALID);
            }

            // 设置用户信息到请求
            addHeader(mutate, USER_ID, userId);
            addHeader(mutate, USER_NAME, username);
            // 内部请求来源参数清除
            removeHeader(mutate, FROM_SOURCE);
            return chain.filter(exchange.mutate().request(mutate.build()).build());
        };
    }

    //添加头部信息
    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }
    //移除头部信息
    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

     //内容编码,配置为UTF-8
     static String urlEncode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return StringUtils.EMPTY;
        }
    }

    //请求token
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(authJwtProperties.getHeader());
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }

    //jwt鉴权失败处理类
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, ServerHttpResponse serverHttpResponse, ResponseCodeEnum responseCodeEnum) {
        log.warn("token异常处理,请求路径:{}", exchange.getRequest().getPath());
        serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        CommonResult<Object> responseResult = new CommonResult<>(responseCodeEnum.getCode(),responseCodeEnum.getMessage());
        DataBuffer dataBuffer = serverHttpResponse.bufferFactory()
                .wrap(JSON.toJSONStringWithDateFormat(responseResult, JSON.DEFFAULT_DATE_FORMAT)
                        .getBytes(StandardCharsets.UTF_8));
        return serverHttpResponse.writeWith(Flux.just(dataBuffer));
    }

}

 关于统一返回类 CommonResult 和枚举类 ResponseCodeEnum,下面放出相关代码,可进行参考:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
    private Integer code;
    private String message;
    private T      data;
    private int dataSize;

    public CommonResult(Integer code,String message){
        this(code,message,null,0);
    }
    public CommonResult(Integer code,String message,T data){
        this(code,message,data,0);
    }

}
public enum ResponseCodeEnum {

    SUCCESS(200, "成功"),
    FAIL(412, "失败"),
    LOGIN_ERROR(202, "用户名或密码错误"),

    UNKNOWN_ERROR(500, "未知错误"),
    PARAMETER_ILLEGAL(400, "参数不合法"),

    TOKEN_INVALID(412, "token 已过期或验证不正确!"),
    TOKEN_SIGNATURE_INVALID(403, "无效的签名"),
    TOKEN_MISSION(403, "token 缺失"),
    REFRESH_TOKEN_INVALID(412, "refreshToken 无效"),
    LOGOUT_ERROR(444, "用户登出失败");
    private final int code;
    private final String message;
    ResponseCodeEnum(int code, String message) {
    this.code = code;
    this.message = message;
    }
    public int getCode() {
    return code;
    }
    public String getMessage() {
    return message;
    }
}

然后,启动上述两个微服务,即可进行登录认证:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring cloud_08

redis中的token如下:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring cloud_09

 

解析获得的token:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_后端_10

payload为我们添加的信息。

添加新的路由,启动一个其他业务微服务(比如cloud-address-manage),验证携带token访问的效果:

spring:
  cloud:
    gateway:
      routes:
        - id: route_auth # 认证微服务路由规则
          uri: lb://cloud-auth-server # 负载均衡,将请求转发到注册中心注册的 auth 服务进行认证
          predicates: # 断言
            - Path=/api/auth/** # 如果前端请求路径包含 api/auth,则应用这条路由规则
          filters: #过滤器
            - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空

        - id: route_address # 认证微服务路由规则
          uri: lb://cloud-address-manage # 负载均衡,将请求转发到注册中心注册的 address 服务进行认证
          predicates: # 断言
            - Path=/api/address/** # 如果前端请求路径包含 api/address,则应用这条路由规则
          filters: #过滤器
            - RewritePath=/api/(?<segment>.*),/$\{segment} # 将跳转路径中包含的api替换成空

直接访问,显示未携带token(即未登录):

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_spring_11

 添加token后,访问成功:

spring could nacos关掉鉴权配置 spring cloud gateway 鉴权_后端_12

微服务鉴权访问成功。

6.刷新令牌

当认证服务返回给客户端的 JWT 也就是 access_token 过期后,客户端如果需要再次通过发送登录请求重新拿到 access_token会使得用户体验很不友好。而JWT 生成后是不能篡改里面的内容,即使是 JWT 的有效期也不行。所以延长 access_token 有效期的做法并不适合,而且如果长期保持一个 access_token 有效,也是不安全的。所以我们时常使用refresh token来进行token的刷新。

我们一般会把 refresh_token 设置的过期时间稍微长一点,比如两倍于 access_token,当 access_token 过期后,refresh_token 如果还没有过期,就可以利用两者的过期时间差进行重新生成令牌的操作,也就是刷新令牌,同时删除掉redis中缓存的旧令牌。

JWTTokenUtil中已有刷新令牌相关方法,下面进行cloud-auth-server中控制类刷新令牌代码的编写:

/**
     * 刷新JWT令牌,用旧的令牌换新的令牌
     * 参数为需要刷新的令牌
     * header中携带刷新令牌
     */
    @GetMapping("/token/refresh")
    public CommonResult<Object> refreshToken(@RequestHeader(value = "${auth.jwt.header}") String token){
        token = com.seven.springcloud.util.SecurityUtils.replaceTokenPrefix(token);

        if (StringUtils.isEmpty(token)) {
            return new CommonResult<>(ResponseCodeEnum.TOKEN_MISSION.getCode(),
                    ResponseCodeEnum.TOKEN_MISSION.getMessage());
        }

        // 对Token解签名,并验证Token是否过期
        boolean isJwtNotValid = jwtTokenUtil.isTokenExpired(token);
        if(isJwtNotValid){
            return new CommonResult<>(ResponseCodeEnum.TOKEN_INVALID.getCode(),
                    ResponseCodeEnum.TOKEN_INVALID.getMessage());
        }

        // 验证 token 里面的 userId 是否为空
        String userId = jwtTokenUtil.getUserIdFromToken(token);
        String username = jwtTokenUtil.getUserNameFromToken(token);
        if (StringUtils.isEmpty(userId)) {
            return new CommonResult<>(ResponseCodeEnum.TOKEN_INVALID.getCode(),
                    ResponseCodeEnum.TOKEN_INVALID.getMessage());
        }

        // 这里为了保证 refreshToken 只能用一次,刷新后,会从 redis 中删除。
        // 如果用的不是 redis 中的 refreshToken 进行刷新令牌,则不能刷新。
        // 如果使用 redis 中已过期的 refreshToken 也不能刷新令牌。
        boolean isRefreshTokenNotExisted = jwtTokenUtil.isRefreshTokenNotExistCache(token);
        if(isRefreshTokenNotExisted){
            return new CommonResult<>(ResponseCodeEnum.REFRESH_TOKEN_INVALID.getCode(),
                    ResponseCodeEnum.REFRESH_TOKEN_INVALID.getMessage());
        }

        //String us = jwtTokenUtil.getUserIdFromToken(token);
        Map<String, Object> tokenMap = jwtTokenUtil.refreshTokenAndGenerateToken(userId, username);

        return new CommonResult<>(200, ResponseCodeEnum.SUCCESS.getMessage(),tokenMap);
    }

访问时,参数为需要刷新的token,header中携带refresh token,验证refresh token通过,即可生成新的token值进行使用。