最近一直在思考微服务架构下的最佳授权方式,在shiro与jwt之间所作权衡,,本文将阐述JWT 背景原理,以及提及我在开发系统过程中通过API网关来进行JWT鉴权实现过程,下图展示了系统的架构及JWT认证所处位置;
介绍
JWT (JSON Web Token) 是一套特别流行于分布式系统采用的授权标准 ,在采用加密等手段保证安全高效的同时,其基于JSON做为权限认证令牌的特性可以使其携带诸多非敏感数据,其所携带的数据可以确保授权的灵活高效;
其他认证方式
单体应用,基于session 进行认证控制(传统方式):
在传统的单体应用中,系统只需要通过session便可以足够做到完美的权限认证;
2.分布式系统,结合Redis等进行session:
session共享方式是将所有的用户会话集中存储与一台共享session服务器,一般使用redis进行缓存,每次服务接收到客户端请求便到共享session服务器查询该客户端的session,这样便保证了在分布式系统中的所有服务器所获得的是相同的session,这是我之前分布式场景下最常使用的认证方式,但其于JWT相比有什么优缺点呢?
与session共享机制的对比
1.代码侵入,在每个服务上必须有存取session鉴权判断的代码;
2.不安全,虽然redis等可以很方便的进行分布式部署,假若session服务器挂掉,系统便完全无法使用,不满足分布式系统中的高可用;
3.内存无法控制,若用户访问量激增极可能冲破内存,对内存要求高;
与之相反这些正是JWT的优势所在:
4.无代码侵入 ,只需要配置一个认证服务,其他服务可以无需关注权限,并可以提高到API网关进行权限拦截;
5.JWT不依赖session,对内存无要求,大量的访问也从容应对,不易产生硬件瓶颈;
6.可以在JWT中存储角色等信息,减少数据库查询或无需查询(之后可能会再写一篇介绍jwt结合RBAC的设计,只需缓存少量的资源与角色的映射可以完成复杂灵活的鉴权操作);
JWT的组成
JWT在未解码的条件下看,就是一串由两个 . 相连接的三段编码:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIQTI1NiJ9.eyJyb2xlIjpbeyJyb2xlSWQiOjEsInVzZXJuYW1lIjoiemhhb3poYW96aGFvMiJ9XX0=.26d6c858e666ee510eb14640689e1bb121
第一段:eyJ0eXAiOiJKV1QiLCJhbGciOiJIQTI1NiJ9 为header 保存加密算法等头部信息 其内容本质为一段经过BASE64 编码的JSON Object
第二段:eyJyb2xlIjpbeyJyb2xlSWQiOjEsInVzZXJuYW1lIjoiemhhb3poYW96aGFvMiJ9XX0= 为playload 保存服务端自定义的信息,如有效时间、用户ID 、角色ID、远程IP其内容本质为一段经过BASE64 编码的JSON Object
第三段:26d6c858e666ee510eb14640689e1bb121 为Signature 为一段加密后的字段,服务端在接收到客户端Header中的jwt后便通过与这段字段的比对可以得知header playload是否为服务端提供,是否被修改后文将阐述加密过程;
上方的JWT的原内容为:
# HEADER coding时忽略注释,JSON中不可以存在注释
{
"alg": "HS256", #签名加密算法
"typ": "JWT" #指明为一条jwt类型
}
# PLAYLOAD 可以交由服务端放入自身想放入的任何有利于鉴权的信息
切记不可放入敏感数据如密码Base64只是编码 jwt内容人人可读
{
"sub": "1234567890",
"username": "John Doe",
"admin": true
}
# SIGNATURE 签名加密算法采用HEADER中的alg指定算法
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
#这是加密公式,当然你也可以自定义,只需要覆盖到HEADER PLAYLOAD即可,这样第三方对jwt内容稍作改动将无法与服务端重新计算数值相匹配;
在加密过程中需要一个secret变量这是整个系统最机密的一条字符了,加密即通过base64编码的HEADER + PLAYLOAD + secret 进行加密,该值只能保持在服务端,外部不可见;
JWT过程(图片从JWT官网 copy)
JWT的过程,由客户端登录系统时登录成功,系统将生成相应的json数据编码并加密生成签名,形成一个 头部json Base64编码 + 荷载(playload) json Base64 + 对(头部json+ playload json)HA256加密生成的签名 的三段编码,各段编码以 “ . ” 隔开;注意第一二段(header & playload)采用Base64 编码 并非不可逆的加密算法,其仅仅起到编码作用,任何第三方都可以用Base64解码器对其数据进行解析还原,这是很多人存在的一个误区,第三段才是真实的不可逆加密采用HA256或MD5等都可以后面讲具体讲解加密流程及为何通过该段内容可以获知jwt真实性;
1.client 发起登录请求 携带 username password 等信息
2.server 接收用户登录请求,验证账户正确,正确返回一条jwt编码
3.客户端接收到jwt编码,之后每次发起请求在请求头中加入一条k/v值
4.server接收到client请求,获取到请求头中的jwt信息,对jwt进行校验若签名核对成功,则为server自身发出并未有第三方修改过真实可靠的信息,若为正确信息,对jwt中的header与playload进行Base64解析获取原json,读取json 中的信息获取用户的UserId、Role、jwt有效期等信息,在此时可以对用户访问的资源(路径)进行鉴权是否匹配该用户权限(该部分将在下一篇文章中提到);
5.若jwt合法,且符合server的鉴权规则那么返回给客户端相应请求;
加密及检验过程详解
前面已经或多或少提到了加密解密过程,若还是一头雾水请看该部分拨开迷雾,分布式系统是如何通过两段非加密的Base64编码数据及一段加密签名进行认证服务的;
加密:
生成头JSON,荷载(playload) JSON
将头JSON Base64编码 + 荷载JSON Base64编码 +secret 三者拼接进行加密得到签名
JSON Base64编码 + 荷载JSON Base64编码 + 签名 三者通过 . 相连接
一条 hhh.ppp.sss 格式的JWT 即生成
解密:
取得Jwt hhh.ppp.sss 格式字符,通过 . 将字符分为三段
对第一段进行Base64解析得到header json,获取加密算法类型
将第一段Header JSON Base64编码 + 第二段 荷载JSON Base64编码 + secret采用相应的加密算法加密得到签名
将步骤三得到的签名与步骤一分成的第三段也就是客户端传入的签名进行匹配,匹配成功说明该jwt为server自身产出;
获取playload内信息,通过信息可以做鉴权操作;
成功访问;
通过这些步骤,保证了第三方无法修改jwt,jwt只能自产自销,在分布式环境下服务接收到合法的jwt便可知是本系统内自身或其他服务发出的jwt,该用户是合法的;
JWT在Spring Cloud系统上的实现
核心代码:
CodecUtils.java
package cn.zynworld.hnister.common.utils;
import com.google.gson.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Created by zhaoyuening on 2018/1/26.
* 密码盐值加密
*/
public class CodecUtils {
//敏感值 用于对jwt HA256加密
private final static String SECRET = "jklf-=ertjerk.,sdf";
private static final Base64 BASE_64 = new Base64();
/**
* 盐值加密
* @param password
* @param sale
* @return
*/
public static String getSalePassword(String password,String sale){
password = getSHA256Str(password);
password = password + sale;
password = getSHA256Str(password);
return password;
}
/**
* 编码base64
* @param str
* @return
*/
public static String encodeBase64(String str){
return BASE_64.encodeToString(str.getBytes());
}
/**
* 解码base64
* @param str
* @return
*/
public static String decodeBase64(String str){
return new String(BASE_64.decode(str.getBytes()));
}
/**
* 获取盐值
* @return
*/
public static String getSale(){
return new Date().getTime()+"";
}
/**
* 获取SHA256加密字符串
* @param str
* @return
*/
public static String getSHA256Str(String str){
MessageDigest messageDigest;
String encdeStr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
byte[] hash = messageDigest.digest(str.getBytes("UTF-8"));
encdeStr = Hex.encodeHexString(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encdeStr;
}
/**
* 检验用户登录
* @param password
* @param sale
* @param encodedPassword
* @return
*/
public static boolean checkUser(String password,String sale,String encodedPassword){
if (StringUtils.isEmpty(password) || StringUtils.isEmpty(sale) || StringUtils.isEmpty(encodedPassword)){
return false;
}
String salePassword = getSalePassword(password, sale);
if (salePassword.equals(encodedPassword)){
return true;
}
return false;
}
//JWT
public static String getJwtString(){
return null;
}
//用于装载 jwt 信息的载体
public static class JwtBean{
private Map headMap = new HashMap();
private Map playloadMap = new HashMap();
private static Gson gson = new GsonBuilder().create();
private static JsonParser jsonParser = new JsonParser();
public JwtBean(){}
/**
* 通过jwt串 解码后获取JwtBean
* 若非合法jwt 将返回null
* @param jwt
* @return
*/
public static JwtBean getJwtBean(String jwt){
if (jwt == null){
return null;
}
String[] jwts = jwt.split("\\.");
if (jwts.length != 3){
return null;
}
//验证
String signature = getSHA256Str(jwts[0] + "." + jwts[1] + SECRET);
if (!jwts[2].equals(signature)){
return null;
}
//验证成功 构建jwtBean
JwtBean jwtBean = null;
try{
JsonElement headElement = jsonParser.parse(decodeBase64(jwts[0]));
JsonElement payloadElement = jsonParser.parse(decodeBase64(jwts[1]));
jwtBean = new JwtBean();
jwtBean.setHeadMap( JsonUtils.jsonToMap(decodeBase64(jwts[0])));
jwtBean.setPlayloadMap( JsonUtils.jsonToMap(decodeBase64(jwts[1])));
}catch (Exception e){
return null;
}
return jwtBean;
}
public void addHead(String key,Object value){
headMap.put(key,value);
}
public void addPlayload(String key,Object value){
playloadMap.put(key,value);
}
public Map getHeadMap() {
return headMap;
}
public Map getPlayloadMap() {
return playloadMap;
}
public Object getHead(String key){
return headMap.get(key);
}
public Object getPlayload(String key){
return playloadMap.get(key);
}
public String getHeadJson(){
return gson.toJson(headMap);
}
public String getPlayloadJson(){
return gson.toJson(playloadMap);
}
public JwtBean setHeadMap(Map headMap) {
this.headMap = headMap;
return this;
}
public JwtBean setPlayloadMap(Map playloadMap) {
this.playloadMap = playloadMap;
return this;
}
@Override
public String toString(){
String jwt = null;
String headJson = gson.toJson(headMap);
String playload = gson.toJson(playloadMap);
//base64
headJson = encodeBase64(headJson);
playload = encodeBase64(playload);
jwt = headJson + "." +playload;
jwt = jwt + "." + getSHA256Str(jwt + SECRET);
return jwt;
}
}
}
JsonUtils.java
package cn.zynworld.hnister.common.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.util.*;
/**
* Created by zhaoyuening on 2018/1/26.
*/
public class JsonUtils {
private final static JsonParser JSON_PARSER = new JsonParser();
public static Map jsonToMap(String json){
JsonElement element = JSON_PARSER.parse(json);
if (! element.isJsonObject()){
return null;
}
return (Map) jsonToObject(element);
}
public static Object jsonToObject(JsonElement element){
if (element.isJsonPrimitive()){
return element.getAsJsonPrimitive().getAsString();
}
if (element.isJsonObject()){
Map map = new HashMap();
for (String key :
element.getAsJsonObject().keySet()) {
JsonElement jsonElement = element.getAsJsonObject().get(key);
map.put(key, jsonToObject(jsonElement));
}
return map;
}
if (element.isJsonArray()){
List list = new ArrayList();
for (int i=0;i
JsonElement jsonElement = element.getAsJsonArray().get(i);
list.add(jsonToObject(jsonElement));
}
return list;
}
return null;
}
}
系统内所有编码加密相关功能都实现在了CodecUtils类下,另外jwt需要大量的json2map、map2json操作在JsonUtils类下依赖于google开源gson,可以通过jsonToMap等方法实现json字符到对象的转换;
以上代码最核心部分为我实现在CodecUtils下的内部类JwtBean,其可以帮助系统自动的在编码与对象间灵活切换,可以通过该类下的getJwtBean(String jwt)将客户端发来的jwt编码自动解析为系统可以获取信息的对象,若为非法jwt将返回null;
在CodecUtils类下有一个关键字段SECRET便是我们前面提到的,需要其与header playload联合加密获取到签名不得暴露;
jwt的生成(在负责账户的service中):
在我的系统中所有外部请求都需要通过Zuul API 网关,所以选择在zuul内写一个java的过滤器,对流量进行过滤:
这样即可以达成所有service不需要再关注认证鉴权等流程,服务的开发只需要专心的关注业务流程就好啦!
JWT相对灵活多变,我采用了自己实现不一定需要完全按标准实施,不过大家可以查看jwt官网获得相应的库也很方便哦。https://jwt.io/#libraries