Keycloak自定义实现第三方登录
第三方Oauth登录
- 由于对接的第三方IDP不一定都是标准的openid connect实现,所以都需要根据第三方的Oauth文档进行定制;
Keycloak对于新增Social IDP的实现,都是标准,以及灵活的;
我们完全可以参照 Keycloak 本身已实现的Github LinkedIn等,快速实现我们的需求;
我们这里以酷家乐的Oauth2 接口进行说明
酷家乐 Oauth2接口分析
- 请求code的参数及返回都是标准的,这里无需进行修改
- 换取token接口,由于返回参数进行data的wrap,以及字段名为驼峰的,所以需要对这部分进行修改定制
- 获取用户信息
由于获取用户信息,还需要额外请求用户的OpenId,所以需要对该部分进行修改
定制Provider
提供工程类: KujialeIdentityProviderFactory
工厂类完全可以从GitHubIdentityProviderFactory
拷贝过来,修改成自己 PROVIDER_ID, NAME 以及create自己的Provider
public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFactory<GitHubIdentityProvider> implements SocialIdentityProviderFactory<GitHubIdentityProvider> {
public static final String PROVIDER_ID = "github";
@Override
public String getName() {
return "GitHub";
}
@Override
public GitHubIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new GitHubIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
}
@Override
public OAuth2IdentityProviderConfig createConfig() {
return new OAuth2IdentityProviderConfig();
}
@Override
public String getId() {
return PROVIDER_ID;
}
}
解析accessToken 以及获取用户信息
解析用户信息
KujialeIdentityProvider 完整代码
public class KujialeIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {
public static final String AUTH_URL = "https://oauth.kujiale.com/oauth2/show";
public static final String TOKEN_URL = "https://oauth.kujiale.com/oauth2/auth/token";
public static final String OPENID_URL = "https://oauth.kujiale.com/oauth2/auth/user";
public static final String PROFILE_URL = "https://oauth.kujiale.com/oauth2/openapi/user";
public static final String DEFAULT_SCOPE = "get_user_info";
public static final String OAUTH2_PARAMETER_ACCESS_TOKEN = "accessToken";
public KujialeIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL);
}
@Override
protected boolean supportsExternalExchange() {
return true;
}
@Override
protected String getProfileEndpointForValidation(EventBuilder event) {
return PROFILE_URL;
}
@Override
protected String getDefaultScopes() {
return DEFAULT_SCOPE;
}
/**
* 提取酷家乐用户信息,转换为keycloak用户实体
*
* @param event
* @param profile
* @return
*/
@Override
protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
if (!profile.has("d") || profile.get("d").isEmpty()) {
throw new NullPointerException("kujiale idp user info response is null " + profile.toString());
}
JsonNode userNode = profile.get("d");
String openId = getJsonProperty(userNode, "openId");
if (openId == null || openId.isEmpty()) {
throw new NullPointerException("kujiale idp user info is null " + userNode.asText());
}
BrokeredIdentityContext user = new BrokeredIdentityContext(openId);
String userName = userNode.get("userName").asText();
user.setUsername(userName);
user.setFirstName(userName);
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, userNode, getConfig().getAlias());
return user;
}
/**
* 构建获取酷家乐用户信息的请求 暂不支持
*
* @param subjectToken
* @param userInfoUrl
* @return
*/
@Override
protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
return SimpleHttp.doGet(userInfoUrl, session)
.header("Authorization", "Bearer " + subjectToken);
}
/**
* 获取酷家乐用户信息
*
* @param response
* @return
*/
@Override
public BrokeredIdentityContext getFederatedIdentity(String response) {
String accessToken;
try {
JsonNode resNode = asJsonNode(response);
accessToken = extractTokenFromResponse(resNode.get("d").toString(), OAUTH2_PARAMETER_ACCESS_TOKEN);
} catch (Exception ex) {
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
}
if (accessToken == null) {
throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
}
BrokeredIdentityContext context = doGetFederatedIdentity(accessToken);
context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
return context;
}
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
BrokeredIdentityContext context = null;
try {
JsonNode openidResponse = generateOpenIdRequest(accessToken).asJson();
String openId = openidResponse.get("d").asText();
if (openId == null) {
throw new Exception("Can not get openId from kujiale IDP");
}
JsonNode profile = generateUserInfoRequest(accessToken, openId).asJson();
context = extractIdentityFromProfile(null, profile);
} catch (Exception ex) {
logger.warn("Cannot GetFederatedIdentity from kujiale IDP, error " + ex.getMessage());
}
return context;
}
public SimpleHttp generateOpenIdRequest(String accessToken) {
SimpleHttp openIdRequest = SimpleHttp.doGet(OPENID_URL, session)
.param("access_token", accessToken);
return openIdRequest;
}
public SimpleHttp generateUserInfoRequest(String accessToken, String openId) {
SimpleHttp openIdRequest = SimpleHttp.doGet(PROFILE_URL, session)
.param("open_id", openId)
.param("access_token", accessToken);
return openIdRequest;
}
}
定制属性映射: IdentityProviderMapper
其实属性映射,本质上只是新增个说明,我们的Provider支持 Import Attribute,
所以代码都是模板代码
public class KujialeUserAttributeMapper extends AbstractJsonUserAttributeMapper {
private static final String[] cp = new String[]{KujialeIdentityProviderFactory.PROVIDER_ID};
@Override
public String[] getCompatibleProviders() {
return cp;
}
@Override
public String getId() {
return "kujiale-user-attribute-mapper";
}
}
添加META-INF
添加
org.keycloak.broker.social.SocialIdentityProviderFactory
内容为:自己的ProviderFactory全类名
添加
org.keycloak.broker.provider.IdentityProviderMapper
内容为 自己Mapper的全类名
NoClassDefFoundError报错
相关问题链接
https://stackoverflow.com/questions/57778240/noclassdeffounderror-in-a-provider-jar-when-using-a-class-from-org-keycloak-auth
解决方法
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Dependencies>org.keycloak.keycloak-services</Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
部署Provider及配置使用
部署
拷贝到 /standalone/deployments中,重启服务
重启服务后,到 Admin ==> server info 确认新增的Provider生效
配置
新增IDP![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/37e2dbca979aee2778b78079871e05c3.png)
设置属性mapper
可以设置默认使用的IDP
第一次登录,会自动创建用户已经,导入对应的属性信息
属性:
关联的IDP信息