【项目实战】Spring Cloud Gateway 实现JWT / Token登录认证流程

目录

一、前言

1. Spring Cloud Gateway 简介

2. token 简介

二、登录认证流程

三、项目实战

1. auth 认证服务

2. gateway 网关服务


一、前言

本文将演示,登录时,如何创建token,然后在网关校验token,并提取用户信息放到Header请求头中传给下游业务系统。

项目采用 Spring Cloud Gateway作为微服务的统一路由的网关,由auth认证模块负责生成token,gateway网关模块负责校验token,统一入口

网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。

1. Spring Cloud Gateway 简介

Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。

2. token 简介

一个jwt实际上就是一个字符串,它由三部分组成,头部载荷签名,这三个部分都是json格式。

详情可参考:https://blog.csdn.net/a1036645146/article/details/103726635

2.1 头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。

{
    "typ": "JWT",
    "alg": "HS256"
}

在这里,我们说明了这是一个JWT,并且我们所用的签名算法是HS256算法。

2.2 载荷(Payload)

载荷可以用来放一些不敏感的信息。

{
    "iss": "John Wu JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "[email protected]",
    "from_user": "B",
    "target_user": "A"
}

这里面的前五个字段都是由JWT的标准所定义的。

  • iss: 该JWT的签发者
  • sub: 该JWT所面向的用户
  • aud: 接收该JWT的一方
  • exp(expires): 什么时候过期,这里是一个Unix时间戳
  • iat(issued at): 在什么时候签发的

把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号.连接在一起(头部在前),形成新的字符串:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

2.3 签名(signature)

最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密后的内容也是一个字符串,最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分,服务端也就无法通过,在jwt中,消息体是透明的,使用签名可以保证消息不被篡改。

当然, 如果一个人的token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的session id 被别人偷走是一样的。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJyb2xlcyI6InB1cmNoYXNlciIsInNvdXJjZSI6IlBDIiwidXNlck5hbWUiOiIxNTYyMjEzMzIzMSIsImV4cCI6MTYwNDgwMDM5NSwidXNlcklkIjoiMTE5MCJ9
.zpM6dyakS1N6kySMIEHuOZfN8l4WlybRbq6VK1cDqGc

二、登录认证流程

用户登录成功以后,认证服务(auth模块)生成token,此后前端的所有请求都在请求头中携带此token。网关负责校验token,并将用户信息放入请求头Header,以便下游系统可以方便的获取用户信息。

基于 Token 认证的基本认证流程如下:

  1. 用户输入登录信息(或者调用 Token 接口,传入用户信息),发送到认证服务进行认证。注:身份认证服务可以和服务端在一起,也可以分离,看微服务拆分情况了,我这里是单独一个auth模块。
  2. 身份验证服务(auth)验证登录信息是否正确,正确则返回Token(一般接口中会包含用户基础信息、权限范围、有效时间等信息)。
  3. 客户端存储Token,再次请求其他接口时,可将 Token 放在 HTTP 请求头中,发起相关 API 调用。
  4. 客户端访问后端接口,统一都需要经过网关,由网关统一配置拦截器,验证 Token 权限,通过后才能转发到相应的后端其他微服务接口。
  5. 验证通过,服务端返回相关资源和数据。

项目涉及模块:

  • gateway -- 网关服务
  • auth -- 认证服务
  • user-service
  • order-service
  • (其它微服务)......

每个模块应用都是基于SpringBoot 构建,使用Nacos作为 配置中心与注册中心,服务间的接口调用采用Feign,发布Http 接口( REST API ),使用gateway作为API统一入口,前端请求不能直接调用下游微服务的具体接口。

三、项目实战

1. auth 认证服务

通过SpringBoot构建一个auth认证服务模块,注册与配置中心统一使用 Nacos。

该模块主要提供登录接口,生成token。

1.1 pom.xml

需要引入jwt工具包,使用最新稳定版本即可。

<dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>${jwt.version}</version>
</dependency>

1.2 JWTUtils.java 工具类

/**
 * jwt工具类
 *
 * @author stwen_gan
 * @since 
 */
public class JWTUtils {

    // token 签名的秘钥,可设置到配置文件中
    private static final String SECRET_KEY = "secretKey:123456";
    // token过期时间
    public static final long TOKEN_EXPIRE_TIME = 7200 * 1000;

    /**
     * 生成jwt
     */
    public String createJwt(String userId){
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        //设置头信息
        HashMap<String, Object> header = new HashMap<>(2);
        header.put("typ", "JWT");
        header.put("alg", "HS256");
        // 生成 token:头部+载荷+签名
        return JWT.create().withHeader(header)
                .withClaim(RequestKeyConstants.USER_ID,userId)
                .withExpiresAt(new Date(System.currentTimeMillis()+TOKEN_EXPIRE_TIME)).sign(algorithm);
    }

    /**
     * 解析jwt
     */
    public Map<String, Claim> parseJwt(String token) {
        Map<String, Claim> claims = null;
        try {
            Algorithm algorithm = Algorithm.HMAC256(key);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            claims = jwt.getClaims();
            return claims;
        } catch (Exception e) {
            return null;
        }
    }
}

注:该工具好类的签名秘钥、过期时间 建议统一配置到配置中心的文件,通过方法入参传入即可。

Token组成:

  • 头部
{
    "typ": "JWT",
    "alg": "HS256"
}
  • 载荷 :用户存放一些非敏感的用户信息(如用户id、用户名、权限、角色等)
  • 签名:通过一个给定的秘钥对上面两部分进行签名

1.3 登录接口

LoginController

注:该接口为了方便演示,省略了部分代码。

@PostMapping("/auth/login")
public Result login(HttpServletRequest request, @Valid @RequestBody LoginDTO loginDto) {

    String ip = IpAddressUtils.getIpAddr(request);
    // 获取用户信息、比对密码
    Result<UserDTO> result = loginFeignApi.login(loginDto,ip);
    if(ResultCode.SUCCESS.getCode()!=result.getCode()){
        log.error(result.getMsg());
        return result;
    }
    UserDTO user = result.getData();
    String token = JWTUtils.createJwt(user.getId() + "");
    data.put("token",token);
    return Result.success(data);
}

其中,loginFeignApi.login(loginDto,ip) 是 通过feign 调用 user-service 模块的接口:根据用户名查询用户信息,并对密码进行MD5加密加盐值校验等,如下:

User user = userService.getUserByName(loginDTO.getUserName());
// 密码md5 校验
if (!user.getPassword().equals(MD5Utils.toMD5Password(user.getUserSalt(), loginDTO.getPassword()))){
    userService.userLog(user,ip,UserLogTypeEnum.LOGIN_FAIL,loginDTO.getSource());
    return Result.error(ResultCode.PASSWORD_ERROR);
}

2. gateway 网关服务

同理,通过SpringBoot构建一个gateway 网关服务模块,注册与配置中心统一使用 Nacos,并配置其他服务接口的路由策略,作为其他服务API的统一入口。

网关负责校验token,并将用户相关信息放入请求头Header,以便下游系统可以方便的获取用户信息。

2.1 pom.xml  

同理,需要需要引入jwt工具包

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

2.2 application.properties

server.port=1234
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# gateway 路由策略
spring.cloud.gateway.routes[0].id=user
spring.cloud.gateway.routes[0].uri=lb://user-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/userService/**
spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
spring.cloud.gateway.routes[1].id=order
spring.cloud.gateway.routes[1].uri=lb://order-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/orderService/**
spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1

### 。。。。。。。

2.3 校验Token

我们可以通过实现 GatewayFilter 或 GlobalFilter 过滤器接口,前端请求通过gateway时,将被过滤校验token的合法性,这里实现GlobalFilter 接口。

TokenFilter.java

/**
 * @description: token过滤器
 * @author: xianhao_gan
 * @date:
 **/
@Slf4j
@Component
public class TokenFilter implements GlobalFilter, Ordered {

    @Autowired
    private JwtUtils jwtUtils;

    /**
     * 校验token
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst(RequestKeyConstants.TOKEN);
        //检查token是否为空
        if (StringUtils.isEmpty(token)) {
            return denyAccess(exchange, ResultCode.TOKEN_NULL);
        }

        Map claimMap1 = jwtUtils.parseJwt(token);
        //token有误
        if (claimMap.containsKey("exception")) {
            log.error() (claimMap1.get("exception").toString());
            return denyAccess(exchange, ResultCode.TOKEN_INVALID);
        }

        //token无误,将用户信息设置进header中,传递到下游服务
        Map<String, Claim> claimMap = claimMap1;
        String userId = claimMap.get(RequestKeyConstants.USER_ID).asString();
        Consumer<HttpHeaders> headers = httpHeaders -> {
            httpHeaders.add(RequestKeyConstants.USER_ID, userId);
        };
        request.mutate().headers(headers).build();
        
        // todo 权限校验

        return chain.filter(exchange);
    }

    /**
     * 拦截并返回自定义的json字符串
     */
    private Mono<Void> denyAccess(ServerWebExchange exchange, ResultCode resultCode) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        //这里在返回头添加编码,否则中文会乱码
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] bytes = JSON.toJSONBytes(Result.error(resultCode), SerializerFeature.WriteMapNullValue);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(buffer));
    }

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

}

  • 当然,实际项目中的代码会比这些复杂,这里为了方便演示,说明原理思路,省略了部分代码,实际设计与开发中可根据此思路来扩展。

  • 对于登录后生成的token,后端设置了固定的有效期,在有效期内用户携带token访问没问题,当tokent过期后会失效,前端会跳转到登录页面让用户重新登录,但用户体验不友好。

  • 改进:活跃的用户应该在无感知的情况下,token失效后自动获取新的token,携带这个新的token进行访问,而长时间不活跃的用户应该在jwt失效后需要重新的登录认证。后续有时间再补充 token超时刷新策略。

参考:https://www.cnblogs.com/cjsblog/p/12425912.html

史上最强Tomcat8性能优化

阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路

B2B电商平台--ChinaPay银联电子支付功能

学会Zookeeper分布式锁,让面试官对你刮目相看

SpringCloud电商秒杀微服务-Redisson分布式锁方案

查看更多好文,进入公众号--撩我--往期精彩

一只 有深度 有灵魂 的公众号0.0

猜你喜欢

转载自blog.csdn.net/a1036645146/article/details/109546416
今日推荐