OAuth2는 사용자가 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는 접근 위임을 위한 개방형 표준입니다.
OAuth(Open Authorization)
쉽게 얘기하면 = 신뢰할 수 있는 다른 서비스에 로그인을 맡기는 것
OAuth는 인증을 위한 개방형 표준 프로토콜입니다.
프로세스 요약
👤Resource Owner: 사용자
🖥️Client: OAuth를 사용하는 웹/앱
🔑Authorization Server: 인증 서버 (네이버/카카오/구글의 로그인 등 인증 처리)
📦Resource Server: 리소스 서버 (사용자 정보를 가진 서버, 프로필, 이메일
👤 사용자 🖥️ 클라이언트 🔑 인증서버 📦 리소스서버
| | | |
| | | |
| 1️⃣ 로그인 버튼 | | |
| --------------> | | |
| | | |
| | 2️⃣ 인증 요청 | |
| | --------------> | |
| | | |
| 3️⃣ 로그인 페이지| | |
| <---------------------------------------- |
| | | |
| 4️⃣ 계정정보 입력| | |
| ----------------------------------------> |
| | | |
| | 5️⃣ 인증 코드 | |
| | <-------------- | |
| | | |
| | 6️⃣ 토큰 요청 | |
| | --------------> | |
| | | |
| | 7️⃣ 액세스 토큰 | |
| | <-------------- | |
| | | |
| | 8️⃣사용자정보요청| |
| | ----------------------------------> |
| | | |
| | 9️⃣ 사용자 정보 | |
| | <---------------------------------- |
| | | |
| 🔟 로그인 완료 | | |
| <-------------- | | |
구현해야할 스프링 부트 프로젝트 요약
+-------------------------------------------------------------+
| Spring Boot Application |
+-------------------------------------------------------------+
| +----------------+ +---------------+ +----------------+ |
| | SecurityConfig| | OAuth2Service| | JWTService | |
| | - 보안 설정 | | - OAuth2 처리 | | - JWT 생성/검증| |
| +----------------+ +---------------+ +----------------+ |
| |
| +----------------+ +---------------+ +----------------+ |
| | UserService | | TokenService | |RedisRepository| |
| | - 사용자 관리 | | - 토큰 관리 | | - 토큰 저장소 | |
| +----------------+ +---------------+ +----------------+ |
| |
| +----------------+ +---------------+ |
| |ExceptionHandler| |CustomFilters | |
| | - 예외 처리 | | - 필터 체인 | |
| +----------------+ +---------------+ |
+-------------------------------------------------------------+
인증 프로세스 흐름:
====================
1. 사용자 인증 요청
- 브라우저에서 /oauth2/authorization/{provider} 접근
- 소셜 로그인 페이지로 리다이렉트
2. 권한 부여 코드 발급
- 사용자가 권한 승인
- Authorization Code 발급
3. Access Token 교환
- Authorization Code로 Access Token 요청
- OAuth2 제공자가 Access Token 발급
4. 사용자 정보 조회
- Access Token으로 사용자 정보 요청
- CustomOAuth2UserService에서 사용자 정보 처리
5. 토큰 관리
- JWT 토큰 생성
- Redis에 Refresh Token 저장
- 클라이언트에 JWT 전달
주요 컴포넌트 설명:
===================
SecurityConfig: 보안 설정 관리
- URL 접근 권한 설정
- OAuth2 클라이언트 설정
- CSRF 보호 설정
OAuth2Service: OAuth2 인증 처리
- 소셜 로그인 처리
- 사용자 정보 매핑
- 인증 결과 처리
TokenService: 토큰 관리
- JWT 토큰 생성/검증
- Refresh Token 관리
- 토큰 저장소 연동
RedisRepository: 토큰 저장소
- Refresh Token 저장
- 토큰 조회/삭제
- 만료 시간 관리
1. client-id, client-secret 발급 받기
kakao: Kakao Developers
naver: 네이버 로그인 - INTRO
google: https://console.cloud.google.com/
2. 의존성 및 설정
// build.radle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-client-id
client-secret: your-client-secret
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- email
- profile
naver:
client-id: your-client-id
client-secret: your-client-secret
scope:
- name
- email
- profile_image
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
kakao:
client-id: your-client-id
client-secret: your-client-secret
scope:
- profile_nickname
- profile_image
client-name: Kakao
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
3. 주요 코드
// SecurityConfig: 전체적인 보안 설정 관리
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService oAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final OAuth2FailureHandler oAuth2FailureHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/v1/auth/**", "/oauth2/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.baseUri("/oauth2/authorization")
.and()
.redirectionEndpoint()
.baseUri("/oauth2/callback/*")
.and()
.userInfoEndpoint()
.userService(oAuth2UserService)
.and()
.successHandler(oAuth2SuccessHandler)
.failureHandler(oAuth2FailureHandler);
return http.build();
}
}
// CustomOAuth2UserService: OAuth2 사용자 정보 처리
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
try {
return processOAuth2User(userRequest, oauth2User);
} catch (Exception ex) {
throw new OAuth2AuthenticationProcessingException("Failed to process OAuth2 user");
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
userRequest.getClientRegistration().getRegistrationId(),
oauth2User.getAttributes()
);
Optional<User> userOptional = userRepository.findByEmail(userInfo.getEmail());
User user;
if (userOptional.isPresent()) {
user = userOptional.get();
user = updateExistingUser(user, userInfo);
} else {
user = registerNewUser(userRequest, userInfo);
}
return UserPrincipal.create(user, oauth2User.getAttributes());
}
}
// OAuth2SuccessHandler: 인증 성공 시 토큰 발급
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
String accessToken = tokenProvider.createAccessToken(userPrincipal);
String refreshToken = tokenProvider.createRefreshToken(userPrincipal);
// Redis에 Refresh Token 저장
redisTemplate.opsForValue().set(
"RT:" + userPrincipal.getId(),
refreshToken,
tokenProvider.getRefreshTokenExpiration(),
TimeUnit.MILLISECONDS
);
TokenResponse tokenResponse = new TokenResponse(accessToken, refreshToken);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(tokenResponse));
}
}
// TokenProvider: JWT 토큰 생성 및 검증
@Component
@RequiredArgsConstructor
public class TokenProvider {
@Value("${app.auth.tokenSecret}")
private String tokenSecret;
@Value("${app.auth.tokenExpirationMsec}")
private long tokenExpirationMsec;
@Value("${app.auth.refreshTokenExpirationMsec}")
private long refreshTokenExpirationMsec;
public String createAccessToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + tokenExpirationMsec);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, tokenSecret)
.compact();
}
public String createRefreshToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshTokenExpirationMsec);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, tokenSecret)
.compact();
}
}
// UserPrincipal: OAuth2 사용자 정보 캡슐화
public class UserPrincipal implements OAuth2User {
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getName() {
return String.valueOf(id);
}
public static UserPrincipal create(User user, Map<String, Object> attributes) {
List<GrantedAuthority> authorities = Collections.singletonList(
new SimpleGrantedAuthority("ROLE_USER")
);
UserPrincipal userPrincipal = new UserPrincipal(
user.getId(),
user.getEmail(),
user.getPassword(),
authorities
);
userPrincipal.setAttributes(attributes);
return userPrincipal;
}
}
// OAuth2UserInfoFactory: 소셜 로그인 제공자별 사용자 정보 처리
public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(String registrationId, Map<String, Object> attributes) {
switch(registrationId.toLowerCase()) {
case "google":
return new GoogleOAuth2UserInfo(attributes);
case "github":
return new GithubOAuth2UserInfo(attributes);
default:
throw new OAuth2AuthenticationProcessingException(
"Sorry! Login with " + registrationId + " is not supported yet.");
}
}
}
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
public abstract String getImageUrl();
}
'개발 > 백엔드' 카테고리의 다른 글
내가 보려고 작성하는 각종 docker 설치 명령어 - mysql, redis, kafka, mongo (0) | 2025.02.14 |
---|---|
Kotlin Spring의 장점과 코루틴 (0) | 2025.01.24 |
Spring GraphQL (1) | 2025.01.06 |
빠르게 GraphQL 기본 개념 정리 (0) | 2025.01.06 |
Redis Serializer 비교 (0) | 2024.12.31 |