提交 132a7f43 编写于 作者: 智布道's avatar 智布道 👁

🥚 添加 amazon 平台

上级 c79b97a0
...@@ -131,4 +131,11 @@ public class AuthConfig { ...@@ -131,4 +131,11 @@ public class AuthConfig {
* @since 1.15.9 * @since 1.15.9
*/ */
private String packId; private String packId;
/**
* 是否开启 PKCE 模式,该配置仅用于支持 PKCE 模式的平台,针对无服务应用,不推荐使用隐式授权,推荐使用 PKCE 模式
*
* @since 1.16.0
*/
private boolean pkce;
} }
...@@ -832,5 +832,32 @@ public enum AuthDefaultSource implements AuthSource { ...@@ -832,5 +832,32 @@ public enum AuthDefaultSource implements AuthSource {
public String refresh() { public String refresh() {
return "https://oauth.aliyun.com/v1/token"; return "https://oauth.aliyun.com/v1/token";
} }
},
/**
* Amazon
*
* @since 1.16.0
*/
AMAZON {
@Override
public String authorize() {
return "https://www.amazon.com/ap/oa";
}
@Override
public String accessToken() {
return "https://api.amazon.com/auth/o2/token";
}
@Override
public String userInfo() {
return "https://api.amazon.com/user/profile";
}
@Override
public String refresh() {
return "https://api.amazon.com/auth/o2/token";
}
} }
} }
package me.zhyd.oauth.enums.scope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Amazon平台 OAuth 授权范围
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @since 1.16.0
*/
@Getter
@AllArgsConstructor
public enum AuthAmazonScope implements AuthScope {
/**
* {@code scope} 含义,以{@code description} 为准
*/
R_LITEPROFILE("profile", "The profile scope includes a user's name and email address", true),
R_EMAILADDRESS("profile:user_id", "The profile:user_id scope only includes the user_id field of the profile", true),
W_MEMBER_SOCIAL("postal_code", "This includes the user's zip/postal code number from their primary shipping address", true);
private final String scope;
private final String description;
private final boolean isDefault;
}
package me.zhyd.oauth.request;
import com.alibaba.fastjson.JSONObject;
import com.xkcoding.http.constants.Constants;
import com.xkcoding.http.support.HttpHeader;
import com.xkcoding.http.util.UrlUtil;
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.AuthAmazonScope;
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.PkceUtil;
import me.zhyd.oauth.utils.UrlBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Amazon登录
* Login with Amazon for Websites Overview: https://developer.amazon.com/zh/docs/login-with-amazon/register-web.html
* Login with Amazon SDK for JavaScript Reference Guide:https://developer.amazon.com/zh/docs/login-with-amazon/javascript-sdk-reference.html
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.16.0
*/
public class AuthAmazonRequest extends AuthDefaultRequest {
public AuthAmazonRequest(AuthConfig config) {
super(config, AuthDefaultSource.AMAZON);
}
public AuthAmazonRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.AMAZON, authStateCache);
}
/**
* https://developer.amazon.com/zh/docs/login-with-amazon/authorization-code-grant.html#authorization-request
*
* @param state state 验证授权流程的参数,可以防止csrf
* @return String
*/
@Override
public String authorize(String state) {
UrlBuilder builder = UrlBuilder.fromBaseUrl(source.authorize())
.queryParam("client_id", config.getClientId())
.queryParam("scope", this.getScopes(" ", true, AuthScopeUtils.getDefaultScopes(AuthAmazonScope.values())))
.queryParam("redirect_uri", config.getRedirectUri())
.queryParam("response_type", "code")
.queryParam("state", getRealState(state));
if (config.isPkce()) {
String cacheKey = this.source.getName().concat(":code_verifier:").concat(config.getClientId());
String codeVerifier = PkceUtil.generateCodeVerifier();
String codeChallengeMethod = "S256";
String codeChallenge = PkceUtil.generateCodeChallenge(codeChallengeMethod, codeVerifier);
builder.queryParam("code_challenge", codeChallenge)
.queryParam("code_challenge_method", codeChallengeMethod);
// 缓存 codeVerifier 十分钟
this.authStateCache.cache(cacheKey, codeVerifier, TimeUnit.MINUTES.toMillis(10));
}
return builder.build();
}
/**
* https://developer.amazon.com/zh/docs/login-with-amazon/authorization-code-grant.html#access-token-request
*
* @return access token
*/
@Override
protected AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> form = new HashMap<>(8);
form.put("grant_type", "authorization_code");
form.put("code", authCallback.getCode());
form.put("redirect_uri", config.getRedirectUri());
form.put("client_id", config.getClientId());
form.put("client_secret", config.getClientSecret());
if (config.isPkce()) {
String cacheKey = this.source.getName().concat(":code_verifier:").concat(config.getClientId());
String codeVerifier = this.authStateCache.get(cacheKey);
form.put("code_verifier", codeVerifier);
}
return getToken(form, this.source.accessToken());
}
@Override
public AuthResponse refresh(AuthToken authToken) {
Map<String, String> form = new HashMap<>(6);
form.put("grant_type", "refresh_token");
form.put("refresh_token", authToken.getRefreshToken());
form.put("client_id", config.getClientId());
form.put("client_secret", config.getClientSecret());
return AuthResponse.builder()
.code(AuthResponseStatus.SUCCESS.getCode())
.data(getToken(form, this.source.refresh()))
.build();
}
private AuthToken getToken(Map<String, String> param, String url) {
HttpHeader httpHeader = new HttpHeader();
httpHeader.add("Host", "api.amazon.com");
httpHeader.add(Constants.CONTENT_TYPE, "application/x-www-form-urlencoded;charset=UTF-8");
String response = new HttpUtils(config.getHttpConfig()).post(url, param, httpHeader, false);
JSONObject jsonObject = JSONObject.parseObject(response);
this.checkResponse(jsonObject);
return AuthToken.builder()
.accessToken(jsonObject.getString("access_token"))
.tokenType(jsonObject.getString("token_type"))
.expireIn(jsonObject.getIntValue("expires_in"))
.refreshToken(jsonObject.getString("refresh_token"))
.build();
}
/**
* 校验响应内容是否正确
*
* @param jsonObject 响应内容
*/
private void checkResponse(JSONObject jsonObject) {
if (jsonObject.containsKey("error")) {
throw new AuthException(jsonObject.getString("error_description").concat(" ") + jsonObject.getString("error_description"));
}
}
/**
* https://developer.amazon.com/zh/docs/login-with-amazon/obtain-customer-profile.html#call-profile-endpoint
*
* @param authToken token信息
* @return AuthUser
*/
@Override
protected AuthUser getUserInfo(AuthToken authToken) {
String accessToken = authToken.getAccessToken();
this.checkToken(accessToken);
HttpHeader httpHeader = new HttpHeader();
httpHeader.add("Host", "api.amazon.com");
httpHeader.add("Authorization", "bearer " + accessToken);
String userInfo = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), new HashMap<>(0), httpHeader, false);
JSONObject jsonObject = JSONObject.parseObject(userInfo);
this.checkResponse(jsonObject);
return AuthUser.builder()
.rawUserInfo(jsonObject)
.uuid(jsonObject.getString("user_id"))
.username(jsonObject.getString("name"))
.nickname(jsonObject.getString("name"))
.email(jsonObject.getString("email"))
.gender(AuthUserGender.UNKNOWN)
.source(source.toString())
.token(authToken)
.build();
}
private void checkToken(String accessToken) {
String tokenInfo = new HttpUtils(config.getHttpConfig()).get("https://api.amazon.com/auth/o2/tokeninfo?access_token=" + UrlUtil.urlEncode(accessToken));
JSONObject jsonObject = JSONObject.parseObject(tokenInfo);
if (!config.getClientId().equals(jsonObject.getString("aud"))) {
throw new AuthException(AuthResponseStatus.ILLEGAL_TOKEN);
}
}
@Override
protected String userInfoUrl(AuthToken authToken) {
return UrlBuilder.fromBaseUrl(source.userInfo())
.queryParam("user_id", authToken.getUserId())
.queryParam("screen_name", authToken.getScreenName())
.queryParam("include_entities", true)
.build();
}
}
package me.zhyd.oauth.utils;
import java.nio.charset.StandardCharsets;
/**
* 该配置仅用于支持 PKCE 模式的平台,针对无服务应用,不推荐使用隐式授权,推荐使用 PKCE 模式
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @since 1.0.0
*/
public class PkceUtil {
public static String generateCodeVerifier() {
String randomStr = RandomUtil.randomString(50);
return Base64Utils.encodeUrlSafe(randomStr);
}
/**
* 适用于 OAuth 2.0 PKCE 增强协议
*
* @param codeChallengeMethod s256 / plain
* @param codeVerifier 客户端生产的校验码
* @return code challenge
*/
public static String generateCodeChallenge(String codeChallengeMethod, String codeVerifier) {
if ("S256".equalsIgnoreCase(codeChallengeMethod)) {
// https://tools.ietf.org/html/rfc7636#section-4.2
// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
return newStringUsAscii(Base64Utils.encodeUrlSafe(Sha256.digest(codeVerifier), true));
} else {
return codeVerifier;
}
}
public static String newStringUsAscii(byte[] bytes) {
return new String(bytes, StandardCharsets.US_ASCII);
}
}
package me.zhyd.oauth.utils;
import java.util.concurrent.ThreadLocalRandom;
/**
* 生成随机字符串
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @since 1.16.0
*/
public class RandomUtil {
/**
* 用于随机选的字符和数字
*/
public static final String BASE_CHAR_NUMBER = "abcdefghijklmnopqrstuvwxyz0123456789";
/**
* 获得一个随机的字符串
*
* @param length 字符串的长度
* @return 指定长度的随机字符串
*/
public static String randomString(int length) {
final StringBuilder sb = new StringBuilder(length);
if (length < 1) {
length = 1;
}
int baseLength = BASE_CHAR_NUMBER.length();
for (int i = 0; i < length; i++) {
int number = ThreadLocalRandom.current().nextInt(baseLength);
sb.append(BASE_CHAR_NUMBER.charAt(number));
}
return sb.toString();
}
}
package me.zhyd.oauth.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* SHA256 加密
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @version 1.0.0
* @since 1.16.0
*/
public class Sha256 {
public static byte[] digest(String str) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
return messageDigest.digest();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册