前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于Maxkey Oauth2接入Grafana,实现单点登录

基于Maxkey Oauth2接入Grafana,实现单点登录

作者头像
MaxKey单点登录开源官方
发布2024-05-10 09:17:00
970
发布2024-05-10 09:17:00
举报

1、接入版本

Maxkey v4.0.3GA Grafana 9.0.7

2、Maxkey接入Grafana的认证流程

3、具体实现步骤

3.1、修改Grafana配置,开启Oauth认证

修改custom.ini文件,如果没有custom.ini文件,可从conf下复制default.ini文件,然后改名为custom.ini。

代码语言:txt
复制
#################################### Server ####################
[server]
# 将Grafana的访问地址设置为Maxkey的访问地址,便于将cookie存入同一个域名下
domain = sso.maxkey.top

# 添加/grafana路径,便于在nginx中进行拦截跳转
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana

#################################### Security #######################
[security]

# 该参数关系到oauth_state的生成规则,无需修改
secret_key = SW2YcwTIb9zpOOhoPsMm

#################################### Generic OAuth #################
[auth.generic_oauth]
name = OAuth
icon = signin
enabled = true
allow_sign_up = true
auto_login = true
#Maxkey平台颁发的client_id 
client_id = xxxxxxx
#Maxkey平台颁发的client_secret 
client_secret = xxxxxxx
scopes = user:read
empty_scopes = false
email_attribute_name = 
email_attribute_path = 
login_attribute_path = user
name_attribute_path =
role_attribute_path = 
role_attribute_strict = false
groups_attribute_path =
id_token_attribute_name =
team_ids_attribute_path =
auth_url = http://sso.maxkey.top/sign/authz/oauth/v20/authorize
token_url = http://sso.maxkey.top/sign/authz/oauth/v20/token
api_url =  http://sso.maxkey.top/sign/api/oauth/v20/me
teams_url =
allowed_domains = 
team_ids =
allowed_organizations =
tls_skip_verify_insecure = false
tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = false

#################################### Basic Auth #####################
[auth.basic]
#关闭默认的登录方式
enabled = false

3.2、Maxkey的相关配置

3.2.1、新增maxkey-web 认证平台模块下application-http.properties配置
代码语言:txt
复制
#填写Grafana中的secret_key的值 
maxkey.sso.grafana.secretKey = SW2YcwTIb9zpOOhoPsMm
#Maxkey颁发给Grafana的秘钥
maxkey.sso.grafana.ClientSecret = xxxxx

3.2.2、通过Maxkey的管理平台配置Grafana认证信息

3.2.3、在Nginx中新增配置
代码语言:txt
复制
server {
    listen 3000;
    server_name localhost;

    location /grafana{
        proxy_pass http://sso.maxkey.top:3000/grafana;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host:3000;
        server_name_in_redirect on;
    }
}

3.2.4、对Maxkey的maxkey-protocol-oauth模块做适应性的修改

1)对org.maxkey.authz.oauth2.provider.endpoint.org.maxkey.authz.oauth2.provider.endpoint类进行修改

代码语言:txt
复制
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;

@Tag(name = "2-1-OAuth v2.0 API文档模块")
@Controller
@RefreshScope
public class AuthorizationEndpoint extends AbstractEndpoint {

private static final String OAUTH_STATE_COOKIE_NAME = "oauth_state";
// An highlighted block
@Value("${maxkey.sso.grafana.secretKey}")
private String grafanaSecretKey;

@Value("${maxkey.sso.grafana.ClientSecret}")
private String ssoClientSecret;

/*隐藏无需修改的方法*/

private String getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser,String returnUrl) {
        try {
            String  successfulRedirect = getSuccessfulRedirect(
                    authorizationRequest,
                    generateCode(authorizationRequest, authUser)
            );
            logger.info("getAuthorizationCodeResponse returnUrl:"+returnUrl);

            //往grafana跳转登录,写入oauth_state参数
            if(successfulRedirect.contains("grafana")){
                String state = this.generateStateString();
                String hashStatecode = this.hashStatecode(state,grafanaSecretKey,ssoClientSecret);
                successfulRedirect = successfulRedirect + "&state="+state;
                HttpServletRequest request = WebContext.getRequest();
                String serverName = request.getServerName();
WebContext.setCookie(WebContext.getResponse(),serverName,OAUTH_STATE_COOKIE_NAME,hashStatecode,10);
            }
            _logger.debug("successfulRedirect " + successfulRedirect);
            return successfulRedirect;
        }
        catch (OAuth2Exception e) {
            return getUnsuccessfulRedirect(authorizationRequest, e, false);
        }
    }

/**
     * @description: 对状态码做Hash运算
     * @date: 2024/4/19 11:02
     * @param state 状态码
     * @return
     */
    private  String hashStatecode(String state,String grafanaSecretKey,String ssoClientSecret)  {
        String combinedString = state +  grafanaSecretKey + ssoClientSecret;
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hashBytes = digest.digest(combinedString.getBytes());
            return DatatypeConverter.printHexBinary(hashBytes).toLowerCase();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * @description: 生成grafana验证的状态码
     * @date: 2024/4/19 11:03
     * @return
     */
    private  String generateStateString() {
        SecureRandom secureRandom = new SecureRandom();
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return  Base64.getUrlEncoder().encodeToString(randomBytes);
    }
    /*隐藏无需修改的方法*/
}

2)对org.maxkey.authz.oauth2.provider.code.AuthorizationCodeTokenGranter进行修改

代码语言:txt
复制
public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {

  @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
       
       /*隐藏无需修改的方法*/
        
        Set<String> redirectUris = client.getRegisteredRedirectUri();
        boolean redirectMismatch=false;
        //match the stored RedirectUri with request redirectUri parameter
        for(String storedRedirectUri : redirectUris){
            //解决从https跳转至http域名下,获取access_token失败问题
            if(redirectUri.startsWith(storedRedirectUri) || storedRedirectUri.contains("grafana")){
                redirectMismatch=true;
            }
        }
        
        if ((redirectUri != null || redirectUriApprovalParameter != null)
                && !redirectMismatch) {
            logger.info("storedAuth redirectUri "+pendingOAuth2Request.getRedirectUri());
            logger.info("redirectUri parameter "+ redirectUri);
            logger.info("stored RedirectUri "+ redirectUris);
            throw new RedirectMismatchException("Redirect URI mismatch.");
        }
        
        if (clientId != null && !clientId.equals(pendingClientId)) {
            // just a sanity check.
            throw new InvalidClientException("Client ID mismatch");
        }
        
        /*隐藏无需修改的方法*/
        
        return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);

    }
}

上述代码块只是对getOAuth2Authentication方法进行了修改,主要修改以下代码,排除了对Grafana的重定向接口的校验,防止获取access_token失败的问题(如果是通过ip访问,可以不做此修改)。

代码语言:txt
复制
if(redirectUri.startsWith(storedRedirectUri) || storedRedirectUri.contains("grafana")){
    redirectMismatch=true;
}

3.3、线上部署的几种方案

说明:以下几种方案,都是采用将Grafana的访问地址代理到同一个Ip或域名下,防止Cookie跨域丢失。

3.3.1、通过Ip+端口的访问方式

IP

组件

192.168.1.15

Maxkey服务

192.168.1.15

Nginx

192.168.1.16

Grafana服务

1) 在Maxkey的管理平台将grafana的登录地址和授权地址,全部设置为maxkey部署服务器的IP(192.168.1.15),然后在grafana部署服务器上,将custom.ini的domain设置为192.168.1.15,rool_url后边加上/grafana路径。如下图

3.3.2、通过域名跳转IP的访问方式

IP /域名

组件

sso.maxkey.top

Nginx(域名代理服务器)

192.168.1.15

Maxkey服务

192.168.1.16

Grafana服务

1)在Maxkey管理平台中将grafana的登录地址和授权地址,全部设置为https://sso.maxkey.top/,将customt.ini的domain设置为sso.maxkey.top,root_url 参数后边加上/grafana路径。

2)新增Nginx配置

代码语言:txt
复制
server {
    listen       443 ssl;
    server_name  sso.maxkey.top;

    ssl_certificate      /ssl/maxkey.pem;
    ssl_certificate_key  /ssl/maxkey.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    location /{
            proxy_pass http://192.168.1.15/;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

server {
    listen       3000 ssl;
    server_name  sso.maxkey.top;

    ssl_certificate      /ssl/maxkey.pem;
    ssl_certificate_key  /ssl/maxkey.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    location /grafana {
            proxy_pass http://192.168.1.16:3000/grafana;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $host:3000;
            server_name_in_redirect on;
            proxy_set_header X-Forwarded-Proto $scheme;
    }
}

4、Maxkey接入Grafana过程中遇到的问题及解决方案

4.1、Grafana登录报错login.OAuthLogin(missing saved state)

原因分析:

Grafana通过Oauth方式认证登录时,会校验cookie中是否有oauth_state参数,没有就会报missing saved state错误。

解决方案:

在Maxkey认证完成,即将重定向跳转到Grafana登录接口时,将oauth_state状态码写入到cookie中。

具体操作,请查看3.2.4章节。

代码语言:txt
复制
/**Grafana是go语言写的,以下展示的Grafana部分源代码**/
//生成一个随机的状态码
func GenStateString() (string, error) {
    rnd := make([]byte, 32)
    if _, err := rand.Read(rnd); err != nil {
        oauthLogger.Error("failed to generate state string", "err", err)
        return "", err
    }
    return base64.URLEncoding.EncodeToString(rnd), nil
}
/**
 * @description: 对状态码做Hash运算
 * @param code 传入的状态码
 * @param seed Maxkey颁发给Grafana的秘钥,即client_secret
 * @param SecretKey 是custom.ini文件中配置的secret_key的值
 */
func (hs *HTTPServer) hashStatecode(code, seed string) string {
    hashBytes := sha256.Sum256([]byte(code + hs.Cfg.SecretKey + seed))
    return hex.EncodeToString(hashBytes[:])
}

展示上述代码,是为了在Maxkey项目中,需要用java实现这两个方法,解决状态码缺失问题。

4.2、Grafana登录报错login.OAuthLogin(state mismatch)

原因分析:

重定向URL地址传递的state参数,做哈希运算后,与cookie中存入的oauth_state不相等造成的。

解决方案:

1)先清除一下缓存,排除一下缓存造成的bug(博主就在这踩过坑,花了几个小时排除代码与参数,血泪史。。。)

2)比对Maxkey中生成oauth_state时用到的client_secret和secret_key是否一致

3)检查go方法中GenStateString()和hashStatecode()的这两个方法,是否在转译java代码时,转译正确。

4.3、Grafana登录报错login.OAuthLogin(NewTransportWithCode)

原因分析:

一般造成这个错误的原因是Maxkey管理平台中配置的授权地址和Grafana配置文件中root_url 不一致,导致Grafana在通过code换取access_token时失败造成的。

解决方案:

可以参考章节3.2.4 (2)中的修改方法,有点简单粗暴,博主由于时间原因,把Grafana所有的这类校验都硬编码排除了,可能存在安全性问题,建议大家可以根据实际情况优化

4.4、其他

建议allowed_domains 设置为空,否则登录时会要检查邮箱域名;

Maxkey登录的用户,建议填写一个合规的邮箱(满足邮箱地址规则即可),否则Grafana会报用户邮箱为空之类的错误。

以上两点,博主在实操过程中遇到,不一定必现,大家感兴趣可以验证下

5、参考

https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/

https://maxkey.top/zh/conf/tutorial.html

https://help.aliyun.com/zh/grafana/use-cases/use-oauth-to-log-on-to-grafana

https://blog.csdn.net/qq_43801592/article/details/123062161

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/memory23/article/details/138276050

本文系转载,前往查看

如有侵权,请联系?cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系?cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、接入版本
  • 2、Maxkey接入Grafana的认证流程
  • 3、具体实现步骤
    • 3.1、修改Grafana配置,开启Oauth认证
      • 3.2、Maxkey的相关配置
        • 3.3、线上部署的几种方案
          • 3.3.1、通过Ip+端口的访问方式
          • 4、Maxkey接入Grafana过程中遇到的问题及解决方案
            • 4.1、Grafana登录报错login.OAuthLogin(missing saved state)
              • 4.2、Grafana登录报错login.OAuthLogin(state mismatch)
                • 4.3、Grafana登录报错login.OAuthLogin(NewTransportWithCode)
                  • 4.4、其他
                  • 5、参考
                  相关产品与服务
                  Grafana 服务
                  Grafana 服务(TencentCloud Managed Service for Grafana,TCMG)是腾讯云基于社区广受欢迎的开源可视化项目 Grafana ,并与 Grafana Lab 合作开发的托管服务。TCMG 为您提供安全、免运维 Grafana 的能力,内建腾讯云多种数据源插件,如 Prometheus 监控服务、容器服务、日志服务 、Graphite 和 InfluxDB 等,最终实现数据的统一可视化。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                  http://www.vxiaotou.com