|
|
@@ -15,6 +15,10 @@ import com.ruoyi.common.utils.MessageUtils;
|
|
|
import org.apache.http.client.config.RequestConfig;
|
|
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
|
|
import org.apache.http.client.methods.HttpGet;
|
|
|
+import org.apache.http.client.methods.HttpPost;
|
|
|
+import org.apache.http.client.entity.UrlEncodedFormEntity;
|
|
|
+import org.apache.http.message.BasicNameValuePair;
|
|
|
+import org.apache.http.NameValuePair;
|
|
|
import org.apache.http.impl.client.CloseableHttpClient;
|
|
|
import org.apache.http.impl.client.HttpClients;
|
|
|
import org.apache.http.util.EntityUtils;
|
|
|
@@ -25,6 +29,7 @@ import org.springframework.stereotype.Service;
|
|
|
|
|
|
import java.net.URL;
|
|
|
import java.net.URLEncoder;
|
|
|
+import java.util.ArrayList;
|
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
|
|
|
|
@@ -34,7 +39,7 @@ import java.util.List;
|
|
|
* <ul>
|
|
|
* <li>Apple:nimbus 验 ES256 identityToken + 校 iss/aud/exp → sub(忽略 email)</li>
|
|
|
* <li>Google:tokeninfo HTTP 验真 + 校 audience → sub</li>
|
|
|
- * <li>LINE:v2/profile HTTP(Bearer)→ userId</li>
|
|
|
+ * <li>LINE:code 换 access_token → v2/profile → userId</li>
|
|
|
* </ul>
|
|
|
* <p>注:Google 走 tokeninfo 而非 firebase-admin,避免 FirebaseApp+服务账号初始化;各家真实 token + clientId 需联调验证。</p>
|
|
|
*
|
|
|
@@ -56,12 +61,20 @@ public class OAuthVerifyService {
|
|
|
private String googleTokeninfoUrl;
|
|
|
@Value("${oauth.line.profile-url}")
|
|
|
private String lineProfileUrl;
|
|
|
+ @Value("${oauth.line.token-url}")
|
|
|
+ private String lineTokenUrl;
|
|
|
+ @Value("${oauth.line.client-id}")
|
|
|
+ private String lineClientId;
|
|
|
+ @Value("${oauth.line.client-secret}")
|
|
|
+ private String lineClientSecret;
|
|
|
+ @Value("${oauth.line.redirect-uri}")
|
|
|
+ private String lineRedirectUri;
|
|
|
|
|
|
/**
|
|
|
* 校验 provider 凭证,返回稳定的 providerUid。
|
|
|
*
|
|
|
* @param provider apple/google/line
|
|
|
- * @param credential Apple identityToken / Google idToken / LINE accessToken
|
|
|
+ * @param credential Apple identityToken / Google idToken / LINE authorization code
|
|
|
*/
|
|
|
public String verify(String provider, String credential) {
|
|
|
if (provider == null || provider.isEmpty()) {
|
|
|
@@ -172,19 +185,43 @@ public class OAuthVerifyService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** LINE:v2/profile HTTP(Bearer accessToken),返回 userId。 */
|
|
|
- private String verifyLine(String accessToken) {
|
|
|
+ /**
|
|
|
+ * LINE:authorization code → 换 access_token → v2/profile 取 userId。
|
|
|
+ *
|
|
|
+ * <p>标准 LINE Login 流程:前端拿到授权 code 后传给后端,后端用 code + clientSecret + clientId
|
|
|
+ * 向 {@code https://api.line.me/oauth2/v2.1/token} 换 access_token(form-urlencoded),
|
|
|
+ * 再用 access_token 调 v2/profile 取稳定的 userId 作为 providerUid。
|
|
|
+ *
|
|
|
+ * @param code LINE 授权码(前端授权后获得,一次性)
|
|
|
+ * @return LINE userId
|
|
|
+ */
|
|
|
+ private String verifyLine(String code) {
|
|
|
try {
|
|
|
- log.debug("[OAuth][LINE] accessToken={}", accessToken);
|
|
|
- JSONObject json = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
|
|
|
- log.debug("[OAuth][LINE] profile 返回: {}", json);
|
|
|
- if (json == null) {
|
|
|
+ log.debug("[OAuth][LINE] code={}", code);
|
|
|
+ // 1. code 换 access_token
|
|
|
+ JSONObject token = JSONObject.parseObject(httpPostForm(lineTokenUrl, new String[][]{
|
|
|
+ {"grant_type", "authorization_code"},
|
|
|
+ {"code", code},
|
|
|
+ {"redirect_uri", lineRedirectUri},
|
|
|
+ {"client_id", lineClientId},
|
|
|
+ {"client_secret", lineClientSecret}
|
|
|
+ }));
|
|
|
+ log.debug("[OAuth][LINE] token 返回: {}", token);
|
|
|
+ if (token == null || token.getString("access_token") == null) {
|
|
|
+ log.warn("[OAuth][LINE] 换 token 失败: {}", token);
|
|
|
+ throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
|
|
|
+ }
|
|
|
+ String accessToken = token.getString("access_token");
|
|
|
+ // 2. access_token 换用户信息
|
|
|
+ JSONObject profile = JSONObject.parseObject(httpGet(lineProfileUrl, accessToken));
|
|
|
+ log.debug("[OAuth][LINE] profile 返回: {}", profile);
|
|
|
+ if (profile == null) {
|
|
|
log.warn("[OAuth][LINE] profile 返回空");
|
|
|
throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
|
|
|
}
|
|
|
- String userId = json.getString("userId");
|
|
|
+ String userId = profile.getString("userId");
|
|
|
if (userId == null || userId.isEmpty()) {
|
|
|
- log.warn("[OAuth][LINE] 无 userId: {}", json);
|
|
|
+ log.warn("[OAuth][LINE] 无 userId: {}", profile);
|
|
|
throw new ServiceException(MessageUtils.message("no.oauth.token.invalid", "LINE"));
|
|
|
}
|
|
|
log.info("[OAuth][LINE] 校验通过 userId={}", userId);
|
|
|
@@ -214,4 +251,24 @@ public class OAuthVerifyService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /** 简单 POST application/x-www-form-urlencoded(LINE 换 token 用)。带超时,防止 LINE 慢/不可达拖垮线程。 */
|
|
|
+ private String httpPostForm(String url, String[][] form) throws Exception {
|
|
|
+ try (CloseableHttpClient client = HttpClients.createDefault()) {
|
|
|
+ HttpPost post = new HttpPost(url);
|
|
|
+ post.setConfig(RequestConfig.custom()
|
|
|
+ .setConnectTimeout(5000)
|
|
|
+ .setSocketTimeout(10000)
|
|
|
+ .setConnectionRequestTimeout(5000)
|
|
|
+ .build());
|
|
|
+ List<NameValuePair> pairs = new ArrayList<>();
|
|
|
+ for (String[] kv : form) {
|
|
|
+ pairs.add(new BasicNameValuePair(kv[0], kv[1]));
|
|
|
+ }
|
|
|
+ post.setEntity(new UrlEncodedFormEntity(pairs, "UTF-8"));
|
|
|
+ try (CloseableHttpResponse resp = client.execute(post)) {
|
|
|
+ return EntityUtils.toString(resp.getEntity(), "UTF-8");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|