登入认证--grafana,nacos,jellyfin的token获取及其授权认证规则(附代码)

自研项目集成开源软件 nacos,grafana,jellyfin

最近在做登录认证,除了项目中所用的jwt,还调研了开源软件,服务注册发现nacos,可视化grafana,音频jellyfin,因为本人是java程序员,对于nacos(java实现的),基本上就是了解下源代码就解决了,token部分本文也做了简单的记录。而grafan虽然是go写的,但是仔细去看它的配置文件会发现,其配置文件 grafana.ini 提供了 " Auth JWT ", 意味着我们不需要去完全明白go项目就可以摸清它的token生成和认证规则。对于jellyfin,作者试图去拉取了源码,也读了C#代码,它的AccessToken就是一串GUID。下面将分别记录各个开源项目获取其token的过程和结果 。

Nacos2.2.0

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;
    }
}

grafana9.3.2

直接进入配置文件: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中存在的一个。
防止读者走弯路,我这里直接:
登入认证--grafana,nacos,jellyfin的token获取及其授权认证规则(附代码)_第1张图片
然后复制左边的token就可以进行测试了。
测试工具:postman
GET http://localhost:3001/?orgId=1
header中添加 jwtToken :${token值}
如果该 token有效,返回200,否则 401,并且返回 invalid jwt。
作者曾多次去grafana日志中查看,拿着错误日志信息到源码中找代码(因为go语言对java开发者来说并不友好)。

代码参考上面nacos部分

jellyfin

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的学习就到这了,有不足的地方欢迎读者指正。

你可能感兴趣的:(grafana,java,开发语言)