diff --git a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java index 973ddbf1f21445b062beb623a33d4b532e543953..70e784f17f8e2e08793bf471ba403220ddc0a132 100644 --- a/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java +++ b/src/main/java/me/zhyd/oauth/config/AuthDefaultSource.java @@ -515,6 +515,33 @@ public enum AuthDefaultSource implements AuthSource { return AuthMicrosoftRequest.class; } }, + /** + * 微软中国(世纪互联) + */ + MICROSOFT_CN { + @Override + public String authorize() { + return "https://login.partner.microsoftonline.cn/common/oauth2/v2.0/authorize"; + } + + @Override + public String accessToken() { + return "https://login.partner.microsoftonline.cn/common/oauth2/v2.0/token"; + } + + @Override + public String userInfo() { + return "https://microsoftgraph.chinacloudapi.cn/v1.0/me"; + } + + @Override + public String refresh() { + return "https://login.partner.microsoftonline.cn/common/oauth2/v2.0/token"; + } + + @Override + public Class getTargetClass() { return AuthMicrosoftCnRequest.class; } + }, /** * 小米 */ diff --git a/src/main/java/me/zhyd/oauth/request/AbstractAuthMicrosoftRequest.java b/src/main/java/me/zhyd/oauth/request/AbstractAuthMicrosoftRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..f3810e29e83dd680e2024c953259977a0b2992cf --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AbstractAuthMicrosoftRequest.java @@ -0,0 +1,181 @@ +package me.zhyd.oauth.request; + +import com.alibaba.fastjson.JSONObject; +import com.xkcoding.http.support.HttpHeader; +import com.xkcoding.http.util.MapUtil; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.enums.AuthResponseStatus; +import me.zhyd.oauth.enums.AuthUserGender; +import me.zhyd.oauth.enums.scope.AuthMicrosoftScope; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.utils.AuthScopeUtils; +import me.zhyd.oauth.utils.HttpUtils; +import me.zhyd.oauth.utils.UrlBuilder; + +import java.util.Map; + +/** + * 微软登录抽象类,负责处理使用微软国际和微软中国账号登录第三方网站的登录方式 + * + * @author mroldx (xzfqq5201314@gmail.com) + * @since 1.16.4 + */ +public abstract class AbstractAuthMicrosoftRequest extends AuthDefaultRequest { + + public AbstractAuthMicrosoftRequest(AuthConfig config, AuthSource source) { + super(config, source); + } + + + public AbstractAuthMicrosoftRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) { + super(config, source, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + return getToken(accessTokenUrl(authCallback.getCode())); + } + + /** + * 获取token,适用于获取access_token和刷新token + * + * @param accessTokenUrl 实际请求token的地址 + * @return token对象 + */ + private AuthToken getToken(String accessTokenUrl) { + HttpHeader httpHeader = new HttpHeader(); + + Map form = MapUtil.parseStringToMap(accessTokenUrl, false); + + String response = new HttpUtils(config.getHttpConfig()).post(accessTokenUrl, form, httpHeader, false); + JSONObject accessTokenObject = JSONObject.parseObject(response); + + this.checkResponse(accessTokenObject); + + return AuthToken.builder() + .accessToken(accessTokenObject.getString("access_token")) + .expireIn(accessTokenObject.getIntValue("expires_in")) + .scope(accessTokenObject.getString("scope")) + .tokenType(accessTokenObject.getString("token_type")) + .refreshToken(accessTokenObject.getString("refresh_token")) + .build(); + } + + /** + * 检查响应内容是否正确 + * + * @param object 请求响应内容 + */ + private void checkResponse(JSONObject object) { + if (object.containsKey("error")) { + throw new AuthException(object.getString("error_description")); + } + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + String token = authToken.getAccessToken(); + String tokenType = authToken.getTokenType(); + String jwt = tokenType + " " + token; + + HttpHeader httpHeader = new HttpHeader(); + httpHeader.add("Authorization", jwt); + + String userInfo = new HttpUtils(config.getHttpConfig()).get(userInfoUrl(authToken), null, httpHeader, false); + JSONObject object = JSONObject.parseObject(userInfo); + this.checkResponse(object); + return AuthUser.builder() + .rawUserInfo(object) + .uuid(object.getString("id")) + .username(object.getString("userPrincipalName")) + .nickname(object.getString("displayName")) + .location(object.getString("officeLocation")) + .email(object.getString("mail")) + .gender(AuthUserGender.UNKNOWN) + .token(authToken) + .source(source.toString()) + .build(); + } + + /** + * 刷新access token (续期) + * + * @param authToken 登录成功后返回的Token信息 + * @return AuthResponse + */ + @Override + public AuthResponse refresh(AuthToken authToken) { + return AuthResponse.builder() + .code(AuthResponseStatus.SUCCESS.getCode()) + .data(getToken(refreshTokenUrl(authToken.getRefreshToken()))) + .build(); + } + + /** + * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state} + * + * @param state state 验证授权流程的参数,可以防止csrf + * @return 返回授权地址 + * @since 1.9.3 + */ + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(super.authorize(state)) + .queryParam("response_mode", "query") + .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) + .build(); + } + + /** + * 返回获取accessToken的url + * + * @param code 授权code + * @return 返回获取accessToken的url + */ + @Override + protected String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(source.accessToken()) + .queryParam("code", code) + .queryParam("client_id", config.getClientId()) + .queryParam("client_secret", config.getClientSecret()) + .queryParam("grant_type", "authorization_code") + .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) + .queryParam("redirect_uri", config.getRedirectUri()) + .build(); + } + + /** + * 返回获取userInfo的url + * + * @param authToken 用户授权后的token + * @return 返回获取userInfo的url + */ + @Override + protected String userInfoUrl(AuthToken authToken) { + return UrlBuilder.fromBaseUrl(source.userInfo()).build(); + } + + /** + * 返回获取accessToken的url + * + * @param refreshToken 用户授权后的token + * @return 返回获取accessToken的url + */ + @Override + protected String refreshTokenUrl(String refreshToken) { + return UrlBuilder.fromBaseUrl(source.refresh()) + .queryParam("client_id", config.getClientId()) + .queryParam("client_secret", config.getClientSecret()) + .queryParam("refresh_token", refreshToken) + .queryParam("grant_type", "refresh_token") + .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) + .queryParam("redirect_uri", config.getRedirectUri()) + .build(); + } +} diff --git a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..eccd60631a7b46ac501b0f33c60c9d8a48ad7ca7 --- /dev/null +++ b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftCnRequest.java @@ -0,0 +1,23 @@ +package me.zhyd.oauth.request; + +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.config.AuthDefaultSource; + +/** + * 微软中国登录(世纪华联) + * + * @author mroldx (xzfqq5201314@gmail.com) + * @since 1.16.4 + */ +public class AuthMicrosoftCnRequest extends AbstractAuthMicrosoftRequest { + + public AuthMicrosoftCnRequest(AuthConfig config) { + super(config, AuthDefaultSource.MICROSOFT_CN); + } + + public AuthMicrosoftCnRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthDefaultSource.MICROSOFT_CN, authStateCache); + } + +} diff --git a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java index 199cd3b5f3fd215975e6c03237a19a9e5519b963..f8b906602544e5b8b7a8c16ad7e9fab511b4c25b 100644 --- a/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java +++ b/src/main/java/me/zhyd/oauth/request/AuthMicrosoftRequest.java @@ -1,32 +1,18 @@ package me.zhyd.oauth.request; -import com.alibaba.fastjson.JSONObject; -import com.xkcoding.http.support.HttpHeader; -import com.xkcoding.http.util.MapUtil; import me.zhyd.oauth.cache.AuthStateCache; import me.zhyd.oauth.config.AuthConfig; import me.zhyd.oauth.config.AuthDefaultSource; -import me.zhyd.oauth.enums.AuthResponseStatus; -import me.zhyd.oauth.enums.AuthUserGender; -import me.zhyd.oauth.enums.scope.AuthMicrosoftScope; -import me.zhyd.oauth.exception.AuthException; -import me.zhyd.oauth.model.AuthCallback; -import me.zhyd.oauth.model.AuthResponse; -import me.zhyd.oauth.model.AuthToken; -import me.zhyd.oauth.model.AuthUser; -import me.zhyd.oauth.utils.AuthScopeUtils; -import me.zhyd.oauth.utils.HttpUtils; -import me.zhyd.oauth.utils.UrlBuilder; - -import java.util.Map; /** * 微软登录 * * @author yangkai.shen (https://xkcoding.com) + * @update:2021-08-24 mroldx (xzfqq5201314@gmail.com) * @since 1.5.0 */ -public class AuthMicrosoftRequest extends AuthDefaultRequest { +public class AuthMicrosoftRequest extends AbstractAuthMicrosoftRequest { + public AuthMicrosoftRequest(AuthConfig config) { super(config, AuthDefaultSource.MICROSOFT); } @@ -35,145 +21,4 @@ public class AuthMicrosoftRequest extends AuthDefaultRequest { super(config, AuthDefaultSource.MICROSOFT, authStateCache); } - @Override - protected AuthToken getAccessToken(AuthCallback authCallback) { - return getToken(accessTokenUrl(authCallback.getCode())); - } - - /** - * 获取token,适用于获取access_token和刷新token - * - * @param accessTokenUrl 实际请求token的地址 - * @return token对象 - */ - private AuthToken getToken(String accessTokenUrl) { - HttpHeader httpHeader = new HttpHeader(); - - Map form = MapUtil.parseStringToMap(accessTokenUrl, false); - - String response = new HttpUtils(config.getHttpConfig()).post(accessTokenUrl, form, httpHeader, false); - JSONObject accessTokenObject = JSONObject.parseObject(response); - - this.checkResponse(accessTokenObject); - - return AuthToken.builder() - .accessToken(accessTokenObject.getString("access_token")) - .expireIn(accessTokenObject.getIntValue("expires_in")) - .scope(accessTokenObject.getString("scope")) - .tokenType(accessTokenObject.getString("token_type")) - .refreshToken(accessTokenObject.getString("refresh_token")) - .build(); - } - - /** - * 检查响应内容是否正确 - * - * @param object 请求响应内容 - */ - private void checkResponse(JSONObject object) { - if (object.containsKey("error")) { - throw new AuthException(object.getString("error_description")); - } - } - - @Override - protected AuthUser getUserInfo(AuthToken authToken) { - String token = authToken.getAccessToken(); - String tokenType = authToken.getTokenType(); - String jwt = tokenType + " " + token; - - HttpHeader httpHeader = new HttpHeader(); - httpHeader.add("Authorization", jwt); - - String userInfo = new HttpUtils(config.getHttpConfig()).get(userInfoUrl(authToken), null, httpHeader, false); - JSONObject object = JSONObject.parseObject(userInfo); - this.checkResponse(object); - return AuthUser.builder() - .rawUserInfo(object) - .uuid(object.getString("id")) - .username(object.getString("userPrincipalName")) - .nickname(object.getString("displayName")) - .location(object.getString("officeLocation")) - .email(object.getString("mail")) - .gender(AuthUserGender.UNKNOWN) - .token(authToken) - .source(source.toString()) - .build(); - } - - /** - * 刷新access token (续期) - * - * @param authToken 登录成功后返回的Token信息 - * @return AuthResponse - */ - @Override - public AuthResponse refresh(AuthToken authToken) { - return AuthResponse.builder() - .code(AuthResponseStatus.SUCCESS.getCode()) - .data(getToken(refreshTokenUrl(authToken.getRefreshToken()))) - .build(); - } - - /** - * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state} - * - * @param state state 验证授权流程的参数,可以防止csrf - * @return 返回授权地址 - * @since 1.9.3 - */ - @Override - public String authorize(String state) { - return UrlBuilder.fromBaseUrl(super.authorize(state)) - .queryParam("response_mode", "query") - .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) - .build(); - } - - /** - * 返回获取accessToken的url - * - * @param code 授权code - * @return 返回获取accessToken的url - */ - @Override - protected String accessTokenUrl(String code) { - return UrlBuilder.fromBaseUrl(source.accessToken()) - .queryParam("code", code) - .queryParam("client_id", config.getClientId()) - .queryParam("client_secret", config.getClientSecret()) - .queryParam("grant_type", "authorization_code") - .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) - .queryParam("redirect_uri", config.getRedirectUri()) - .build(); - } - - /** - * 返回获取userInfo的url - * - * @param authToken 用户授权后的token - * @return 返回获取userInfo的url - */ - @Override - protected String userInfoUrl(AuthToken authToken) { - return UrlBuilder.fromBaseUrl(source.userInfo()).build(); - } - - /** - * 返回获取accessToken的url - * - * @param refreshToken 用户授权后的token - * @return 返回获取accessToken的url - */ - @Override - protected String refreshTokenUrl(String refreshToken) { - return UrlBuilder.fromBaseUrl(source.refresh()) - .queryParam("client_id", config.getClientId()) - .queryParam("client_secret", config.getClientSecret()) - .queryParam("refresh_token", refreshToken) - .queryParam("grant_type", "refresh_token") - .queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthMicrosoftScope.values()))) - .queryParam("redirect_uri", config.getRedirectUri()) - .build(); - } } diff --git a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java index 319470e7dd94e8fe05260385da7c34917a11dbba..b8ce6f78f3d3aa688d60bec91c4a3815f3e4c619 100644 --- a/src/main/java/me/zhyd/oauth/utils/AuthChecker.java +++ b/src/main/java/me/zhyd/oauth/utils/AuthChecker.java @@ -76,6 +76,16 @@ public class AuthChecker { // The redirect uri of alipay is forbidden to use localhost or 127.0.0.1 throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source); } + // 微软的回调地址必须为https的链接或者localhost,不允许使用http + if(AuthDefaultSource.MICROSOFT== source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(redirectUri) ){ + // Microsoft's redirect uri must use the HTTPS or localhost + throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source); + } + // 微软中国的回调地址必须为https的链接或者localhost,不允许使用http + if(AuthDefaultSource.MICROSOFT_CN== source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(redirectUri) ){ + // Microsoft's redirect uri must use the HTTPS or localhost + throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source); + } } /** diff --git a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java index 8b2248043551dbfab9d8eee7369cd208d679a4be..ffcae3275337c203023bf0daf4a2035756b55fe9 100644 --- a/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java +++ b/src/main/java/me/zhyd/oauth/utils/GlobalAuthUtils.java @@ -175,6 +175,19 @@ public class GlobalAuthUtils { return StringUtils.isEmpty(url) || url.contains("127.0.0.1") || url.contains("localhost"); } + /** + * 是否为https协议或本地主机(域名) + * + * @param url 待验证的url + * @return true: https协议或本地主机 false: 非https协议或本机主机 + */ + public static boolean isHttpsProtocolOrLocalHost(String url) { + if (StringUtils.isEmpty(url)) { + return false; + } + return isHttpsProtocol(url) || isLocalHost(url); + } + /** * Generate nonce with given length