google oauth2 적용
This commit is contained in:
parent
544a653f47
commit
54d76e6eb4
|
|
@ -43,6 +43,7 @@ dependencies {
|
|||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||
|
||||
// ---- OpenAPI / Swagger UI ----
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class AuthenticationService {
|
|||
private final AuthCookieService authCookieService;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
// ===================== 로그인 =====================
|
||||
// ===================== 로그인 (비밀번호) =====================
|
||||
|
||||
public AuthenticationResponse authenticate(
|
||||
AuthenticationRequest request,
|
||||
|
|
@ -55,6 +55,16 @@ public class AuthenticationService {
|
|||
throw new InvalidPasswordException("Invalid password");
|
||||
}
|
||||
|
||||
// 발급 로직은 공용 메서드로 위임 (Google 로그인과 동일 경로)
|
||||
return issueTokens(employee, response);
|
||||
}
|
||||
|
||||
// ===================== 토큰 발급 (공용) =====================
|
||||
// 비밀번호 로그인 / Google(OAuth2) 로그인 모두 이 메서드로 토큰을 발급한다.
|
||||
// 호출자는 Employee 를 확보한 뒤 이 메서드를 부른다.
|
||||
|
||||
public AuthenticationResponse issueTokens(Employee employee, HttpServletResponse response) {
|
||||
|
||||
List<EmployeeRole> activeRoles =
|
||||
employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
|
||||
|
||||
|
|
@ -65,11 +75,9 @@ public class AuthenticationService {
|
|||
String refreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
|
||||
|
||||
revokeAllEmployeeTokens(employee);
|
||||
// refresh 도 저장
|
||||
saveEmployeeToken(employee, accessToken, TokenType.ACCESS);
|
||||
saveEmployeeToken(employee, refreshToken, TokenType.REFRESH);
|
||||
|
||||
|
||||
// Cookie로 내려줌
|
||||
authCookieService.addAuthCookie(response, accessToken);
|
||||
authCookieService.addRefreshCookie(response, refreshToken);
|
||||
|
|
@ -118,7 +126,6 @@ public class AuthenticationService {
|
|||
List<String> roles = extractRoles(activeRoles);
|
||||
List<String> permissions = extractPermissions(activeRoles);
|
||||
|
||||
|
||||
String newAccessToken = jwtService.generateToken(employee, roles, permissions);
|
||||
String newRefreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
|
|
@ -21,6 +23,12 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|||
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
|
||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||
|
||||
import com.goi.erp.oauth.HostedDomainAuthorizationRequestResolver;
|
||||
import com.goi.erp.oauth.HttpCookieOAuth2AuthorizationRequestRepository;
|
||||
import com.goi.erp.oauth.OAuth2LoginFailureHandler;
|
||||
import com.goi.erp.oauth.OAuth2LoginSuccessHandler;
|
||||
import com.goi.erp.oauth.OAuth2Properties;
|
||||
|
||||
//import static com.goi.erp.user.Permission.ADMIN_CREATE;
|
||||
//import static com.goi.erp.user.Permission.ADMIN_DELETE;
|
||||
//import static com.goi.erp.user.Permission.ADMIN_READ;
|
||||
|
|
@ -45,8 +53,11 @@ import java.util.List;
|
|||
@EnableMethodSecurity
|
||||
public class SecurityConfiguration {
|
||||
|
||||
// NOTE: "/auth/**" 를 통째로 permitAll 하면 /auth/me 도 인증 없이 통과되어
|
||||
// (access 토큰 만료 시) 200 {authenticated:false} 를 돌려주고, 프론트의
|
||||
// 401-기반 refresh 가 트리거되지 않는다. 공개가 필요한 auth 엔드포인트만
|
||||
// 아래 authorizeHttpRequests 에서 개별적으로 permitAll 한다.
|
||||
private static final String[] WHITE_LIST_URL = {
|
||||
"/auth/**",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/v3/api-docs/**",
|
||||
|
|
@ -61,6 +72,13 @@ public class SecurityConfiguration {
|
|||
private final AuthenticationProvider authenticationProvider;
|
||||
private final LogoutHandler logoutHandler;
|
||||
|
||||
// ---- OAuth2 (Google Workspace) ----
|
||||
private final HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository;
|
||||
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
|
||||
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
private final OAuth2Properties oAuth2Properties;
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
|
@ -83,6 +101,13 @@ public class SecurityConfiguration {
|
|||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
// hd 파라미터를 주입하는 커스텀 인가요청 리졸버
|
||||
OAuth2AuthorizationRequestResolver authorizationRequestResolver =
|
||||
new HostedDomainAuthorizationRequestResolver(
|
||||
clientRegistrationRepository,
|
||||
oAuth2Properties.getAllowedDomain());
|
||||
|
||||
http
|
||||
.cors(Customizer.withDefaults())
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
|
@ -90,6 +115,8 @@ public class SecurityConfiguration {
|
|||
req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
|
||||
.requestMatchers("/auth/login", "/auth/token/refresh", "/auth/login/system").permitAll()
|
||||
// OAuth2 로그인 엔드포인트 (인가 시작 + Google 콜백)
|
||||
.requestMatchers("/oauth2/**", "/login/oauth2/**").permitAll()
|
||||
.requestMatchers("/auth/logout").authenticated()
|
||||
.requestMatchers(WHITE_LIST_URL)
|
||||
.permitAll()
|
||||
|
|
@ -101,6 +128,26 @@ public class SecurityConfiguration {
|
|||
.anyRequest()
|
||||
.authenticated()
|
||||
)
|
||||
// OAuth2 로그인 (Google Workspace). SPA 가 명시적으로 /oauth2/authorization/google 로
|
||||
// 보낼 때만 시작된다. 보호 리소스 접근 시 자동 로그인 리다이렉트는 아래 401 entryPoint 가
|
||||
// 우선하므로 일어나지 않는다(= API/SPA 는 401 받고 refresh 로직이 동작).
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.authorizationEndpoint(authz -> authz
|
||||
.baseUri("/oauth2/authorization")
|
||||
.authorizationRequestRepository(cookieAuthorizationRequestRepository)
|
||||
.authorizationRequestResolver(authorizationRequestResolver)
|
||||
)
|
||||
.redirectionEndpoint(redir -> redir
|
||||
.baseUri("/login/oauth2/code/*")
|
||||
)
|
||||
.successHandler(oAuth2LoginSuccessHandler)
|
||||
.failureHandler(oAuth2LoginFailureHandler)
|
||||
)
|
||||
// 미인증 요청은 403이 아니라 401 로 응답한다.
|
||||
// (Spring Security 기본값은 보통 403 이라 프론트의 401-기반 refresh 가 안 걸린다)
|
||||
.exceptionHandling(ex -> ex.authenticationEntryPoint(
|
||||
(request, resp, authException) ->
|
||||
resp.setStatus(jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED)))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
|
||||
.authenticationProvider(authenticationProvider)
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package com.goi.erp.oauth;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Google 인가 요청에 hd(hosted domain) 파라미터를 붙인다.
|
||||
*
|
||||
* - 회사 도메인(greenoilinc.com) 계정으로 로그인 흐름을 유도하고 계정 선택 화면을 줄여준다.
|
||||
* - 단, hd 파라미터는 "힌트"일 뿐 보안 경계가 아니다. 실제 도메인 강제는
|
||||
* 성공 핸들러에서 ID 토큰의 hd 클레임을 검증하는 것으로 한다(Step 3).
|
||||
*/
|
||||
public class HostedDomainAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
|
||||
|
||||
private final DefaultOAuth2AuthorizationRequestResolver defaultResolver;
|
||||
private final String hostedDomain;
|
||||
|
||||
public HostedDomainAuthorizationRequestResolver(ClientRegistrationRepository repo, String hostedDomain) {
|
||||
// 기본 인가 엔드포인트 base-uri 와 동일하게 맞춤
|
||||
this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization");
|
||||
this.hostedDomain = hostedDomain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
|
||||
return customize(defaultResolver.resolve(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
|
||||
return customize(defaultResolver.resolve(request, clientRegistrationId));
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationRequest customize(OAuth2AuthorizationRequest req) {
|
||||
if (req == null) {
|
||||
return null;
|
||||
}
|
||||
if (hostedDomain == null || hostedDomain.isBlank()) {
|
||||
return req;
|
||||
}
|
||||
Map<String, Object> extra = new HashMap<>(req.getAdditionalParameters());
|
||||
extra.put("hd", hostedDomain);
|
||||
return OAuth2AuthorizationRequest.from(req)
|
||||
.additionalParameters(extra)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package com.goi.erp.oauth;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputFilter;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 세션 대신 쿠키에 OAuth2AuthorizationRequest 를 저장한다.
|
||||
*
|
||||
* 앱이 SessionCreationPolicy.STATELESS 라 기본 세션 기반 저장소를 못 쓰기 때문에 필요.
|
||||
* - 인가 요청 → Google 리다이렉트 시 쿠키에 저장
|
||||
* - Google → 콜백 복귀 시 쿠키에서 복원 후 삭제
|
||||
*
|
||||
* 쿠키는 httpOnly + 짧은 수명(180s) + SameSite=Lax.
|
||||
* (Google 에서 콜백으로 돌아오는 top-level GET 내비게이션에 쿠키가 실리려면 Lax 여야 함. Strict 면 안 됨)
|
||||
* 역직렬화는 ObjectInputFilter 로 허용 클래스를 제한해 gadget 공격을 차단한다.
|
||||
*/
|
||||
@Component
|
||||
public class HttpCookieOAuth2AuthorizationRequestRepository
|
||||
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
|
||||
|
||||
public static final String OAUTH2_AUTH_REQUEST_COOKIE = "OAUTH2_AUTH_REQUEST";
|
||||
private static final int COOKIE_EXPIRE_SECONDS = 180;
|
||||
|
||||
@Value("${auth.cookie.secure:true}")
|
||||
private boolean secure;
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
|
||||
return getCookie(request, OAUTH2_AUTH_REQUEST_COOKIE)
|
||||
.map(c -> deserialize(c.getValue()))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
if (authorizationRequest == null) {
|
||||
deleteCookie(response, OAUTH2_AUTH_REQUEST_COOKIE);
|
||||
return;
|
||||
}
|
||||
addCookie(response, OAUTH2_AUTH_REQUEST_COOKIE,
|
||||
serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
OAuth2AuthorizationRequest authRequest = loadAuthorizationRequest(request);
|
||||
if (authRequest != null) {
|
||||
deleteCookie(response, OAUTH2_AUTH_REQUEST_COOKIE);
|
||||
}
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
// ===================== 직렬화 =====================
|
||||
|
||||
private static String serialize(OAuth2AuthorizationRequest obj) {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
|
||||
oos.writeObject(obj);
|
||||
oos.flush();
|
||||
return Base64.getUrlEncoder().encodeToString(bos.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to serialize OAuth2AuthorizationRequest", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationRequest deserialize(String value) {
|
||||
try {
|
||||
byte[] data = Base64.getUrlDecoder().decode(value);
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
|
||||
ois.setObjectInputFilter(SAFE_FILTER);
|
||||
return (OAuth2AuthorizationRequest) ois.readObject();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 위변조/손상 쿠키 → 인가요청 없음으로 처리
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** OAuth2AuthorizationRequest 와 그 필드(표준 java + spring-security-oauth2)만 허용 */
|
||||
private static final ObjectInputFilter SAFE_FILTER = info -> {
|
||||
Class<?> clazz = info.serialClass();
|
||||
if (clazz == null) {
|
||||
return ObjectInputFilter.Status.UNDECIDED;
|
||||
}
|
||||
String name = clazz.getName();
|
||||
if (name.startsWith("org.springframework.security.oauth2.")
|
||||
|| name.startsWith("java.")
|
||||
|| name.startsWith("[Ljava.")
|
||||
|| name.startsWith("[B")) {
|
||||
return ObjectInputFilter.Status.ALLOWED;
|
||||
}
|
||||
return ObjectInputFilter.Status.REJECTED;
|
||||
};
|
||||
|
||||
// ===================== 쿠키 헬퍼 =====================
|
||||
|
||||
private void addCookie(HttpServletResponse response, String name, String value, int maxAgeSeconds) {
|
||||
ResponseCookie cookie = ResponseCookie.from(name, value)
|
||||
.httpOnly(true)
|
||||
.secure(secure)
|
||||
.path("/")
|
||||
.sameSite("Lax")
|
||||
.maxAge(maxAgeSeconds)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
}
|
||||
|
||||
private void deleteCookie(HttpServletResponse response, String name) {
|
||||
ResponseCookie cookie = ResponseCookie.from(name, "")
|
||||
.httpOnly(true)
|
||||
.secure(secure)
|
||||
.path("/")
|
||||
.sameSite("Lax")
|
||||
.maxAge(0)
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
}
|
||||
|
||||
private Optional<Cookie> getCookie(HttpServletRequest request, String name) {
|
||||
if (request.getCookies() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Arrays.stream(request.getCookies())
|
||||
.filter(c -> name.equals(c.getName()))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.goi.erp.oauth;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* OAuth2 로그인 실패 시 401 + 에러 메시지(JSON).
|
||||
* 기본 핸들러는 /login?error 로 리다이렉트하는데 그런 화면이 없으므로 명시적으로 처리.
|
||||
*/
|
||||
@Component
|
||||
public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
String msg = exception.getMessage() == null ? "" : exception.getMessage().replace("\"", "'");
|
||||
response.getWriter().write(
|
||||
"{\"error\":\"oauth2_login_failed\",\"message\":\"" + msg + "\"}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.goi.erp.oauth;
|
||||
|
||||
import com.goi.erp.auth.AuthenticationService;
|
||||
import com.goi.erp.employee.Employee;
|
||||
import com.goi.erp.employee.EmployeeRepository;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Google(OAuth2) 로그인 성공 후처리.
|
||||
*
|
||||
* 1) hd 클레임으로 도메인 검증 (greenoilinc.com)
|
||||
* 2) 이메일(emp_login_id)로 Employee 조회
|
||||
* 3) 활성 상태 확인
|
||||
* 4) 기존 발급 경로(AuthenticationService.issueTokens) 재사용 → JWT + 쿠키
|
||||
* 5) SPA(success-redirect)로 리다이렉트
|
||||
*
|
||||
* 실패 케이스는 SPA 로 ?login_error=코드 를 붙여 리다이렉트한다.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
private final OAuth2Properties oAuth2Properties;
|
||||
private final EmployeeRepository employeeRepository;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Authentication authentication) throws IOException {
|
||||
|
||||
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
|
||||
String email = oidcUser.getEmail();
|
||||
String hd = (String) oidcUser.getClaims().get("hd");
|
||||
|
||||
// 1) 도메인 검증
|
||||
String allowed = oAuth2Properties.getAllowedDomain();
|
||||
if (allowed != null && !allowed.isBlank() && !allowed.equalsIgnoreCase(hd)) {
|
||||
redirectError(response, "domain_not_allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 이메일로 Employee 조회 (emp_login_id == Google Workspace 이메일)
|
||||
Employee employee = employeeRepository.findByEmpLoginId(email).orElse(null);
|
||||
if (employee == null) {
|
||||
redirectError(response, "not_registered");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) 활성 상태 확인 ('A' = Active)
|
||||
if (!"A".equals(employee.getEmpStatus())) {
|
||||
redirectError(response, "inactive");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) 기존 발급 경로 재사용 (비밀번호 로그인과 동일): JWT 발급 + AUTH/REFRESH 쿠키 세팅
|
||||
authenticationService.issueTokens(employee, response);
|
||||
|
||||
// 5) SPA 로 리다이렉트 (쿠키는 위 302 응답에 함께 실려 나간다)
|
||||
response.sendRedirect(oAuth2Properties.getSuccessRedirect());
|
||||
}
|
||||
|
||||
private void redirectError(HttpServletResponse response, String code) throws IOException {
|
||||
String base = oAuth2Properties.getSuccessRedirect();
|
||||
String sep = base.contains("?") ? "&" : "?";
|
||||
response.sendRedirect(base + sep + "login_error="
|
||||
+ URLEncoder.encode(code, StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.goi.erp.oauth;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* application.yml 의 app.oauth2.* 바인딩.
|
||||
*
|
||||
* app:
|
||||
* oauth2:
|
||||
* allowed-domain: greenoilinc.com
|
||||
* success-redirect: https://portal.greenoilinc.com
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.oauth2")
|
||||
public class OAuth2Properties {
|
||||
|
||||
/** 로그인 허용 Google Workspace 도메인 (hd 클레임 검증 + hd 파라미터) */
|
||||
private String allowedDomain;
|
||||
|
||||
/** 로그인 성공 후 SPA 로 돌려보낼 주소 (Step 3에서 사용) */
|
||||
private String successRedirect;
|
||||
}
|
||||
|
|
@ -13,7 +13,19 @@ spring:
|
|||
format_sql: true
|
||||
database: postgresql
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
google:
|
||||
client-id: ${GOOGLE_CLIENT_ID}
|
||||
client-secret: ${GOOGLE_CLIENT_SECRET}
|
||||
scope:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
redirect-uri: "{baseUrl}/login/oauth2/code/google"
|
||||
# provider 'google'은 Spring Security 내장이라 엔드포인트 설정 불필요
|
||||
application:
|
||||
security:
|
||||
jwt:
|
||||
|
|
@ -27,6 +39,11 @@ application:
|
|||
secret: ${OPR_SYSTEM_CLIENT_SECRET}
|
||||
permissions:
|
||||
- "H:R:A"
|
||||
app:
|
||||
oauth2:
|
||||
allowed-domain: greenoilinc.com
|
||||
# 로그인 성공 후 SPA로 돌려보낼 주소
|
||||
success-redirect: https://portal.greenoilinc.com
|
||||
auth:
|
||||
cookie:
|
||||
secure: true # prod: true / local: false
|
||||
|
|
|
|||
Loading…
Reference in New Issue