【SpringCloud-9】JWT

这一篇主要介绍一下,微服务之间的用户权限问题。 通常呢,对于用户的登录鉴权,有两种方式:

1、基于session的方式:

session是要存到服务端的,但是分布式服务太多,不可能每个服务端都存。  那就只有使用session共享,将sessionId放到cookie中。 但是呢也有问题,现在客户端形式很多,像PC端 h5对cookie的支持还好,移动端有时候对cookie就没法有效使用。 

2、基于token的方式:

token通常包含一些用户信息,生成后加密返给客户端。 服务端可以不用存储,返回给客户端自行存储。  但是呢,一般token都包含较多信息,每个接口都传输,带宽占用多。 另外,token验证都要请求认证中心,压力较大。 

3、基于JWT(JSON Web Token

  • 什么是JWT

JWT本身不是springcloud的东西,只是在微服务中使用起来会很方便。 认证服务按照jwt的格式,颁发授权令牌后,不用存储。JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证服务完成授权。

  • JWT结构

​​JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz

  • Header部分​​​​​​​

包含令牌类型和使用的hash算法(如HMAC SHA256或 RSA) ,如:​​​​​​​

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

将上面内容用Base64Url编码,就是JWT的第一部分。 

  • Payload部分

​​​​​​​这部分也是一个json对象,存放有效信息,如 iss(签发者), exp(过期时间戳), sub(⾯向的

⽤户)等,也可⾃定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原内容。

​​​​​​​{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

第二部分也是使用Base64Url编码。 

  • Signature部分 

签名部分,用于防止JWT内容被篡改。  处理方式是,将第一部分和第二部分的Base64编码字符串,用"."连接,再加上一个自定义的secret,一起使用Header中的签名算法加密。如secret为abc123:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  abc123
)

得到的结果:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

Z-quzzUBR0Yyj6B37GElTRVPiHoIAWY4-q9i05aYCA8

这个结果是可以被反解析的,各服务就可以自己解析了。

在springcloud中,如何使用JWT呢。 

比如,在认证服务器登录并颁发JWT令牌给客户端,后续在网关校验令牌,这时候网关无需请求认证服务器,自己可以反解析令牌并校验。 如果通过,继续访问后续服务。

【SpringCloud-9】JWT_第1张图片

 

项目搭建 示例:

  • JWT管理服务:主要提供创建jwt令牌和反解析令牌的方法,并提供jar包。
  • 认证中心:依赖JWT管理服务的jar包,登录成功后,调用jar包中的方法,创建令牌。
  • 网关:依赖JWT管理服务的jar包,反解析请求中的令牌,进行鉴权。

JWT管理服务:

1、导入jwt的包

        
                io.jsonwebtoken
            jjwt
            0.9.1
            compile
        

2、配置文件:

auth:
  jwt:
    enabled: true   # 是否开启JWT登录认证功能
    secret: abc123  # JWT 私钥,用于校验JWT令牌的合法性
    expiration: 3600000 # JWT 令牌的有效期,一个小时
    header: Authorization # HTTP 请求的 Header 名称,该 Header作为参数传递 JWT 令牌

3、configuration

@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;
 
    //是否使用默认的JWTAuthController
    private Boolean useDefaultController = false;
 
}

 4、核心api

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 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 generateTokenAndRefreshToken(String userId, String username) {
        //生成令牌及刷新令牌
        Map tokenMap = buildToken(userId, username);
        //redis缓存结果
        cacheToken(userId, tokenMap);
        return tokenMap;
    }
 
    //将token缓存进redis
    private void cacheToken(String userId, Map 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 buildToken(String userId, String username) {
        //生成token令牌
        String accessToken = generateToken(userId, username, null);
        //生成刷新令牌
        String refreshToken = generateRefreshToken(userId, username, null);
        //存储两个令牌及过期时间,返回结果
        HashMap 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 payloads) {
        Map claims = buildClaims(userId, username, payloads);;
 
        return generateToken(claims);
    }
    public String generateRefreshToken(String userId, String username, Map payloads) {
        Map claims = buildClaims(userId, username, payloads);
 
        return generateRefreshToken(claims);
    }
    //构建map存储令牌需携带的信息
    private Map buildClaims(String userId, String username, Map payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();
 
        Map 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 refreshTokenAndGenerateToken(String userId, String username) {
        Map 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 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 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;
    }

}

核心api中主要包含了创建jwt,解析jwt,刷新token等方法。 具体的可根据实际场景定制。

认证中心和网关,就没有什么特殊的了。 只需要依赖jwt管理服务的jar包,做jwt令牌的创建颁发,和令牌校验。

你可能感兴趣的:(SpringCloud,JWT)