mirror of
https://gitee.com/yadong.zhang/JustAuth.git
synced 2025-12-06 16:58:24 +08:00
commit
2ee2483aee
22
pom.xml
22
pom.xml
@ -63,6 +63,8 @@
|
||||
<fastjson-version>1.2.83</fastjson-version>
|
||||
<alipay-sdk-version>4.17.5.ALL</alipay-sdk-version>
|
||||
<jacoco-version>0.8.2</jacoco-version>
|
||||
<jwt.version>0.12.3</jwt.version>
|
||||
<bcpkix-jdk18on.version>1.77</bcpkix-jdk18on.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -100,6 +102,26 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>${bcpkix-jdk18on.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -186,4 +186,16 @@ public class AuthConfig {
|
||||
* Microsoft Entra ID(原微软 AAD)中的租户 ID
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 苹果开发者账号中的密钥标识符
|
||||
* @see <a href="https://developer.apple.com/help/account/configure-app-capabilities/create-a-sign-in-with-apple-private-key/">create-a-sign-in-with-apple-private-key</a>
|
||||
*/
|
||||
private String kid;
|
||||
|
||||
/**
|
||||
* 苹果开发者账号中的团队ID
|
||||
* @see <a href="https://developer.apple.com/help/glossary/team-id/">team id</a>
|
||||
*/
|
||||
private String teamId;
|
||||
}
|
||||
|
||||
@ -1296,6 +1296,31 @@ public enum AuthDefaultSource implements AuthSource {
|
||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
||||
return AuthProginnRequest.class;
|
||||
}
|
||||
},
|
||||
|
||||
APPLE {
|
||||
@Override
|
||||
public String authorize() {
|
||||
return "https://appleid.apple.com/auth/authorize";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens">generate_and_validate_tokens</a>
|
||||
*/
|
||||
@Override
|
||||
public String accessToken() {
|
||||
return "https://appleid.apple.com/auth/token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String userInfo() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
||||
return AuthAppleRequest.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -29,6 +29,10 @@ public enum AuthResponseStatus {
|
||||
ILLEGAL_STATUS(5009, "Illegal state"),
|
||||
REQUIRED_REFRESH_TOKEN(5010, "The refresh token is required; it must not be null"),
|
||||
ILLEGAL_TOKEN(5011, "Invalid token"),
|
||||
ILLEGAL_KID(5012, "Invalid key identifier(kid)"),
|
||||
ILLEGAL_TEAM_ID(5013, "Invalid team id"),
|
||||
ILLEGAL_CLIENT_ID(5014, "Invalid client id"),
|
||||
ILLEGAL_CLIENT_SECRET(5015, "Invalid client secret"),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
19
src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java
Normal file
19
src/main/java/me/zhyd/oauth/enums/scope/AuthAppleScope.java
Normal file
@ -0,0 +1,19 @@
|
||||
package me.zhyd.oauth.enums.scope;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope/">scope</a>
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum AuthAppleScope implements AuthScope {
|
||||
EMAIL("email", "用户邮箱", true),
|
||||
NAME("name", "用户名", true),
|
||||
;
|
||||
|
||||
private final String scope;
|
||||
private final String description;
|
||||
private final boolean isDefault;
|
||||
}
|
||||
@ -57,4 +57,15 @@ public class AuthCallback implements Serializable {
|
||||
*/
|
||||
private String oauth_verifier;
|
||||
|
||||
/**
|
||||
* 苹果仅在用户首次授权应用程序时返回此值。如果您的应用程序已经获得了用户的授权,那么苹果将不会再次返回此值
|
||||
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/useri">user info</a>
|
||||
*/
|
||||
private String user;
|
||||
|
||||
/**
|
||||
* 苹果错误信息,仅在用户取消授权时返回此值
|
||||
* @see <a href="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms">error response</a>
|
||||
*/
|
||||
private String error;
|
||||
}
|
||||
|
||||
@ -62,4 +62,8 @@ public class AuthToken implements Serializable {
|
||||
private String screenName;
|
||||
private Boolean oauthCallbackConfirmed;
|
||||
|
||||
/**
|
||||
* Apple附带属性
|
||||
*/
|
||||
private String username;
|
||||
}
|
||||
|
||||
156
src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java
Normal file
156
src/main/java/me/zhyd/oauth/request/AuthAppleRequest.java
Normal file
@ -0,0 +1,156 @@
|
||||
package me.zhyd.oauth.request;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.impl.security.AbstractJwk;
|
||||
import lombok.Data;
|
||||
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.scope.AuthAppleScope;
|
||||
import me.zhyd.oauth.exception.AuthException;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthToken;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.utils.AuthScopeUtils;
|
||||
import me.zhyd.oauth.utils.StringUtils;
|
||||
import me.zhyd.oauth.utils.UrlBuilder;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AuthAppleRequest extends AuthDefaultRequest {
|
||||
|
||||
private static final String AUD = "https://appleid.apple.com";
|
||||
|
||||
private volatile PrivateKey privateKey;
|
||||
|
||||
public AuthAppleRequest(AuthConfig config) {
|
||||
super(config, AuthDefaultSource.APPLE);
|
||||
}
|
||||
|
||||
public AuthAppleRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthDefaultSource.APPLE, authStateCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
return UrlBuilder.fromBaseUrl(super.authorize(state))
|
||||
.queryParam("response_mode", "form_post")
|
||||
.queryParam("scope", this.getScopes(" ", false, AuthScopeUtils.getDefaultScopes(AuthAppleScope.values())))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
if (!StringUtils.isEmpty(authCallback.getError())) {
|
||||
throw new AuthException(authCallback.getError());
|
||||
}
|
||||
this.config.setClientSecret(this.getToken());
|
||||
// if failed will throw AuthException
|
||||
String response = doPostAuthorizationCode(authCallback.getCode());
|
||||
JSONObject accessTokenObject = JSONObject.parseObject(response);
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
|
||||
AuthToken.AuthTokenBuilder builder = AuthToken.builder()
|
||||
.accessToken(accessTokenObject.getString("access_token"))
|
||||
.expireIn(accessTokenObject.getIntValue("expires_in"))
|
||||
.refreshToken(accessTokenObject.getString("refresh_token"))
|
||||
.tokenType(accessTokenObject.getString("token_type"))
|
||||
.idToken(accessTokenObject.getString("id_token"));
|
||||
if (!StringUtils.isEmpty(authCallback.getUser())) {
|
||||
try {
|
||||
AppleUserInfo userInfo = JSONObject.parseObject(authCallback.getUser(), AppleUserInfo.class);
|
||||
builder.username(userInfo.getName().getFirstName() + " " + userInfo.getName().getLastName());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthUser getUserInfo(AuthToken authToken) {
|
||||
Base64.Decoder urlDecoder = Base64.getUrlDecoder();
|
||||
String[] idToken = authToken.getIdToken().split("\\.");
|
||||
String payload = new String(urlDecoder.decode(idToken[1]));
|
||||
JSONObject object = JSONObject.parseObject(payload);
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(object)
|
||||
.uuid(object.getString("sub"))
|
||||
.email(object.getString("email"))
|
||||
.username(authToken.getUsername())
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkConfig(AuthConfig config) {
|
||||
super.checkConfig(config);
|
||||
if (StringUtils.isEmpty(config.getClientId())) {
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_CLIENT_ID, source);
|
||||
}
|
||||
if (StringUtils.isEmpty(config.getClientSecret())) {
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_CLIENT_SECRET, source);
|
||||
}
|
||||
if (StringUtils.isEmpty(config.getKid())) {
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_KID, source);
|
||||
}
|
||||
if (StringUtils.isEmpty(config.getTeamId())) {
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_TEAM_ID, source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取token
|
||||
* @see <a href="https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret">creating-a-client-secret</a>
|
||||
* @return jwt token
|
||||
*/
|
||||
private String getToken() {
|
||||
return Jwts.builder().header().add(AbstractJwk.KID.getId(), this.config.getKid()).and()
|
||||
.issuer(this.config.getTeamId())
|
||||
.subject(this.config.getClientId())
|
||||
.audience().add(AUD).and()
|
||||
.expiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(3)))
|
||||
.issuedAt(new Date())
|
||||
.signWith(getPrivateKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
private PrivateKey getPrivateKey() {
|
||||
if (this.privateKey == null) {
|
||||
synchronized (this) {
|
||||
if (this.privateKey == null) {
|
||||
try (PEMParser pemParser = new PEMParser(new StringReader(this.config.getClientSecret()))) {
|
||||
JcaPEMKeyConverter pemKeyConverter = new JcaPEMKeyConverter();
|
||||
PrivateKeyInfo keyInfo = (PrivateKeyInfo) pemParser.readObject();
|
||||
this.privateKey = pemKeyConverter.getPrivateKey(keyInfo);
|
||||
} catch (IOException e) {
|
||||
throw new AuthException("Failed to get apple private key", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.privateKey;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class AppleUserInfo {
|
||||
private AppleUsername name;
|
||||
private String email;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class AppleUsername {
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ public abstract class AuthDefaultRequest implements AuthRequest {
|
||||
throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE, source);
|
||||
}
|
||||
// 校验配置合法性
|
||||
AuthChecker.checkConfig(config, source);
|
||||
this.checkConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -295,4 +295,8 @@ public abstract class AuthDefaultRequest implements AuthRequest {
|
||||
return encode ? UrlUtil.urlEncode(scopeStr) : scopeStr;
|
||||
}
|
||||
|
||||
protected void checkConfig(AuthConfig config) {
|
||||
AuthChecker.checkConfig(config, source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSONObject;
|
||||
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.AuthFacebookScope;
|
||||
import me.zhyd.oauth.exception.AuthException;
|
||||
@ -11,6 +12,7 @@ import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthToken;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.utils.AuthScopeUtils;
|
||||
import me.zhyd.oauth.utils.GlobalAuthUtils;
|
||||
import me.zhyd.oauth.utils.UrlBuilder;
|
||||
|
||||
/**
|
||||
@ -87,6 +89,16 @@ public class AuthFacebookRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkConfig(AuthConfig config) {
|
||||
super.checkConfig(config);
|
||||
// facebook的回调地址必须为https的链接
|
||||
if (AuthDefaultSource.FACEBOOK == source && !GlobalAuthUtils.isHttpsProtocol(config.getRedirectUri())) {
|
||||
// Facebook's redirect uri must use the HTTPS protocol
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查响应内容是否正确
|
||||
*
|
||||
|
||||
@ -3,6 +3,9 @@ package me.zhyd.oauth.request;
|
||||
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.exception.AuthException;
|
||||
import me.zhyd.oauth.utils.GlobalAuthUtils;
|
||||
|
||||
/**
|
||||
* 微软中国登录(世纪华联)
|
||||
@ -20,4 +23,14 @@ public class AuthMicrosoftCnRequest extends AbstractAuthMicrosoftRequest {
|
||||
super(config, AuthDefaultSource.MICROSOFT_CN, authStateCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkConfig(AuthConfig config) {
|
||||
super.checkConfig(config);
|
||||
// 微软中国的回调地址必须为https的链接或者localhost,不允许使用http
|
||||
if (AuthDefaultSource.MICROSOFT_CN == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(config.getRedirectUri())) {
|
||||
// Microsoft's redirect uri must use the HTTPS or localhost
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,6 +3,9 @@ package me.zhyd.oauth.request;
|
||||
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.exception.AuthException;
|
||||
import me.zhyd.oauth.utils.GlobalAuthUtils;
|
||||
|
||||
/**
|
||||
* 微软登录
|
||||
@ -21,4 +24,14 @@ public class AuthMicrosoftRequest extends AbstractAuthMicrosoftRequest {
|
||||
super(config, AuthDefaultSource.MICROSOFT, authStateCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkConfig(AuthConfig config) {
|
||||
super.checkConfig(config);
|
||||
// 微软的回调地址必须为https的链接或者localhost,不允许使用http
|
||||
if (AuthDefaultSource.MICROSOFT == source && !GlobalAuthUtils.isHttpsProtocolOrLocalHost(config.getRedirectUri())) {
|
||||
// Microsoft's redirect uri must use the HTTPS or localhost
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -63,21 +63,6 @@ public class AuthChecker {
|
||||
if (!GlobalAuthUtils.isHttpProtocol(redirectUri) && !GlobalAuthUtils.isHttpsProtocol(redirectUri)) {
|
||||
throw new AuthException(AuthResponseStatus.ILLEGAL_REDIRECT_URI, source);
|
||||
}
|
||||
// facebook的回调地址必须为https的链接
|
||||
if (AuthDefaultSource.FACEBOOK == source && !GlobalAuthUtils.isHttpsProtocol(redirectUri)) {
|
||||
// Facebook's redirect uri must use the HTTPS protocol
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user