From d29444435227527a584225f290b22d62cfe91194 Mon Sep 17 00:00:00 2001 From: shimingxy Date: Wed, 5 Feb 2020 19:42:35 +0800 Subject: [PATCH] change Social Sign On use JustAuth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit change Social Sign On use JustAuth, dingtalk sinaweibo is tested --- build.gradle | 5 + .../AbstractSocialSignOnEndpoint.java | 122 ++++-------------- .../socialsignon/OAuthServiceBuilder.java | 91 ------------- .../socialsignon/SocialSignOnEndpoint.java | 16 +-- .../service/SocialSignOnProvider.java | 92 +++---------- .../service/SocialSignOnProviderService.java | 63 ++++++++- maxkey-client-sdk/.classpath | 1 - .../client/oauth/builder/api/OAuthApi20.java | 22 +++- maxkey-core/.classpath | 1 - .../main/java/org/maxkey/web/WebContext.java | 17 ++- maxkey-dao/.classpath | 2 - .../maxkey-protocol-cas/.classpath | 1 - maxkey-web-manage/.classpath | 6 +- maxkey-web-maxkey/.classpath | 7 +- .../maxkey/web/endpoint/IndexEndpoint.java | 4 +- .../config/applicationLogin.properties | 61 +++++++++ .../config/applicationSocialSignOn.properties | 83 ------------ .../spring/maxkey-support-social.xml | 47 ++----- .../src/main/resources/spring/maxkey.xml | 1 - .../static/images/social/dingtalk.png | Bin 0 -> 23805 bytes 20 files changed, 233 insertions(+), 409 deletions(-) delete mode 100644 maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/OAuthServiceBuilder.java delete mode 100644 maxkey-web-maxkey/src/main/resources/config/applicationSocialSignOn.properties create mode 100644 maxkey-web-maxkey/src/main/resources/static/images/social/dingtalk.png diff --git a/build.gradle b/build.gradle index 516cd5fe7..89035d329 100644 --- a/build.gradle +++ b/build.gradle @@ -202,6 +202,10 @@ subprojects { compile group: 'org.opensaml', name: 'openws', version: '1.5.4' compile group: 'org.opensaml', name: 'xmltooling', version: '1.4.4' + compile group: 'cn.hutool', name: 'hutool-core', version: '5.1.2' + compile group: 'cn.hutool', name: 'hutool-http', version: '5.1.2' + implementation 'me.zhyd.oauth:JustAuth:1.13.2' + compile group: 'org.javassist', name: 'javassist', version: '3.23.0-GA' compile group: 'org.owasp.esapi', name: 'esapi', version: '2.2.0.0' compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.2' @@ -232,6 +236,7 @@ subprojects { compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: "${jacksonVersion}" compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: "${jacksonVersion}" compile group: 'com.fasterxml', name: 'classmate', version: '1.5.0' + compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62' compile group: 'org.reactivestreams', name: 'reactive-streams', version: '1.0.2' compile group: 'io.projectreactor', name: 'reactor-core', version: '3.2.10.RELEASE' diff --git a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/AbstractSocialSignOnEndpoint.java b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/AbstractSocialSignOnEndpoint.java index 1034b23b3..51ec3b951 100644 --- a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/AbstractSocialSignOnEndpoint.java +++ b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/AbstractSocialSignOnEndpoint.java @@ -3,25 +3,18 @@ */ package org.maxkey.authn.support.socialsignon; -import java.util.HashMap; -import java.util.Map; - import org.maxkey.authn.support.socialsignon.service.SocialSignOnProvider; import org.maxkey.authn.support.socialsignon.service.SocialSignOnProviderService; import org.maxkey.authn.support.socialsignon.service.SocialSignOnUserTokenService; -import org.maxkey.client.http.HttpVerb; -import org.maxkey.client.http.Response; -import org.maxkey.client.oauth.model.OAuthRequest; -import org.maxkey.client.oauth.model.Token; -import org.maxkey.client.oauth.model.Verifier; -import org.maxkey.client.oauth.oauth.OAuthService; -import org.maxkey.util.JsonUtils; -import org.maxkey.util.StringUtils; import org.maxkey.web.WebContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.request.AuthRequest; + /** * @author Crystal.Sea * @@ -45,11 +38,10 @@ public class AbstractSocialSignOnEndpoint { public final static String SOCIALSIGNON_TYPE_BIND="socialsignon_type_bind"; } - protected Token accessToken; protected SocialSignOnProvider socialSignOnProvider; - protected OAuthService oauthService; + protected AuthRequest authRequest; protected String accountJsonString; @@ -65,103 +57,41 @@ public class AbstractSocialSignOnEndpoint { - protected OAuthService buildOAuthService(String provider){ + protected AuthRequest buildAuthRequest(String provider){ SocialSignOnProvider socialSignOnProvider = socialSignOnProviderService.get(provider); _logger.debug("socialSignOn Provider : "+socialSignOnProvider); if(socialSignOnProvider!=null){ - OAuthServiceBuilder oAuthServiceBuilder=new OAuthServiceBuilder(socialSignOnProvider); - oauthService=oAuthServiceBuilder.builderOAuthService(); - WebContext.setAttribute(SOCIALSIGNON_OAUTH_SERVICE_SESSION, socialSignOnProvider); - WebContext.setAttribute(SOCIALSIGNON_PROVIDER_SESSION, oauthService); - return oauthService; + authRequest=socialSignOnProviderService.getAuthRequest(provider); + WebContext.setAttribute(SOCIALSIGNON_OAUTH_SERVICE_SESSION, authRequest); + WebContext.setAttribute(SOCIALSIGNON_PROVIDER_SESSION, socialSignOnProvider); + return authRequest; } return null; } - - /** - * get accessToken - * @param service - * @return - */ - protected Token getAccessToken() { - - socialSignOnProvider=(SocialSignOnProvider)WebContext.getAttribute(SOCIALSIGNON_OAUTH_SERVICE_SESSION); - oauthService=(OAuthService)WebContext.getAttribute(SOCIALSIGNON_PROVIDER_SESSION); - String oauthVerifier = WebContext.getRequest().getParameter(socialSignOnProvider.getVerifierCode()); + + protected String authCallback() { + authRequest=(AuthRequest)WebContext.getAttribute(SOCIALSIGNON_OAUTH_SERVICE_SESSION); + socialSignOnProvider=(SocialSignOnProvider)WebContext.getAttribute(SOCIALSIGNON_PROVIDER_SESSION); WebContext.removeAttribute(SOCIALSIGNON_OAUTH_SERVICE_SESSION); WebContext.removeAttribute(SOCIALSIGNON_PROVIDER_SESSION); - if(StringUtils.isNullOrBlank(socialSignOnProvider.getVerifierCode())) - return null; - // getting access token - Verifier verifier = new Verifier(oauthVerifier); - this.accessToken=oauthService.getAccessToken(null, verifier); - - return accessToken; - } - - protected String requestAccountJson() { - OAuthRequest oauthRequest = new OAuthRequest(HttpVerb.GET, this.convertAccountUrl(socialSignOnProvider.getAccountUrl(),socialSignOnProvider.getProvider(), accessToken)); - oauthService.signRequest(accessToken, oauthRequest); - Response oauthResponse = oauthRequest.send(); - accountJsonString=oauthResponse.getBody(); - _logger.debug("requestAccountJson : "+accountJsonString); - return accountJsonString; - } - - - - @SuppressWarnings("unchecked") - protected String getAccountId() { - //if(StringUtils.isNullOrBlank(accountJsonString)) { - requestAccountJson(); - //} - - if(this.provider.equals("qq")){ - accountJsonString=accountJsonString.substring(accountJsonString.indexOf("{"), accountJsonString.indexOf("}")+1); - } - Map map = new HashMap(); - - map=(HashMap)JsonUtils.json2Object(accountJsonString, map); - if(this.provider.equals("qqweibo")){ - if(accessToken.getResponseObject().get(socialSignOnProvider.getAccountId())!=null){ - accountId=accessToken.getResponseObject().get(socialSignOnProvider.getAccountId()).toString(); - } - }else if(this.provider.equals("qq")){ - accountId=map.get(socialSignOnProvider.getAccountId()).toString(); - - }else{ - if(map.get(socialSignOnProvider.getAccountId())!=null){ - accountId=map.get(socialSignOnProvider.getAccountId()).toString(); - } - } - + + AuthCallback authCallback=new AuthCallback(); + authCallback.setCode(WebContext.getRequest().getParameter("code")); + authCallback.setAuth_code(WebContext.getRequest().getParameter("auth_code")); + authCallback.setOauthToken(WebContext.getRequest().getParameter("oauthToken")); + authCallback.setAuthorization_code(WebContext.getRequest().getParameter("authorization_code")); + authCallback.setOauthVerifier(WebContext.getRequest().getParameter("oauthVerifier")); + authCallback.setState(WebContext.getRequest().getParameter("state")); + + AuthResponse authResponse=authRequest.login(authCallback); + _logger.debug("Response : "+authResponse); + accountId=socialSignOnProviderService.getAccountId(socialSignOnProvider.getProvider(), authResponse); _logger.debug("getAccountId : "+accountId); return accountId; } - private String convertAccountUrl(String accountUrl,String provider,Token accessToken) { - if("sinaweibo".equals(provider)) { - if(null!=accessToken.getResponseObject()) { - Object uid = accessToken.getResponseObject().get("uid"); - accountUrl = this.convertUrl(accountUrl, "uid", uid == null ? "" : uid.toString()); - } - } - return accountUrl; - } - - private String convertUrl(String url,String paramName,String paramVal) { - StringBuilder sb = new StringBuilder(url); - if (url.indexOf('?') < 0) { - sb.append('?'); - } - else { - sb.append('&'); - } - sb.append(paramName+"=").append(paramVal); - return sb.toString(); - } } diff --git a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/OAuthServiceBuilder.java b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/OAuthServiceBuilder.java deleted file mode 100644 index 28f1ed431..000000000 --- a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/OAuthServiceBuilder.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.maxkey.authn.support.socialsignon; - -import org.maxkey.authn.support.socialsignon.service.SocialSignOnProvider; -import org.maxkey.client.http.SignatureType; -import org.maxkey.client.oauth.builder.ServiceBuilder; -import org.maxkey.client.oauth.builder.api.Api; -import org.maxkey.client.oauth.builder.api.OAuthApi20; -import org.maxkey.client.oauth.oauth.OAuthService; -import org.maxkey.web.WebContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OAuthServiceBuilder { - private static Logger _logger = LoggerFactory.getLogger(OAuthServiceBuilder.class); - - private SocialSignOnProvider socialSignOnProvider; - - private Api api; - - - /** - * - */ - public OAuthServiceBuilder() { - - } - - - /** - * @param socialSignOnProvider - */ - public OAuthServiceBuilder(SocialSignOnProvider socialSignOnProvider) { - - this.socialSignOnProvider = socialSignOnProvider; - String callbackUrl=WebContext.getHttpContextPath()+ "/logon/oauth20/callback/"+socialSignOnProvider.getProvider(); - - socialSignOnProvider.setCallBack(callbackUrl); - - api = new OAuthApi20(socialSignOnProvider.getAuthorizeUrl(), - socialSignOnProvider.getAccessTokenUrl(), - socialSignOnProvider.getAccessTokenMethod()); - - _logger.debug("api : "+api); - } - - - public OAuthService builderOAuthService() { - - if(socialSignOnProvider.getScope()==null||socialSignOnProvider.getScope().equals("")){ - return new ServiceBuilder().provider(api) - .apiKey(socialSignOnProvider.getClientId()) - .apiSecret(socialSignOnProvider.getClientSecret()) - .callback(socialSignOnProvider.getCallBack()) - .signatureType(SignatureType.QueryString) - .debug() - .build(); - }else{ - return new ServiceBuilder().provider(api) - .apiKey(socialSignOnProvider.getClientId()) - .apiSecret(socialSignOnProvider.getClientSecret()) - .scope(socialSignOnProvider.getScope()) - .callback(socialSignOnProvider.getCallBack()) - .signatureType(SignatureType.QueryString) - .debug() - .build(); - } - } - - - - public SocialSignOnProvider getSocialSignOnProvider() { - return socialSignOnProvider; - } - - - public void setSocialSignOnProvider(SocialSignOnProvider socialSignOnProvider) { - this.socialSignOnProvider = socialSignOnProvider; - } - - - public Api getApi() { - return api; - } - - - public void setApi(Api api) { - this.api = api; - } - - -} diff --git a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/SocialSignOnEndpoint.java b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/SocialSignOnEndpoint.java index cab94943b..efc4f6bd4 100644 --- a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/SocialSignOnEndpoint.java +++ b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/SocialSignOnEndpoint.java @@ -8,7 +8,6 @@ import javax.servlet.http.HttpServletRequest; import org.maxkey.authn.realm.AbstractAuthenticationRealm; import org.maxkey.authn.support.socialsignon.service.SocialSignOnUserToken; import org.maxkey.constants.LOGINTYPE; -import org.maxkey.util.JsonUtils; import org.maxkey.web.WebContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +21,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; +import me.zhyd.oauth.utils.AuthStateUtils; + /** * @author Crystal.Sea * @@ -38,7 +39,7 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{ public ModelAndView socialSignOnAuthorize(String provider){ _logger.debug("SocialSignOn provider : "+provider); - String authorizationUrl=buildOAuthService(provider).getAuthorizationUrl(null); + String authorizationUrl=buildAuthRequest(provider).authorize(AuthStateUtils.createState()); _logger.debug("authorize SocialSignOn : "+authorizationUrl); return WebContext.redirect(authorizationUrl); } @@ -89,8 +90,7 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{ public ModelAndView callback(@PathVariable String provider ) { this.provider=provider; - this.getAccessToken(); - this.getAccountId(); + this.authCallback(); _logger.debug(this.accountId); SocialSignOnUserToken socialSignOnUserToken =new SocialSignOnUserToken(); socialSignOnUserToken.setProvider(provider); @@ -121,8 +121,8 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{ socialSignOnUserToken.setSocialUserInfo(accountJsonString); socialSignOnUserToken.setUid(WebContext.getUserInfo().getId()); socialSignOnUserToken.setUsername(WebContext.getUserInfo().getUsername()); - socialSignOnUserToken.setAccessToken(JsonUtils.object2Json(accessToken)); - socialSignOnUserToken.setExAttribute(JsonUtils.object2Json(accessToken.getResponseObject())); + //socialSignOnUserToken.setAccessToken(JsonUtils.object2Json(accessToken)); + //socialSignOnUserToken.setExAttribute(JsonUtils.object2Json(accessToken.getResponseObject())); _logger.debug("Social Bind : "+socialSignOnUserToken); this.socialSignOnUserTokenService.delete(socialSignOnUserToken); this.socialSignOnUserTokenService.insert(socialSignOnUserToken); @@ -139,9 +139,9 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{ _logger.debug("Social Sign On from "+socialSignOnUserToken.getProvider()+" mapping to user "+socialSignOnUserToken.getUsername()); if(WebContext.setAuthentication(socialSignOnUserToken.getUsername(), LOGINTYPE.SOCIALSIGNON,this.socialSignOnProvider.getProviderName(),"xe00000004","success")){ - socialSignOnUserToken.setAccessToken(JsonUtils.object2Json(this.accessToken)); + //socialSignOnUserToken.setAccessToken(JsonUtils.object2Json(this.accessToken)); socialSignOnUserToken.setSocialUserInfo(accountJsonString); - socialSignOnUserToken.setExAttribute(JsonUtils.object2Json(accessToken.getResponseObject())); + //socialSignOnUserToken.setExAttribute(JsonUtils.object2Json(accessToken.getResponseObject())); this.socialSignOnUserTokenService.update(socialSignOnUserToken); } diff --git a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProvider.java b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProvider.java index 4d4414a5c..d9bcd9c4d 100644 --- a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProvider.java +++ b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProvider.java @@ -11,13 +11,6 @@ public class SocialSignOnProvider { private String icon; private String clientId; private String clientSecret; - private String callBack; - private String authorizeUrl; - private String accessTokenUrl; - private String accessTokenMethod; - private String scope; - private String verifierCode; - private String accountUrl; private String accountId; private int sortOrder; @@ -30,96 +23,63 @@ public class SocialSignOnProvider { public SocialSignOnProvider() { } - - + public String getProvider() { return provider; } + public void setProvider(String provider) { this.provider = provider; } - public String getIcon() { - return icon; - } - public void setIcon(String icon) { - this.icon = icon; - } + public String getProviderName() { return providerName; } + public void setProviderName(String providerName) { this.providerName = providerName; } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + public String getClientId() { return clientId; } + public void setClientId(String clientId) { this.clientId = clientId; } + public String getClientSecret() { return clientSecret; } + public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } - public String getAuthorizeUrl() { - return authorizeUrl; - } - public void setAuthorizeUrl(String authorizeUrl) { - this.authorizeUrl = authorizeUrl; - } - public String getAccessTokenUrl() { - return accessTokenUrl; - } - public void setAccessTokenUrl(String accessTokenUrl) { - this.accessTokenUrl = accessTokenUrl; - } - public String getAccessTokenMethod() { - return accessTokenMethod; - } - public void setAccessTokenMethod(String accessTokenMethod) { - this.accessTokenMethod = accessTokenMethod; - } - public String getScope() { - return scope; - } - public void setScope(String scope) { - this.scope = scope; - } - public String getVerifierCode() { - return verifierCode; - } - public void setVerifierCode(String verifierCode) { - this.verifierCode = verifierCode; - } - public String getAccountUrl() { - return accountUrl; - } - public void setAccountUrl(String accountUrl) { - this.accountUrl = accountUrl; - } public String getAccountId() { return accountId; } + public void setAccountId(String accountId) { this.accountId = accountId; } + public int getSortOrder() { return sortOrder; } + public void setSortOrder(int sortOrder) { this.sortOrder = sortOrder; } - public String getCallBack() { - return callBack; - } - - public void setCallBack(String callBack) { - this.callBack = callBack; - } - public boolean isUserBind() { return userBind; } @@ -127,17 +87,7 @@ public class SocialSignOnProvider { public void setUserBind(boolean userBind) { this.userBind = userBind; } - - @Override - public String toString() { - return "SocialSignOnProvider [provider=" + provider + ", providerName=" - + providerName + ", icon=" + icon + ", clientId=" + clientId - + ", clientSecret=" + clientSecret + ", authorizeUrl=" - + authorizeUrl + ", accessTokenUrl=" + accessTokenUrl - + ", accessTokenMethod=" + accessTokenMethod + ", scope=" - + scope + ", verifierCode=" + verifierCode + ", accountUrl=" - + accountUrl + ", accountId=" + accountId + ", sortOrder=" - + sortOrder + ", userBind=" + userBind + "]"; - } + + } diff --git a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProviderService.java b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProviderService.java index e203b2e21..a35bb9f12 100644 --- a/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProviderService.java +++ b/maxkey-authentications/src/main/java/org/maxkey/authn/support/socialsignon/service/SocialSignOnProviderService.java @@ -3,9 +3,15 @@ package org.maxkey.authn.support.socialsignon.service; import java.util.HashMap; import java.util.List; +import org.maxkey.web.WebContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.*; + public class SocialSignOnProviderService{ @@ -19,7 +25,62 @@ public class SocialSignOnProviderService{ public SocialSignOnProvider get(String provider){ return socialSignOnProviderMaps.get(provider); } - + + public AuthRequest getAuthRequest(String provider) { + AuthRequest authRequest = null; + AuthConfig authConfig = AuthConfig.builder() + .clientId(this.get(provider).getClientId()) + .clientSecret(this.get(provider).getClientSecret()) + .redirectUri(WebContext.getHttpContextPath()+ "/logon/oauth20/callback/"+provider) + .build(); + + if(provider.equalsIgnoreCase("WeChatOpen")) { + authRequest = new AuthWeChatOpenRequest(authConfig); + }else if(provider.equalsIgnoreCase("sinaweibo")) { + authRequest = new AuthWeiboRequest(authConfig); + }else if(provider.equalsIgnoreCase("qq")) { + authRequest = new AuthQqRequest(authConfig); + }else if(provider.equalsIgnoreCase("Alipay")) { + authRequest = new AuthAlipayRequest(authConfig); + }else if(provider.equalsIgnoreCase("Twitter")) { + authRequest = new AuthTwitterRequest(authConfig); + }else if(provider.equalsIgnoreCase("google")) { + authRequest = new AuthGoogleRequest(authConfig); + }else if(provider.equalsIgnoreCase("Windows")) { + authRequest = new AuthMicrosoftRequest(authConfig); + }else if(provider.equalsIgnoreCase("Linkedin")) { + authRequest = new AuthLinkedinRequest(authConfig); + }else if(provider.equalsIgnoreCase("DingTalk")) { + authRequest = new AuthDingTalkRequest(authConfig); + } + + + + return authRequest; + } + + public String getAccountId(String provider,AuthResponse authResponse) { + if(provider.equalsIgnoreCase("WeChatOpen")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("sinaweibo")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("qq")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("Alipay")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("Twitter")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("google")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("Windows")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("Linkedin")) { + return ((AuthUser)authResponse.getData()).getUuid(); + }else if(provider.equalsIgnoreCase("DingTalk")) { + return ((AuthUser)authResponse.getData()).getUuid(); + } + return null; + } public List getSocialSignOnProviders() { return socialSignOnProviders; } diff --git a/maxkey-client-sdk/.classpath b/maxkey-client-sdk/.classpath index 7a7f31fec..b79fc0c54 100644 --- a/maxkey-client-sdk/.classpath +++ b/maxkey-client-sdk/.classpath @@ -10,7 +10,6 @@ - diff --git a/maxkey-client-sdk/src/main/java/org/maxkey/client/oauth/builder/api/OAuthApi20.java b/maxkey-client-sdk/src/main/java/org/maxkey/client/oauth/builder/api/OAuthApi20.java index 0c5c08ecd..ceee17018 100644 --- a/maxkey-client-sdk/src/main/java/org/maxkey/client/oauth/builder/api/OAuthApi20.java +++ b/maxkey-client-sdk/src/main/java/org/maxkey/client/oauth/builder/api/OAuthApi20.java @@ -81,12 +81,22 @@ public class OAuthApi20 extends DefaultApi20 @Override public String getAuthorizationUrl(OAuthConfig config){ // Append scope if present - if (config.hasScope()){ - return String.format(authorizeUrl+scope, config.getApiKey(), HttpEncoder.encode(config.getCallback()), HttpEncoder.encode(config.getScope())); - } - else{ - return String.format(authorizeUrl, config.getApiKey(), HttpEncoder.encode(config.getCallback())); - } + //dingtalk + if(authorizeUrl.indexOf("oapi.dingtalk.com")>-1) { + if (config.hasScope()){ + return String.format(authorizeUrl+scope, config.getApiKey(), config.getCallback(), HttpEncoder.encode(config.getScope())); + } + else{ + return String.format(authorizeUrl, config.getApiKey(), config.getCallback()); + } + }else { + if (config.hasScope()){ + return String.format(authorizeUrl+scope, config.getApiKey(), HttpEncoder.encode(config.getCallback()), HttpEncoder.encode(config.getScope())); + } + else{ + return String.format(authorizeUrl, config.getApiKey(), HttpEncoder.encode(config.getCallback())); + } + } } public String getAuthorizeUrl() { diff --git a/maxkey-core/.classpath b/maxkey-core/.classpath index 7a7f31fec..b79fc0c54 100644 --- a/maxkey-core/.classpath +++ b/maxkey-core/.classpath @@ -10,7 +10,6 @@ - diff --git a/maxkey-core/src/main/java/org/maxkey/web/WebContext.java b/maxkey-core/src/main/java/org/maxkey/web/WebContext.java index dd87c2778..a54f0702a 100644 --- a/maxkey-core/src/main/java/org/maxkey/web/WebContext.java +++ b/maxkey-core/src/main/java/org/maxkey/web/WebContext.java @@ -1,12 +1,12 @@ package org.maxkey.web; -import java.util.ArrayList; import java.util.Locale; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.logging.LogFactory; +import org.maxkey.authn.BasicAuthentication; import org.maxkey.authn.realm.AbstractAuthenticationRealm; import org.maxkey.config.ApplicationConfig; import org.maxkey.domain.UserInfo; @@ -16,8 +16,6 @@ import org.maxkey.web.message.Message; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -83,11 +81,18 @@ public final class WebContext { UserInfo loadeduserInfo = authenticationRealm.loadUserInfo(username,""); if (loadeduserInfo != null) { - ArrayList grantedAuthority = authenticationRealm.grantAuthority(loadeduserInfo); setUserInfo(loadeduserInfo); - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loadeduserInfo.getUsername(), loadeduserInfo.getPassword(), grantedAuthority); + BasicAuthentication authentication =new BasicAuthentication(); + authentication.setJ_username(loadeduserInfo.getUsername()); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =new UsernamePasswordAuthenticationToken( + authentication, + "PASSWORD", + authenticationRealm.grantAuthority(loadeduserInfo)); - SecurityContextHolder.getContext().setAuthentication(authentication); + authentication.setAuthenticated(true); + WebContext.setAuthentication(usernamePasswordAuthenticationToken); + WebContext.setUserInfo(loadeduserInfo); + authenticationRealm.insertLoginHistory(loadeduserInfo, type, provider, code, message); } return true; diff --git a/maxkey-dao/.classpath b/maxkey-dao/.classpath index 8b3d57e2b..540156aa7 100644 --- a/maxkey-dao/.classpath +++ b/maxkey-dao/.classpath @@ -16,14 +16,12 @@ - - diff --git a/maxkey-protocols/maxkey-protocol-cas/.classpath b/maxkey-protocols/maxkey-protocol-cas/.classpath index 7a7f31fec..b79fc0c54 100644 --- a/maxkey-protocols/maxkey-protocol-cas/.classpath +++ b/maxkey-protocols/maxkey-protocol-cas/.classpath @@ -10,7 +10,6 @@ - diff --git a/maxkey-web-manage/.classpath b/maxkey-web-manage/.classpath index 7fea4e142..2a1397787 100644 --- a/maxkey-web-manage/.classpath +++ b/maxkey-web-manage/.classpath @@ -14,6 +14,10 @@ - + + + + + diff --git a/maxkey-web-maxkey/.classpath b/maxkey-web-maxkey/.classpath index 1d13cc0b0..1501fbb14 100644 --- a/maxkey-web-maxkey/.classpath +++ b/maxkey-web-maxkey/.classpath @@ -16,11 +16,14 @@ - - + + + + + diff --git a/maxkey-web-maxkey/src/main/java/org/maxkey/web/endpoint/IndexEndpoint.java b/maxkey-web-maxkey/src/main/java/org/maxkey/web/endpoint/IndexEndpoint.java index 16741741a..bab6c95c2 100644 --- a/maxkey-web-maxkey/src/main/java/org/maxkey/web/endpoint/IndexEndpoint.java +++ b/maxkey-web-maxkey/src/main/java/org/maxkey/web/endpoint/IndexEndpoint.java @@ -31,13 +31,13 @@ public class IndexEndpoint { _logger.debug("IndexEndpoint /forwardindex."); ModelAndView modelAndView=new ModelAndView(); Integer passwordSetType=(Integer)WebContext.getSession().getAttribute(WebConstants.CURRENT_LOGIN_USER_PASSWORD_SET_TYPE); - if(passwordSetType==PASSWORDSETTYPE.PASSWORD_NORMAL){ + if(passwordSetType==null || passwordSetType==PASSWORDSETTYPE.PASSWORD_NORMAL){ if(applicationConfig.getLoginConfig().getDefaultUri()!=null&& !applicationConfig.getLoginConfig().getDefaultUri().equals("")){ if(applicationConfig.getLoginConfig().getDefaultUri().startsWith("http")){ return WebContext.redirect(applicationConfig.getLoginConfig().getDefaultUri()); } - return WebContext.forward(applicationConfig.getLoginConfig().getDefaultUri()); + return WebContext.redirect(applicationConfig.getLoginConfig().getDefaultUri()); } modelAndView.setViewName("index"); return modelAndView; diff --git a/maxkey-web-maxkey/src/main/resources/config/applicationLogin.properties b/maxkey-web-maxkey/src/main/resources/config/applicationLogin.properties index 9e6af77d9..5f71c578c 100644 --- a/maxkey-web-maxkey/src/main/resources/config/applicationLogin.properties +++ b/maxkey-web-maxkey/src/main/resources/config/applicationLogin.properties @@ -76,3 +76,64 @@ config.oidc.metadata.authorizationEndpoint=http://${config.server.name}/maxkey/o config.oidc.metadata.tokenEndpoint=http://${config.server.name}/maxkey/oauth/v20/token config.oidc.metadata.userinfoEndpoint=http://${config.server.name}/maxkey/api/connect/userinfo ############################################################################# + + +############################################################################ +# Social Sign On Configuration # +#you config client.id & client.secret only +############################################################################ + +############################################################################ +#sina weibo +config.socialsignon.sinaweibo.provider=sinaweibo +config.socialsignon.sinaweibo.provider.name=\u65B0\u6D6A\u5FAE\u535A +config.socialsignon.sinaweibo.icon=images/social/sinaweibo.png +config.socialsignon.sinaweibo.client.id=3379757634 +config.socialsignon.sinaweibo.client.secret=1adfdf9800299037bcab9d1c238664ba +config.socialsignon.sinaweibo.account.id=id +config.socialsignon.sinaweibo.sortorder=1 + +#Google +config.socialsignon.google.provider=google +config.socialsignon.google.provider.name=Google +config.socialsignon.google.icon=images/social/google.png +config.socialsignon.google.client.id=519914515488.apps.googleusercontent.com +config.socialsignon.google.client.secret=3aTW3Iw7e11QqMnHxciCaXTt +config.socialsignon.google.account.id=id +config.socialsignon.google.sortorder=2 + +#QQ +config.socialsignon.qq.provider=qq +config.socialsignon.qq.provider.name=QQ +config.socialsignon.qq.icon=images/social/qq.png +config.socialsignon.qq.client.id=101224990 +config.socialsignon.qq.client.secret=09d7481b68d888f01831e3ef7c1c3015 +config.socialsignon.qq.account.id=openid +config.socialsignon.qq.sortorder=4 + +#dingtalk +config.socialsignon.dingtalk.provider=dingtalk +config.socialsignon.dingtalk.provider.name=dingtalk +config.socialsignon.dingtalk.icon=images/social/dingtalk.png +config.socialsignon.dingtalk.client.id=dingoawf2jyiwh2uzqnphg +config.socialsignon.dingtalk.client.secret=Crm7YJbMKfRlvG2i1SHpg4GHVpqF_oXiEjhmRQyiSiuzNRWpbFh9i0UjDTfhOoN9 +config.socialsignon.dingtalk.account.id=openid +config.socialsignon.dingtalk.sortorder=4 + +#Windows Live +config.socialsignon.live.provider=Windows +config.socialsignon.live.provider.name=Windows Live +config.socialsignon.live.icon=images/social/live.png +config.socialsignon.live.client.id=00000000401129A4 +config.socialsignon.live.client.secret=Kx-OAmHaoqG5vcitm3-TASOSZD1ebu64 +config.socialsignon.live.account.id=id +config.socialsignon.live.sortorder=5 + +#facebook +config.socialsignon.facebook.provider=facebook +config.socialsignon.facebook.provider.name=facebook +config.socialsignon.facebook.icon=images/social/facebook.png +config.socialsignon.facebook.client.id=appKey +config.socialsignon.facebook.client.secret=appSecret +config.socialsignon.facebook.account.id=id +config.socialsignon.facebook.sortorder=7 \ No newline at end of file diff --git a/maxkey-web-maxkey/src/main/resources/config/applicationSocialSignOn.properties b/maxkey-web-maxkey/src/main/resources/config/applicationSocialSignOn.properties deleted file mode 100644 index a36948c7b..000000000 --- a/maxkey-web-maxkey/src/main/resources/config/applicationSocialSignOn.properties +++ /dev/null @@ -1,83 +0,0 @@ -############################################################################ -# MaxKey -############################################################################ -# Social Sign On Configuration # -#you config client.id & client.secret only -############################################################################ - -############################################################################ -#sina weibo -config.socialsignon.sinaweibo.provider=sinaweibo -config.socialsignon.sinaweibo.provider.name=\u65B0\u6D6A\u5FAE\u535A -config.socialsignon.sinaweibo.icon=images/social/sinaweibo.png -config.socialsignon.sinaweibo.client.id=3379757634 -config.socialsignon.sinaweibo.client.secret=1adfdf9800299037bcab9d1c238664ba -config.socialsignon.sinaweibo.authorize.url=https://api.weibo.com/oauth2/authorize?client_id=%s&redirect_uri=%s&response_type=code -config.socialsignon.sinaweibo.accesstoken.url=https://api.weibo.com/oauth2/access_token -config.socialsignon.sinaweibo.accesstoken.method=POST -config.socialsignon.sinaweibo.scope=all -config.socialsignon.sinaweibo.verifier.code=code -config.socialsignon.sinaweibo.account.url=https://api.weibo.com/2/users/show.json -config.socialsignon.sinaweibo.account.id=id -config.socialsignon.sinaweibo.sortorder=1 - -#Google -config.socialsignon.google.provider=google -config.socialsignon.google.provider.name=Google -config.socialsignon.google.icon=images/social/google.png -config.socialsignon.google.client.id=519914515488.apps.googleusercontent.com -config.socialsignon.google.client.secret=3aTW3Iw7e11QqMnHxciCaXTt -config.socialsignon.google.authorize.url=https://accounts.google.com/o/oauth2/auth?client_id=%s&redirect_uri=%s&response_type=code -config.socialsignon.google.accesstoken.url=https://accounts.google.com/o/oauth2/token?access_type=offline -config.socialsignon.google.accesstoken.method=POST -config.socialsignon.google.scope=openid email profile -#config.socialsignon.google.scope=https://www.googleapis.com/auth/userinfo.email -config.socialsignon.google.verifier.code=code -config.socialsignon.google.account.url=https://www.googleapis.com/plus/v1/people/me -config.socialsignon.google.account.id=id -config.socialsignon.google.sortorder=2 - -#QQ -config.socialsignon.qq.provider=qq -config.socialsignon.qq.provider.name=QQ -config.socialsignon.qq.icon=images/social/qq.png -config.socialsignon.qq.client.id=101224990 -config.socialsignon.qq.client.secret=09d7481b68d888f01831e3ef7c1c3015 -config.socialsignon.qq.authorize.url=https://graph.qq.com/oauth2.0/authorize?client_id=%s&redirect_uri=%s&response_type=code -config.socialsignon.qq.accesstoken.url=https://graph.qq.com/oauth2.0/token -config.socialsignon.qq.accesstoken.method=POST -config.socialsignon.qq.scope=read -config.socialsignon.qq.verifier.code=code -config.socialsignon.qq.account.url=https://graph.qq.com/oauth2.0/me -config.socialsignon.qq.account.id=openid -config.socialsignon.qq.sortorder=4 - -#Windows Live -config.socialsignon.live.provider=live -config.socialsignon.live.provider.name=Windows Live -config.socialsignon.live.icon=images/social/live.png -config.socialsignon.live.client.id=00000000401129A4 -config.socialsignon.live.client.secret=Kx-OAmHaoqG5vcitm3-TASOSZD1ebu64 -config.socialsignon.live.authorize.url=https://login.live.com/oauth20_authorize.srf?client_id=%s&redirect_uri=%s&response_type=code -config.socialsignon.live.accesstoken.url=https://login.live.com/oauth20_token.srf -config.socialsignon.live.accesstoken.method=GET -config.socialsignon.live.scope=wl.basic -config.socialsignon.live.verifier.code=code -config.socialsignon.live.account.url=https://apis.live.net/v5.0/me -config.socialsignon.live.account.id=id -config.socialsignon.live.sortorder=5 - -#facebook -config.socialsignon.facebook.provider=facebook -config.socialsignon.facebook.provider.name=facebook -config.socialsignon.facebook.icon=images/social/facebook.png -config.socialsignon.facebook.client.id=appKey -config.socialsignon.facebook.client.secret=appSecret -config.socialsignon.facebook.authorize.url=https://www.facebook.com/dialog/oauth?client_id=%s&redirect_uri=%s&response_type=code -config.socialsignon.facebook.accesstoken.url=https://graph.facebook.com/oauth/access_token -config.socialsignon.facebook.accesstoken.method=GET -config.socialsignon.facebook.scope=read -config.socialsignon.facebook.verifier.code=code -config.socialsignon.facebook.account.url=https://graph.facebook.com/me -config.socialsignon.facebook.account.id=id -config.socialsignon.facebook.sortorder=7 diff --git a/maxkey-web-maxkey/src/main/resources/spring/maxkey-support-social.xml b/maxkey-web-maxkey/src/main/resources/spring/maxkey-support-social.xml index a02ac97c6..7502c939f 100644 --- a/maxkey-web-maxkey/src/main/resources/spring/maxkey-support-social.xml +++ b/maxkey-web-maxkey/src/main/resources/spring/maxkey-support-social.xml @@ -28,13 +28,6 @@ - - - - - - - @@ -44,13 +37,6 @@ - - - - - - - @@ -60,13 +46,6 @@ - - - - - - - @@ -76,13 +55,6 @@ - - - - - - - @@ -92,15 +64,16 @@ - - - - - - - - + + + + + + + + + @@ -110,6 +83,8 @@ + + diff --git a/maxkey-web-maxkey/src/main/resources/spring/maxkey.xml b/maxkey-web-maxkey/src/main/resources/spring/maxkey.xml index 59012b959..7f1442074 100644 --- a/maxkey-web-maxkey/src/main/resources/spring/maxkey.xml +++ b/maxkey-web-maxkey/src/main/resources/spring/maxkey.xml @@ -25,7 +25,6 @@ classpath:config/applicationConfig.properties classpath:config/applicationLogin.properties classpath:config/applicationSaml.properties - classpath:config/applicationSocialSignOn.properties diff --git a/maxkey-web-maxkey/src/main/resources/static/images/social/dingtalk.png b/maxkey-web-maxkey/src/main/resources/static/images/social/dingtalk.png new file mode 100644 index 0000000000000000000000000000000000000000..5c09c5a20449025f2f1b83f9bc2b2b91d94d225a GIT binary patch literal 23805 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