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/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.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配置
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://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