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

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。

#################################### 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配置

#填写Grafana中的secret_key的值 
maxkey.sso.grafana.secretKey = SW2YcwTIb9zpOOhoPsMm
#Maxkey颁发给Grafana的秘钥
maxkey.sso.grafana.ClientSecret = xxxxx

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

在这里插入图片描述
在这里插入图片描述

3.2.3、在Nginx中新增配置

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类进行修改

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进行修改

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访问,可以不做此修改)。

if(redirectUri.startsWith(storedRedirectUri) || storedRedirectUri.contains("grafana")){
	redirectMismatch=true;
}

3.3、线上部署的几种方案

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

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

IP组件
192.168.1.15Maxkey服务
192.168.1.15Nginx
192.168.1.16Grafana服务

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.topNginx(域名代理服务器)
192.168.1.15Maxkey服务
192.168.1.16Grafana服务

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

2)新增Nginx配置

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章节。

/**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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Levi.Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值