最近在做登录认证,除了项目中所用的jwt,还调研了开源软件,服务注册发现nacos,可视化grafana,音频jellyfin,因为本人是java程序员,对于nacos(java实现的),基本上就是了解下源代码就解决了,token部分本文也做了简单的记录。而grafan虽然是go写的,但是仔细去看它的配置文件会发现,其配置文件 grafana.ini 提供了 " Auth JWT ", 意味着我们不需要去完全明白go项目就可以摸清它的token生成和认证规则。对于jellyfin,作者试图去拉取了源码,也读了C#代码,它的AccessToken就是一串GUID。下面将分别记录各个开源项目获取其token的过程和结果 。
nacos是基于java maven 项目构建,github:https://github.com/alibaba/nacos
登录的逻辑接口是:com.alibaba.nacos.plugin.auth.impl.controller.UserController
当用户登录匹配用户名和密码成功后,nacos会返回给前端一个JWT token,实现的底层代码在
com.alibaba.nacos.plugin.auth.impl.JwtTokenManager#createToken
实现规则:配置文件中的
nacos.core.auth.plugin.nacos.token.secret.key 作为jwt的签名密钥
nacos.core.auth.plugin.nacos.token.expire.seconds 作为token的失效时间
加密算法是:SignatureAlgorithm.HS256
sub:nacos的用户名,
将 JwtTokenManager 直接拿过来稍作修改,作为一个jwt 的工具类也是极好的。
假设已经获取到了nacos的token,怎么测试呢?
启动nacos->访问 localhost:8848/nacos->f12->application(或者中文‘应用’)->local storage(‘本地存储空间’)->
双击密钥输入框,输入:token
双击值输入框,输入:{“accessToken”:“eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTY3NzA4MDg3Mn0.cBF67mhkJUVwcx4SZ0z1iaVIrbmj8I8IXBmyggwuZsc”,“tokenTtl”:10,“globalAdmin”:true,“username”:“jack”}
,作为测试,只需要将 accessToekn替换为通过认证规则获取到的接口即可,保存刷新网页即可发现,nacos已经自动登录。至此说明,token有效。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.security.Keys;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
/**
* JWT token manager.
* @author slc
*/
@Component
public class JwtTokenManager {
private static final String AUTHORITIES_KEY = "auth";
/**
* secret key.
*/
@Value("${token.jwt.secretKey}")
private String secretKey;
/**
* kid and k for grafana
*/
private String kid = "2391ecd4-3c16-4a54-9083-2269f556b5da";
private String k = "Y4_Om2Jh-AW_5j6Jc87AffSgskxFWpSFXrFMC9Ju2GQ";
/**
* secret key byte array.
*/
private byte[] secretKeyBytes;
/**
* Token validity time(seconds).
*/
@Value("${token.jwt.tokenValidityInSeconds}")
private long tokenValidityInSeconds;
private JwtParser jwtParser;
/**
* init tokenValidityInSeconds and secretKey properties.
*/
public String getSecretKey() {
return secretKey;
}
/**
* Create token.
*
* @param userName auth info
* @return token
*/
public String createToken(String userName) {
long now = System.currentTimeMillis();
Date validity;
validity = new Date(now + this.getTokenValidityInSeconds() * 1000L);
Claims claims = Jwts.claims().setSubject(userName);
return Jwts.builder().setClaims(claims).setExpiration(validity)
.signWith(Keys.hmacShaKeyFor(this.getSecretKeyBytes()), SignatureAlgorithm.HS256).compact();
}
/**
* for grafana
*
* @param userName
* @return
*/
public String createGrafanaToken(String userName) {
long now = System.currentTimeMillis();
Date validity = new Date(now + this.getTokenValidityInSeconds() * 1000L);
Claims claims = Jwts.claims().setSubject(userName);
HashMap<String, Object> map = new HashMap<>();
map.put("kid", kid);
map.put("typ", "JWT");
return Jwts.builder().setHeader(map).setClaims(claims).setExpiration(validity)
.signWith(Keys.hmacShaKeyFor(Base64.decodeBase64(k)), SignatureAlgorithm.HS256).compact();
}
/**
* validate token.
*
* @param token token
*/
public void validateToken(String token) {
if (jwtParser == null) {
jwtParser = Jwts.parserBuilder().setSigningKey(this.getSecretKeyBytes()).build();
}
jwtParser.parseClaimsJws(token);
}
public byte[] getSecretKeyBytes() {
if (secretKeyBytes == null) {
try {
secretKeyBytes = Decoders.BASE64.decode(secretKey);
} catch (DecodingException e) {
secretKeyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
}
}
return secretKeyBytes;
}
public long getTokenValidityInSeconds() {
return tokenValidityInSeconds;
}
}
直接进入配置文件:grafana.ini,如果是windows操作系统,需要copy sample.ini ,更名 custom.ini
当对custom.ini做修改的时候,配置生效。
进入.ini配置文件,搜索:Auth JWT
[auth.jwt]
enabled = true
header_name = jwtToken
;email_claim = sub
username_claim = sub
;jwk_set_url =
jwk_set_file = E:/grafana-9.3.2/json/jwks.json
cache_ttl = 60m
;expect_claims = {“aud”: [“foo”, “bar”]}
;key_file = /path/to/key/file
;role_attribute_path =
;role_attribute_strict = false
;auto_sign_up = false
;url_login = false
;allow_assign_grafana_admin = false
第一次配置可以直接照抄。
; 是注释符号,我这里就解释下放开注释的几行配置。
enabled = true 开启 jwt 认证
header_name = jwtToken 请求头信息带着key为 jwtToken的键值对信息,value即为token。也说明grafan的token认证 是从头信息中获取到token的
username_claim = sub:jwt 的payload 中的sub字段的value值 就是 登录的用户名
jwk_set_file = E:/grafana-9.3.2/json/jwks.json:jwk 就是一个密钥,类似于nacos中的nacos.core.auth.plugin.nacos.token.secret.key ,这里它是一个json文件,我本地的配置如下:
{
“keys”: [{
“kty”: “oct”,
“kid”: “2391ecd4-3c16-4a54-9083-2269f556b5da”,
“k”: “Y4_Om2Jh-AW_5j6Jc87AffSgskxFWpSFXrFMC9Ju2GQ”,
“alg”: “HS256”
}]
}
,当然我们也可以从网站获取,只不过格式要参照上面的。获取的网址:https://8gwifi.org/jwkfunctions.jsp
,然后我们就可以在https://jwt.io/ 生成jwt token了
注意事项:
HERADER中要配置 “kid”: “2391ecd4-3c16-4a54-9083-2269f556b5da”,否则认证会报错,作者也是看了grafana许多报错日志才把这个坑填上。
“k”: “Y4_Om2Jh-AW_5j6Jc87AffSgskxFWpSFXrFMC9Ju2GQ” 是签名密钥,这个填在VERIFY SIGNATURE部分,并且把 secret base64 encoded 勾选起来。
secret base64 encoded:意思是指该密钥是经过base64加密后生成的密钥,即原密钥->base64加密=现密钥k:Y4_Om2Jh-AW_5j6Jc87AffSgskxFWpSFXrFMC9Ju2GQ
PAYLOAD部分只需要配置之一个sub,用户名一定要是grafana中存在的一个。
防止读者走弯路,我这里直接:
然后复制左边的token就可以进行测试了。
测试工具:postman
GET http://localhost:3001/?orgId=1
header中添加 jwtToken :${token值}
如果该 token有效,返回200,否则 401,并且返回 invalid jwt。
作者曾多次去grafana日志中查看,拿着错误日志信息到源码中找代码(因为go语言对java开发者来说并不友好)。
代码参考上面nacos部分
jellyfin 是基于C# 语言编写的一个媒体管理软件,作者也github下载过源码:https://github.com/jellyfin/jellyfin
其token认证的接口是:UserController /Users/AuthenticateByName
顺着代码可以摸索到 其token在 class Device 中有定义,
AccessToken = Guid.NewGuid().ToString(“N”, CultureInfo.InvariantCulture);
可见,其token 只是一串全局唯一的字符串,保存在*.db文件中。下次访问资源的时候带上用户id和token 比对校验。
那么对于这样的生成规则作者思考要么我也生成一个GUID放进 jellyfin的db文件中,但是转念一想,一片茫然。作者果断采取最笨最有效的方式,java接口 resetTemplate 模拟http请求登录接口,将返回的token值封装。
url :http://localhost:8096//Users/AuthenticateByName
请求体参数:
requestMap.put(“Username”, “admin”);
requestMap.put(“Pw”, “admin”);
踩坑点:源码中读到 header 中要有 X-Emby-Authorization 的值,可以将作者的值完全拷贝MediaBrowser Client=“Jellyfin Web”, Device=“Chrome”, DeviceId=“TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwOC4wLjAuMCBTYWZhcmkvNTM3LjM2fDE2NzY5NDU3MzA4Mzk1”, Version=“10.8.9”
此处由于是封装http请求,不再多述。
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.util.LinkedHashMap;
import java.util.Map;
public class RestTemplateDemo {
public static void main(String[] args) throws JsonProcessingException {
String requestUrl = "/Users/AuthenticateByName";
String url = "http://localhost:8096" + requestUrl;
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("X-Emby-Authorization", "MediaBrowser Client=\"Jellyfin Web\", Device=\"Chrome\", DeviceId=\"TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwOC4wLjAuMCBTYWZhcmkvNTM3LjM2fDE2NzY5NDU3MzA4Mzk1\", Version=\"10.8.9\"");
JSONObject requestMap = new JSONObject();
requestMap.put("Username", "admin");
requestMap.put("Pw", "admin");
HttpEntity<JSONObject> entity = new HttpEntity<>(requestMap, headers);
ObjectMapper objectMapper = new ObjectMapper();
try {
String similarJSON = objectMapper.writeValueAsString(requestMap);
System.out.println(similarJSON);
} catch (Exception e) {
e.printStackTrace();
}
//使用JSONObject,不需要创建实体类VO来接受返参,缺点是别人不知道里面有哪些字段,即不知道有那些key
JSONObject body = restTemplate.postForObject(url, entity, JSONObject.class);
String accessToken = (String) body.get("AccessToken");
Map<String, String> user = (LinkedHashMap<String, String>) body.get("User");
String id = user.get("Id");
System.out.println(accessToken);
System.out.println(id);
}
}
至此,对于上述开源软件token的学习就到这了,有不足的地方欢迎读者指正。