前言
由于项目原因,需要启用Keycloak做SSO,但是项目中又使用IDM管制访问权限,我使用的方法如下。
一、实现原理
1.用户登录时,先通过Keycloak的登录页;
2.登录通过后,拿Keycloak token换取IDM的信任,并获得IDM token。
二、使用步骤
1.Enable Keycloak SSO
1.1 Reference keycloak.js library
<script src="keycloak.js"></script>
1.2 Add keycloak.init method
var initOptions = {
// responseMode: 'fragment',
// flow: 'standard'
url: 'https://keycloak-prd.xxx.com/auth',
realm: 'xxx',
clientId: 'xxx'
};
var keycloak = Keycloak(initOptions);
keycloak.init({onLoad: 'login-required'}).success(function (authenticated) {
//alert(authenticated ? 'authenticated' : 'not authenticated');
if (!authenticated) {
alert('not authenticated');
} else {
keycloak.loadUserProfile().success(data => {
console.info(data);
})
}
console.info(keycloak);
}).error(function () {
alert('failed to initialize');
});
function loadProfile() {
keycloak.loadUserProfile().success(function(profile) {
output(profile);
}).error(function() {
output('Failed to load profile');
});
}
1.3 After the above settings, the system will be redirected to your keycloak's login page.
2.Trust keycloak's token in IDM
代码如下(示例):
/// <summary>
/// Login with Keycloak
/// </summary>
[HttpGet]
public async Task<IActionResult> LoginWithKeycloak(string returnUrl, string keyCloakToken)
{
if (string.IsNullOrEmpty(returnUrl))
{
return Redirect("~/");
}
if (string.IsNullOrEmpty(keyCloakToken))
{
returnUrl = returnUrl + (returnUrl.IndexOf('?') > 0 ? "&" : "?") + "errmsg=No Keycloak token";
return Redirect(returnUrl);
}
bool logged = false;
string errmsg = string.Empty;
try
{
var jsonPayload = Base64UrlEncoder.Decode(keyCloakToken.Split('.')[1]);
JObject claims = (JObject)JsonConvert.DeserializeObject(jsonPayload);
string keyCloakUrl = claims["iss"].ToString() + "/account";
//从工厂获取请求对象
var client = _httpClientFactory.CreateClient();
var request = new HttpRequestMessage()
{
Method = HttpMethod.Get,
RequestUri = new Uri(keyCloakUrl),
Content = new StringContent(string.Empty)
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", keyCloakToken);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content.ReadAsStringAsync().Result;
JObject user = (JObject)JsonConvert.DeserializeObject(responseContent);
/*{ "username": "", "firstName": "", "lastName": "", "email": "", "emailVerified": false, "attributes": { "LDAP_ENTRY_DN": ["" ], "modifyTimestamp": [""], "createTimestamp": [""], "LDAP_ID": [""] }}*/
AuthenticationProperties props = null;
// issue authentication cookie with subject ID and username
var isuser = new IdentityServerUser(user["username"].ToString().ToUpper())
{
DisplayName = user["firstName"].ToString()
};
await HttpContext.SignInAsync(isuser, props);
logged = true;
}
}
catch (Exception ex)
{
errmsg = ex.Message;
}
returnUrl = returnUrl + (returnUrl.IndexOf('?') > 0 ? "&" : "?") + ("step=IdmLogin") + (logged ? "" : ("&errmsg=" + errmsg));
return Redirect(returnUrl);
}
3.Get IDM's token
代码如下(示例):
index2.html
<html>
<head>
<script src="keycloak.js"></script>
<script src="oidc-client.min.js"></script>
</head>
<body>
<div>
<button onclick="keycloak.login()">Login</button>
<button onclick="keycloak.login({ action: 'UPDATE_PASSWORD' })">Update Password</button>
<button onclick="keycloak.logout()">Logout</button>
<button onclick="keycloak.register()">Register</button>
<button onclick="keycloak.accountManagement()">Account</button>
<button onclick="refreshToken(9999)">Refresh Token</button>
<button onclick="refreshToken(30)">Refresh Token (if <30s validity)</button>
<button onclick="loadProfile()">Get Profile</button>
<button onclick="updateProfile()">Update profile</button>
<button onclick="loadUserInfo()">Get User Info</button>
<button onclick="output(keycloak.tokenParsed)">Show Token</button>
<button onclick="output(keycloak.refreshTokenParsed)">Show Refresh Token</button>
<button onclick="output(keycloak.idTokenParsed)">Show ID Token</button>
<button onclick="showExpires()">Show Expires</button>
<button onclick="output(keycloak)">Show Details</button>
<button onclick="output(keycloak.createLoginUrl())">Show Login URL</button>
<button onclick="output(keycloak.createLogoutUrl())">Show Logout URL</button>
<button onclick="output(keycloak.createRegisterUrl())">Show Register URL</button>
<button onclick="output(keycloak.createAccountUrl())">Show Account URL</button>
</div>
<h5>Result of Keycloak</h5>
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px; word-wrap: break-word; height:100px; overflow: scroll;" id="output"></pre>
<h5>Events of Keycloak</h5>
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px; word-wrap: break-word; height:50px; overflow: scroll;" id="events"></pre>
<div>
<button onclick="renewIdmToken()">Refresh Token</button>
</div>
<h5>Result of IDM</h5>
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px; word-wrap: break-word; height:100px; overflow: scroll;" id="outputidm"></pre>
<h5>Events of IDM</h5>
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px; word-wrap: break-word; height:50px; overflow: scroll;" id="eventsidm"></pre>
<script>
</script>
<script src="idm-config.js"></script>
<script src="keycloak-config.js"></script>
</body>
</html>
idm-config.js
let initIdmOptions = {
authority: "https://xx.xxx.com/auth",
client_id: "peoplex",
redirect_uri: "https://xx.xxx.com/keycloak/idm-callback.html",
response_type: "id_token token",
scope: "openid profile api1",
post_logout_redirect_uri: "https://xx.xxx.com/keycloak/index2.html",
// silent renew will get a new access_token via an iframe
// just prior to the old access_token expiring (60 seconds prior)
silent_redirect_uri: "https://xx.xxx.com/keycloak/idm-silent.html",
automaticSilentRenew: true,
}
var oidcMgr = new Oidc.UserManager(initIdmOptions);
function loginWithIdentityServer4(token)
{
var url = initIdmOptions.authority + '/Account/LoginWithKeycloak?returnUrl={0}&keyCloakToken={1}';
url = url.replace('{0}',window.location.href).replace('{1}',token);
window.location.replace(url);
}
function idmLogin() {
oidcMgr.signinRedirect();
}
function idmLogout() {
oidcMgr.signoutRedirect();
}
function idmRedirectCallback()
{
new Oidc.UserManager().signinRedirectCallback().then(function (user) {
console.log(user);
window.history.replaceState({},
window.document.title,
window.location.origin + window.location.pathname);
window.location = "index2.html?step=Completed";
});
}
function IdmSilentCallback()
{
oidcMgr.signinSilentCallback();
}
function getIdmToken() {
oidcMgr.getUser().then(function (user) {
if (user) {
console.log(user);
outputIdm(user);
} else {
console.log("Not logged in");
outputIdm("Not logged in");
}
});
}
function renewIdmToken() {
oidcMgr.signinSilent()
.then(function () {
console.log("silent renew success");
}).catch(function (err) {
console.log("silent renew error", err);
});
}
function outputIdm(data) {
if (typeof data === 'object') {
data = JSON.stringify(data, null, ' ');
}
document.getElementById('outputidm').innerHTML = data;
}
function eventIdm(event) {
var e = document.getElementById('eventsidm').innerHTML;
document.getElementById('eventsidm').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
}
oidcMgr.events.addUserLoaded(function (user) {
//console.log("User loaded");
eventIdm("User loaded");
getIdmToken();
});
oidcMgr.events.addUserUnloaded(function () {
//console.log("User logged out locally");
eventIdm("User logged out locally");
getIdmToken();
});
oidcMgr.events.addAccessTokenExpiring(function () {
//console.log("Access token expiring..." + new Date());
eventIdm("Access token expiring...");
});
oidcMgr.events.addSilentRenewError(function (err) {
//console.log("Silent renew error: " + err.message);
eventIdm("Silent renew error: " + err.message);
});
oidcMgr.events.addUserSignedOut(function () {
//console.log("User signed out of OP");
eventIdm("User signed out of OP");
oidcMgr.removeUser();
});
idm-callback.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<script type="text/javascript" src="oidc-client.min.js"></script>
</head>
<body>
Loading...
<script src="idm-config.js"></script>
<script>
idmRedirectCallback();
</script>
</body>
</html>
idm-silent.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<script type="text/javascript" src="oidc-client.min.js"></script>
</head>
<body>
Loading...
<script src="idm-config.js"></script>
<script>
IdmSilentCallback();
</script>
</body>
</html>
keycloak-config.js
let initOptions = {
url: 'https://xx.xxx.com/auth',
realm: 'k8sprdwzsi40',
clientId: 'wigps',
onLoad: 'login-required'
}
let keycloak = Keycloak(initOptions);
keycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {
if (!auth) {
alert('not authenticated');
window.location.reload();
return;
}
let errmsg = getQueryString('errmsg');
if(errmsg){
alert(getQueryString('errmsg'));
return;
}
let step = getQueryString('step');
if(!step){
loginWithIdentityServer4(keycloak.token);
return;
}
else{
switch (step){
case 'IdmLogin':
idmLogin();
break;
case 'Completed':
getIdmToken();
loadProfile();
break;
default:
break;
}
}
//Token Refresh
setInterval(() => {
refreshToken(70);
}, 1000*60)
}).catch(() => {
alert("Authenticated Failed, ailed to initialize");
});
function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}
function loadProfile() {
keycloak.loadUserProfile().then(function(profile) {
output(profile);
console.log(keycloak);
}).catch(function() {
output('Failed to load profile');
});
}
function updateProfile() {
var url = keycloak.createAccountUrl().split('?')[0];
var req = new XMLHttpRequest();
req.open('POST', url, true);
req.setRequestHeader('Accept', 'application/json');
req.setRequestHeader('Content-Type', 'application/json');
req.setRequestHeader('Authorization', 'bearer ' + keycloak.token);
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
output('Success');
} else {
output('Failed');
}
}
}
req.send('{"email":"myemail@foo.bar","firstName":"test","lastName":"bar"}');
}
function loadUserInfo() {
keycloak.loadUserInfo().then(function(userInfo) {
output(userInfo);
}).catch(function() {
output('Failed to load user info');
});
}
function refreshToken(minValidity) {
keycloak.updateToken(minValidity).then(function(refreshed) {
if (refreshed) {
output(keycloak.tokenParsed);
renewIdmToken();
} else {
output('Token not refreshed, valid for ' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
}
}).catch(function() {
output('Failed to refresh token');
});
}
function showExpires() {
if (!keycloak.tokenParsed) {
output("Not authenticated");
return;
}
var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
if (keycloak.refreshTokenParsed) {
o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
}
output(o);
}
function output(data) {
if (typeof data === 'object') {
data = JSON.stringify(data, null, ' ');
}
document.getElementById('output').innerHTML = data;
}
function event(event) {
var e = document.getElementById('events').innerHTML;
document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
}
keycloak.onAuthSuccess = function () {
event('Auth Success');
};
keycloak.onAuthError = function (errorData) {
event("Auth Error: " + JSON.stringify(errorData) );
};
keycloak.onAuthRefreshSuccess = function () {
event('Auth Refresh Success');
};
keycloak.onAuthRefreshError = function () {
event('Auth Refresh Error');
};
keycloak.onAuthLogout = function () {
event('Auth Logout');
};
keycloak.onTokenExpired = function () {
event('Access token expired.');
};
keycloak.onActionUpdate = function (status) {
switch (status) {
case 'success':
event('Action completed successfully'); break;
case 'cancelled':
event('Action cancelled by user'); break;
case 'error':
event('Action failed'); break;
}
};
Please see the getIdmToken() method in the idm-config.js for IDM's token。
总结
以上就是今天要讲的内容,本文仅仅简单介绍了IDM和keycloak的使用,而IDM和keycloak提供了大量函数和方法。