貼了幾次都切掉,只好補充另一篇
前一篇 URL: (https://ithelp.ithome.com.tw/articles/10398837/)
package tw.lewishome.webapp.base.security.oauth2;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
import java.util.Map;
import java.io.Serializable;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.security.SecurityConstants;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.database.primary.service.SysUserProfileService;
/**
* OAuth2SuccessHandler 處理 OAuth2.0 登入成功的後續邏輯。
*
* 此元件在使用者通過 OAuth2.0 認證後被 Spring Security 呼叫,可實現:
* <ul>
* <li>記錄登入稽核日誌</li>
* <li>同步 OAuth2 使用者資訊至系統資料庫</li>
* <li>簽發應用自訂的 JWT token</li>
* <li>轉導至指定的成功頁面或 REST 端點</li>
* <li>設定 session 或 cookie 相關資訊</li>
* </ul>
*
* 主要職責:
* <ul>
* <li>在 OAuth2.0 成功後處理 SysUserProfile 的建立/更新</li>
* <li>可選:簽發應用 JWT(若不依賴 OAuth2 provider 的 token)</li>
* <li>記錄登入成功事件(可整合 LoginSuccessLogger)</li>
* <li>決定轉導的目標 URL(首頁、個人頁面、API token 端點等)</li>
* </ul>
*
* 使用場景:
* <ul>
* <li>OAuth2.0 社群登入後建立或更新本地使用者</li>
* <li>簽發 JWT 供 REST API 使用</li>
* <li>跨域登入場景中的 token 傳遞</li>
* </ul>
*
* @author Lewis
* @since 2024
*/
@Component
@Slf4j
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
@Autowired(required = false)
private OAuth2JwtConverter oauth2JwtConverter;
@Autowired(required = false)
SystemEnvReader systemEnvReader;
/**
* Fix for javadoc warning: default constructor
* Constructs a new OAuth2SuccessHandler instance.
*/
public OAuth2SuccessHandler() {
// Default constructor
}
@Autowired(required = false)
private SysUserProfileService sysUserProfileService;
/**
* 在 OAuth2.0 認證成功後被呼叫的方法
*
* @param request
* HTTP 請求
* @param response
* HTTP 回應
* @param authentication
* OAuth2AuthenticationToken(包含使用者資訊)
* @throws IOException
* 若 I/O 錯誤發生
* @throws ServletException
* 若 Servlet 錯誤發生
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (!(authentication instanceof OAuth2AuthenticationToken)) {
log.warn(
"[OAuth2SuccessHandler] onAuthenticationSuccess - Authentication is NOT OAuth2AuthenticationToken, redirecting to default page");
redirectToDefaultPage(response);
return;
}
try {
OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication;
OAuth2User principal = oauth2Token.getPrincipal();
OAuth2User usedPrincipal = principal;
// 記錄登入成功
logLoginSuccess(oauth2Token, usedPrincipal);
// 同步 OAuth2 使用者資訊至系統(可選)
syncUserProfile(oauth2Token, usedPrincipal);
// 決定轉導 URL(可以是首頁、API token 端點或其他)
String redirectUrl = determineTargetUrl(request, oauth2Token);
response.sendRedirect(redirectUrl);
} catch (Exception ex) {
log.error("Error in OAuth2SuccessHandler", ex);
redirectToErrorPage(response);
}
}
/**
* 記錄 OAuth2 登入成功事件
*
* @param oauth2Token
* OAuth2AuthenticationToken
* @param principal
* OAuth2User
*/
private void logLoginSuccess(OAuth2AuthenticationToken oauth2Token, OAuth2User principal) {
String username = principal.getName();
String registrationId = oauth2Token.getAuthorizedClientRegistrationId();
String email = oauth2JwtConverter != null ? oauth2JwtConverter.extractEmail(principal) : null;
log.info("OAuth2 login success - registrationId: {}, username: {}, email: {}",
registrationId, username, email);
// 若有 LoginSuccessLogger bean,可在此呼叫以記錄稽核日誌
// loginSuccessLogger.logLoginSuccess(username, "OAuth2:" + registrationId);
}
/**
* 同步 OAuth2 使用者資訊至系統資料庫(SysUserProfile)
*
* @param oauth2Token
* OAuth2AuthenticationToken
* @param principal
* OAuth2User
*/
private void syncUserProfile(OAuth2AuthenticationToken oauth2Token, OAuth2User principal) {
if (sysUserProfileService == null) {
log.debug("SysUserProfileService is not available, skipping user profile sync");
return;
}
// 取得 OAuth2 提供者 ID
String registrationId = oauth2Token.getAuthorizedClientRegistrationId();
try {
// 呼叫服務方法同步使用者資訊
sysUserProfileService.syncOAuth2User(principal, registrationId);
log.debug("OAuth2 user profile synced successfully for provider: {}", registrationId);
} catch (Exception ex) {
log.error("Failed to sync OAuth2 user profile", ex);
// 不中斷登入流程,只記錄錯誤
}
}
/**
* 決定成功登入後的轉導 URL
*
* @param request
* HTTP 請求
* @param oauth2Token
* OAuth2AuthenticationToken
* @return 轉導目標 URL
*/
private String determineTargetUrl(HttpServletRequest request, OAuth2AuthenticationToken oauth2Token) {
// 檢查是否有 redirect_uri parameter
String redirectUri = request.getParameter("redirect_uri");
if (redirectUri != null && isValidRedirectUri(redirectUri)) {
return redirectUri;
}
// 檢查 session 中是否有保存的 target URL
String targetUrl = (String) request.getSession().getAttribute("oauth2_target_url");
if (targetUrl != null) {
request.getSession().removeAttribute("oauth2_target_url");
return targetUrl;
}
// 預設轉導至首頁(NOT /callback to avoid infinite loop)
// 這裡不能是 /callback,會造成無限迴圈
return "/landing";
}
/**
* 驗證轉導 URI 是否合法(防止開放重導向)
*
* @param redirectUri
* 待驗證的 URI
* @return true 若為合法的相對路徑或白名單 URI
*/
private boolean isValidRedirectUri(String redirectUri) {
// 簡單的驗證:只允許相對路徑或白名單域名
if (redirectUri.startsWith("/")) {
// 相對路徑,合法
return !redirectUri.contains("//");
}
// 可在此加入白名單檢查
// return whitelist.contains(redirectUri);
return false;
}
/**
* 轉導至預設頁面(通常是首頁)
*
* @param response
* HTTP 回應
* @throws IOException
* 若 I/O 錯誤發生
*/
private void redirectToDefaultPage(HttpServletResponse response) throws IOException {
String loginPageUrl = systemEnvReader.getProperty("LOGIN_PAGE_URL",SecurityConstants.LOGIN_PAGE_URL);
response.sendRedirect(loginPageUrl);
}
/**
* 轉導至錯誤頁面
*
* @param response
* HTTP 回應
* @throws IOException
* 若 I/O 錯誤發生
*/
private void redirectToErrorPage(HttpServletResponse response) throws IOException {
String loginPageUrl = systemEnvReader.getProperty("LOGIN_PAGE_URL",SecurityConstants.LOGIN_PAGE_URL);
response.sendRedirect(loginPageUrl +"?error");
}
/**
* Serializable OAuth2User wrapper that returns email as name.
*/
public static class EmailPrincipal implements OAuth2User, Serializable {
private static final long serialVersionUID = 1L;
private final Map<String, Object> attributes;
private final Collection<? extends GrantedAuthority> authorities;
private final String name;
/**
* Constructor
* @param attributes
* @param authorities
* @param name
*/
public EmailPrincipal(Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities,
String name) {
this.attributes = attributes;
this.authorities = authorities;
this.name = name;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getName() {
return name;
}
}
}