Skip to content

Commit

Permalink
feat(UNI-160): support open platform login
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff-Tian committed Aug 26, 2023
1 parent c64d3a2 commit 5a8ed6b
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 34 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,21 @@ docker pull jefftian/keycloak-heroku:latest
## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=Jeff-Tian/keycloak-services-social-weixin&type=Date)](https://star-history.com/#Jeff-Tian/keycloak-services-social-weixin&Date)

## 原理

### 开放平台微信登录

#### 先构建授权链接

链接如下:

```
https://open.weixin.qq.com/connect/qrconnect?scope=snsapi_login&state=d3Yvfou3pdgp-UNVZ-i7DTDEbv4rZTWx6Wh7lmxzyvk.98VO-haMdj4.c0L0bnybTEatKpqInU02nQ&response_type=code&appid=wxc09e145146844e43&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Frealms%2Fmaster%2Fbroker%2Fweixin%2Fendpoint
```

用户使用微信扫描以上链接中展示的二维码后,会跳转到微信的授权页面,用户点击同意后,会跳转到我们的回调地址,并且带上 code 和 state 参数,如下:

```
https://keycloak.jiwai.win/realms/master/broker/weixin/endpoint?code=011er8000zwPzQ1Fvw200DTBCP1er80K&state=d3Yvfou3pdgp-UNVZ-i7DTDEbv4rZTWx6Wh7lmxzyvk.98VO-haMdj4.c0L0bnybTEatKpqInU02nQ
```
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services-social-weixin</artifactId>
<version>0.3.9</version>
<version>0.4.0</version>
<name>Keycloak Services Social WeiXin</name>
<description/>
<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,43 +51,46 @@
public class WeiXinIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig>
implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {

public static final String AUTH_URL = "https://open.weixin.qq.com/connect/qrconnect";
public static final String TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
public static final String DEFAULT_SCOPE = "snsapi_login";

public static final String WECHAT_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
public static final String WECHAT_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
public static final String WECHAT_DEFAULT_SCOPE = "snsapi_userinfo";
public static final String CUSTOMIZED_LOGIN_URL_FOR_PC = "customizedLoginUrl";

public static final String PROFILE_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

public static final String OPEN_AUTH_URL = "https://open.weixin.qq.com/connect/qrconnect";
public static final String OPEN_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
public static final String OPEN_DEFAULT_SCOPE = "snsapi_login";
public static final String OAUTH2_PARAMETER_CLIENT_ID = "appid";
public static final String OAUTH2_PARAMETER_CLIENT_SECRET = "secret";

public static final String OPEN_CLIENT_ID = "openClientId";
public static final String OPEN_CLIENT_SECRET = "openClientSecret";
public static final String OPEN_CLIENT_ENABLED = "openClientEnabled";

public static final String WECHAT_MOBILE_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
public static final String WECHAT_MP_TOKEN_URL = OPEN_TOKEN_URL;
public static final String WECHAT_MP_DEFAULT_SCOPE = "snsapi_userinfo";
public static final String CUSTOMIZED_LOGIN_URL_FOR_PC = "customizedLoginUrl";
public static final String WECHAT_MP_APP_ID = "clientId2";
public static final String WECHAT_MP_APP_SECRET = "clientSecret2";

public static final String PROFILE_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

public static final String WMP_APP_ID = "wmpClientId";
public static final String WMP_APP_SECRET = "wmpClientSecret";
public static final String WMP_AUTH_URL = "https://api.weixin.qq.com/sns/jscode2session";

public static final String OPENID = "openid";
public static final String WECHATFLAG = "micromessenger";

public final WeixinIdentityCustomAuth customAuth;

public WeiXinIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL);
config.setAuthorizationUrl(OPEN_AUTH_URL);
config.setTokenUrl(OPEN_TOKEN_URL);

customAuth = new WeixinIdentityCustomAuth(session, config, this);
}

public WeiXinIdentityProvider(KeycloakSession session, WeixinIdentityProviderConfig config) {
super(session, config);
config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL);
config.setAuthorizationUrl(OPEN_AUTH_URL);
config.setTokenUrl(OPEN_TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL);

customAuth = new WeixinIdentityCustomAuth(session, config, this);
Expand Down Expand Up @@ -177,7 +180,7 @@ public Response performLogin(AuthenticationRequest request) {

@Override
protected String getDefaultScopes() {
return DEFAULT_SCOPE;
return OPEN_DEFAULT_SCOPE;
}

/**
Expand All @@ -201,39 +204,51 @@ protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {

if (isWechatBrowser(ua)) {// 是微信浏览器
logger.info("----------wechat");
uriBuilder = UriBuilder.fromUri(WECHAT_AUTH_URL);
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, WECHAT_DEFAULT_SCOPE)
uriBuilder = UriBuilder.fromUri(WECHAT_MOBILE_AUTH_URL);
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, WECHAT_MP_DEFAULT_SCOPE)
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
} else {
var config = getConfig();
if (config instanceof WeixinIdentityProviderConfig) {
if (config.getConfig().get(OPEN_CLIENT_ENABLED) != null && config.getConfig().get(OPEN_CLIENT_ENABLED).equals("true")) {
logger.info("----------open client enabled");
uriBuilder = UriBuilder.fromUri(OPEN_AUTH_URL);
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, OPEN_DEFAULT_SCOPE)
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, config.getConfig().get(OPEN_CLIENT_ID))
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());

return uriBuilder;
}

var customizedLoginUrlForPc = ((WeixinIdentityProviderConfig) config).getCustomizedLoginUrlForPc();

if (customizedLoginUrlForPc != null && !customizedLoginUrlForPc.isEmpty()) {
uriBuilder = UriBuilder.fromUri(customizedLoginUrlForPc);

uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, WECHAT_DEFAULT_SCOPE)
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, WECHAT_MP_DEFAULT_SCOPE)
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getConfig().get(WECHAT_MP_APP_ID))
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, config.getConfig().get(WECHAT_MP_APP_ID))
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());

return uriBuilder;
} else {
uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl());
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
uriBuilder = UriBuilder.fromUri(config.getAuthorizationUrl());
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, config.getDefaultScope())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, config.getClientId())
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
}
} else {
uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl());
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
uriBuilder = UriBuilder.fromUri(config.getAuthorizationUrl());
uriBuilder.queryParam(OAUTH2_PARAMETER_SCOPE, config.getDefaultScope())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, config.getClientId())
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
}
}
Expand Down Expand Up @@ -290,7 +305,7 @@ public Endpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder
@GET
public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_STATE) String state,
@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_PARAMETER_CODE) String authorizationCode, @QueryParam(OAuth2Constants.ERROR) String error, @QueryParam(OAuth2Constants.SCOPE_OPENID) String openid, @QueryParam("clientId") String clientId, @QueryParam("tabId") String tabId) {
logger.info("OAUTH2_PARAMETER_CODE = " + authorizationCode);
logger.info(String.format("OAUTH2_PARAMETER_CODE = %s, %s, %s, %s, %s", authorizationCode, error, openid, clientId, tabId));
var wechatLoginType = WechatLoginType.FROM_PC_QR_CODE_SCANNING;

if (headers != null && isWechatBrowser(headers.getHeaderString("user-agent").toLowerCase())) {
Expand Down Expand Up @@ -369,8 +384,8 @@ public void setFederatedIdentity(@QueryParam(AbstractOAuth2IdentityProvider.OAUT
public SimpleHttp[] generateTokenRequest(String authorizationCode, WechatLoginType wechatLoginType) {
logger.info(String.format("generateTokenRequest, code = %s, loginType = %s", authorizationCode, wechatLoginType));
if (WechatLoginType.FROM_WECHAT_BROWSER.equals(wechatLoginType)) {
logger.info(String.format("from wechat browser, posting to %s", WECHAT_TOKEN_URL));
return new SimpleHttp[]{SimpleHttp.doPost(WECHAT_TOKEN_URL, session)
logger.info(String.format("from wechat browser, posting to %s", WECHAT_MP_TOKEN_URL));
return new SimpleHttp[]{SimpleHttp.doPost(WECHAT_MP_TOKEN_URL, session)
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getConfig().get(WECHAT_MP_APP_ID))
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getConfig().get(WECHAT_MP_APP_SECRET))
Expand All @@ -389,6 +404,16 @@ public SimpleHttp[] generateTokenRequest(String authorizationCode, WechatLoginTy
return new SimpleHttp[]{wechatSession, tokenRes};
}

var isOpenClientEnabled = getConfig().getConfig().get(OPEN_CLIENT_ENABLED);

if (isOpenClientEnabled.equals("true")) {
return new SimpleHttp[]{SimpleHttp.doPost(getConfig().getTokenUrl(), session).param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getConfig().get(OPEN_CLIENT_ID))
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getConfig().get(OPEN_CLIENT_SECRET))
.param(OAUTH2_PARAMETER_REDIRECT_URI, getConfig().getConfig().get(OAUTH2_PARAMETER_REDIRECT_URI))
.param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE), null};
}

return new SimpleHttp[]{SimpleHttp.doPost(getConfig().getTokenUrl(), session).param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ public String getId() {
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name(WeiXinIdentityProvider.WECHAT_MP_APP_ID)
.label("公众号 App Id")
.helpText("当用户使用 PC 进行关注微信公众号即登录时,要使用的 app Id,即微信公众号(不是开放平台)的 appid")
.label("PC 用的公众号 App Id")
.helpText("当用户使用 PC 进行关注微信公众号即登录时,要使用的 app Id,即微信公众号(不是开放平台)的 appid。可以和上面的 Client ID 一样,也可以不一样")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(WeiXinIdentityProvider.WECHAT_MP_APP_SECRET)
.label("公众号 App Secret")
.helpText("当用户使用 PC 进行关注微信公众号即登录时,要使用的 app Secret,即微信公众号(不是开放平台)的 app secret")
.label("PC 用的公众号 App Secret")
.helpText("当用户使用 PC 进行关注微信公众号即登录时,要使用的 app Secret,即微信公众号(不是开放平台)的 app secret。可以和上面的 Client Secret 一样,也可以不一样")
.type(ProviderConfigProperty.STRING_TYPE)
.add()

Expand All @@ -56,6 +56,22 @@ public List<ProviderConfigProperty> getConfigProperties() {
.type(ProviderConfigProperty.STRING_TYPE)
.add()

.property().name(WeiXinIdentityProvider.OPEN_CLIENT_ID)
.label("开放平台 Client ID")
.helpText("当用户使用微信开放平台登录时,要使用的 Client ID,即微信开放平台的 appid")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(WeiXinIdentityProvider.OPEN_CLIENT_SECRET)
.label("开放平台 Client Secret")
.helpText("当用户使用微信开放平台登录时,要使用的 Client Secret,即微信开放平台的 app secret")
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(WeiXinIdentityProvider.OPEN_CLIENT_ENABLED)
.label("是否启用开放平台登录")
.helpText("是否启用开放平台登录,默认不启用,即使用关注公众号的方式登录。如果启用,则使用开放平台的方式登录")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.add()

.property().name(WeiXinIdentityProvider.WMP_APP_ID)
.label("小程序 appId")
.helpText("小程序的 appid")
Expand Down

0 comments on commit 5a8ed6b

Please sign in to comment.