从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

在上一篇中,我们建立了Auth服务,这个服务就是专门用来处理用户认证,授权的服务,这里我们集成JWT来作为认证的票据。

什么JWT

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

JWT请求流程

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

 

1. 用户使用账号和面发出post请求;
2. 服务器使用私钥创建一个jwt;
3. 服务器返回这个jwt给浏览器;
4. 浏览器将该jwt串在请求头中像服务器发送请求;
5. 服务器验证该jwt;
6. 返回响应的资源给浏览器。

JWT的主要应用场景

身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

优点

1.简洁(Compact): 可以通过URLPOST参数或者在HTTP header发送,因为数据量小,传输速度也很快
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。

`

JWT的结构

JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。
就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证

Header

JWT的头部承载两部分信息:token类型和采用的加密算法。

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

声明类型:这里是jwt
声明加密的算法:通常直接使用 HMAC SHA256

加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。
MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

Payload

载荷就是存放有效信息的地方。
有效信息包含三个部分
1.标准中注册的声明
2.公共的声明
3.私有的声明

标准中注册的声明 (建议但不强制使用) :

iss: jwt签发者
sub: 面向的用户(jwt所面向的用户)
aud: 接收jwt的一方
exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 :

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 :

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

Signature

jwt的第三部分是一个签证信息
这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

参考资料:https://www.jianshu.com/p/e88d3f8151db

新建服务springcloud_common,存放公共依赖

和以前一样建立maven项目:

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

BaseApiService:获取用户信息,后面用到
BaseResult:封装通用的返回接口信息
AuthApiConstant类封装一些通用的常量值
BaseApiConstant类封装的是通用返回的接口常量:

包redis下是封装的关于redis的一些配置信息,在每个引用到Common依赖的服务中,都要在配置文件中配置redis

RedisConfig:redis配置信息
RedisParameter: redis参数信息
RedisUtil: 工具类,用于操作redis

代码的话我就不贴出来了,你们对照着源码拷贝

然后我们在Auth服务配置Redis的相关信息

server:
  port: 8003
spring:
  application:
    name: api-auth
  redis:
    hostserver: 192.168.0.132:6379 #该节点配置redis单机,配置后下面哨兵模式失效
    database: 4 # Redis数据库索引(默认为0)
    timeout: 10000 # 连接超时时间(毫秒)
    password:  # Redis服务器连接密码(默认为空)
    perKey: general_redis #redis前缀
    sentinel:
      master: mymaster
      nodes:
    lettuce:
      pool:
        max-active: 200 # 连接池最大连接数(使用负值表示没有限制
        max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 10 # 连接池中的最大空闲连接
        min-idle: 0  # 连接池中的最小空闲连接
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8001/eureka/

Auth集成JWT

pom引入JWT依赖,这里使用jsonwebtoken

  <!-- 引入jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

封装JWT工具类:

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

package com.springcloud.jwt;

import cn.hutool.core.date.DateUtil;

import com.springcloud.common.constant.AuthApiConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

/**
 * JWT工具
 */
@Component
@Slf4j
public class JwtUtil {
    /**
     * 密钥
     */
    private static final String SECRET = AuthApiConstant.AUTH_SECRET;


    /**
     * 生成用户token,设置token超时时间
     */
    public String createToken(Map map) {
        Claims claims = Jwts.claims();
        claims.put(AuthApiConstant.AUTH_USER_INFO_NAME, map);
        return Jwts.builder().setClaims(claims).setExpiration(DateUtil.offsetSecond(new Date(), Integer.parseInt(map.get(AuthApiConstant.AUTH_USER_REFRESH_NAME).toString())))
                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

    /**
     * 通过Token获取用户信息
     *
     * @param token
     * @return
     */
    public Claims getClaimByToken(String token) {
        try {
            Jws<Claims> jws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            return jws.getBody();
        } catch (Exception e) {
            return null;
        }

    }


    /**
     * 校验token是否过期
     *
     * @param expiration
     * @return
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

}

注意:

我们这里Auth服务的方法全部抽取到一个集中管理的服务中:

在 springcloud_parent下新建服务springcloud_api,springcloud_api下又有很多子模块,统一管理每个服务的实体类和接口,不明白的直接看荡源码看一下

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

AuthService:
package com.springcloud.service;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Map;

public interface AuthService {
    @GetMapping("/auth/getToken")
    Map<String, Object> getToken(@RequestParam("userName") String userName, @RequestParam("password") String password);

    @GetMapping("/auth/verifyToken")
    Map<String, Object> verifyToken(@RequestParam(value = "token") String token);

    @GetMapping("/auth/refreshToken")
    Map<String, Object> refreshToken(@RequestParam(value = "refreshToken") String refreshToken);
}

服务Auth新建类TokenManage,并且实现AuthService

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

TokenManage:

package com.springcloud.manage;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.springcloud.common.constant.AuthApiConstant;
import com.springcloud.common.redis.RedisUtil;
import com.springcloud.jwt.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class TokenManage {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;


    /**
     * 生成token
     *
     * @param userId             用户id
     * @param userName           名字
     * @param expire             token 过期实际时间
     * @param refreshTokenExpire refreshToken过期时间
     * @return
     */
    public Map<String, Object> getTokenInfo(String userId, String userName,
                                            int expire, int refreshTokenExpire) {
        Map<String, Object> map = new HashMap();
        map.put(AuthApiConstant.AUTH_USER_USERNAME_NAME, userName); //用户名
        map.put(AuthApiConstant.AUTH_USER_ID, userId);    //用户编号
        map.put(AuthApiConstant.AUTH_USER_REFRESH_NAME, expire);// Token过期时间
        //产生认证token
        String token = jwtUtil.createToken(map);
        //生成refreshToken
        String refreshToken = redisUtil.getKeys(AuthApiConstant.AUTH_REDIS_TOKENKEY + userId + ":*").stream().findFirst().orElse(null);
        if (StrUtil.isBlank(refreshToken)) {
            refreshToken = IdUtil.simpleUUID();
        } else {
            refreshTokenExpire = Convert.toInt(redisUtil.getExpire(refreshToken));
            refreshToken = refreshToken.substring(refreshToken.lastIndexOf(":") + 1);
        }
        //将用户信息map放到redis
        redisUtil.set(AuthApiConstant.AUTH_REDIS_TOKENKEY + userId + ":" + refreshToken, map, refreshTokenExpire);
        map.put(AuthApiConstant.TOKEN_NAME, token);
        map.put(AuthApiConstant.AUTH_USER_REFRESHTOKEN_NAME, refreshToken);
        map.put(AuthApiConstant.AUTH_USER_REFRESHTOKENEXPIRE_NAME, refreshTokenExpire);
        return map;
    }
}

token过期刷新方案

1、单点登录
用户登录,后端验证用户成功之后生成两个token,这两个token分别是access_token(访问接口使用的token)、refresh_token(access_token过期后用于刷续期的token,注意设置refresh_token的过期时间需比access_token的过期时间长),后端将用户信息和这两个token存放到redis中并返回给前端。
前端在获取到登录成功返回的两个token之后,将之存放到localStorage本地存储中。
2、接口请求
前端封装统一接口请求函数、token刷新函数,在请求成功之后对返回结果进行校验,如果token过期,则调用token刷新函数请求新的token.
后端在接收到token刷新请求之后通过结合redis中存放的用户信息、token和refresh_token对请求参数进行验证,验证通过之后生成新的token和refresh_token存放到redis中并返回给前端。至此完成token刷新。

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT
启动项目后:

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT然后这里的界面我是集成的Swagger2,有个配置文件

从零开始搭建SpringCloud微服务框架(三)————Auth服务集成JWT

代码:

package com.springcloud.config;

import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
import com.springcloud.common.constant.AuthApiConstant;
import io.swagger.annotations.Api;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {
    @Bean
    public Docket defaultApi2() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title(AuthApiConstant.SWAGGER_TITLE)
                .version(AuthApiConstant.SWAGGER_VERSION)
                .build();
    }
}

东西很多,用博客写出来难免会漏掉一些东西,遗漏的地方请看源码,先写到这吧,累了。

不足之处,多加指教。

项目地址:

https://gitee.com/bosszddquan/springcloudframe

上一篇:SpringCloud中利用OAuth2实现客户端单设备登录


下一篇:定义类