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