这一篇主要介绍一下,微服务之间的用户权限问题。 通常呢,对于用户的登录鉴权,有两种方式:
1、基于session的方式:
session是要存到服务端的,但是分布式服务太多,不可能每个服务端都存。 那就只有使用session共享,将sessionId放到cookie中。 但是呢也有问题,现在客户端形式很多,像PC端 h5对cookie的支持还好,移动端有时候对cookie就没法有效使用。
2、基于token的方式:
token通常包含一些用户信息,生成后加密返给客户端。 服务端可以不用存储,返回给客户端自行存储。 但是呢,一般token都包含较多信息,每个接口都传输,带宽占用多。 另外,token验证都要请求认证中心,压力较大。
3、基于JWT(JSON Web Token )
JWT本身不是springcloud的东西,只是在微服务中使用起来会很方便。 认证服务按照jwt的格式,颁发授权令牌后,不用存储。JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证服务完成授权。
JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
包含令牌类型和使用的hash算法(如HMAC SHA256或 RSA) ,如:
{
"alg": "HS256",
"typ": "JWT"
}
将上面内容用Base64Url编码,就是JWT的第一部分。
这部分也是一个json对象,存放有效信息,如 iss(签发者), exp(过期时间戳), sub(⾯向的
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
第二部分也是使用Base64Url编码。
签名部分,用于防止JWT内容被篡改。 处理方式是,将第一部分和第二部分的Base64编码字符串,用"."连接,再加上一个自定义的secret,一起使用Header中的签名算法加密。如secret为abc123:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), abc123 )
得到的结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
Z-quzzUBR0Yyj6B37GElTRVPiHoIAWY4-q9i05aYCA8
这个结果是可以被反解析的,各服务就可以自己解析了。
在springcloud中,如何使用JWT呢。
比如,在认证服务器登录并颁发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令牌的创建颁发,和令牌校验。