
이전 글에서 이어집니다.
https://hwlee9905.tistory.com/23
Spring Security를 활용하여 JWT 발급 자체 로그인, OAuth2 구현하기 - 2
이전글에서 이어집니다 https://hwlee9905.tistory.com/22 Spring Security를 활용하여 JWT 로그인 구현하기 - 1 Spring Security의 개념 스프링 시큐리티는 스프링 기반의 어플리케이션의 보안(인증과 권한)을 담당
hwlee9905.tistory.com
OAuth(Open Authorization)란?
구글, 페이스북, 트위터와 같은 다양한 플랫폼의 특정한 사용자 데이터에 접근하기 위해 제3자 클라이언트(우리의 서비스)가 사용자의 접근 권한을 위임(Delegated Authorization)받을 수 있는 표준 프로토콜이다.
쉽게 말하자면, 우리의 서비스가 우리 서비스를 이용하는 유저의 타사 플랫폼 정보에 접근하기 위해서 권한을 타사 플랫폼으로부터 위임 받는 것 이다.
쉽게 말하면, 어플리케이션을 이용할 때 사용자가 해당 어플리케이션에 ID, PW등의 정보를 제공하지 않고,
신뢰할 수 있는 외부 어플리케이션(Naver, Google, Kakao, Facebook 등)의 Open API에
ID, PW를 입력하여 해당 어플리케이션이 인증 과정을 처리해주는 방식이다.
용어자체는 어렵지만 요즘 사이트는 자체로그인은 아예 구현하지 않는 사이트도 많이 있다.
네이버, 구글, 카카오 아이디만 있으면 해당 사이트에서 필요로 하는 추가정보들을 제외하고는 자동적으로 외부 어플리케이션의 리소스 서버에서 제공해주므로, 사용자 입장에서도 편리하고 보안이 체계적이고 강력한 외부 어플리케이션 서버에서 사용자 보안을 담당해주기 때문에 어플리케이션을 개발하는 개발자 입장에서도 보안에 대한 부담이 한층 덜하다.
OAuth2 로그인 순서
자체 로그인을 구현할 때는 Spring Server에서 인증에 관한 부분을 모두 도맡아 했지만
OAuth2 방식에서는 인증을 외부 인증 서버에서 대신 처리해주므로,
우리는 인증 서버에 우리의 어플리케이션을 등록하고, Spring Security Config에 OAuth와 관련한 설정을 해주고 JWT 토큰만 발급해주면 된다.
네이버 소셜 로그인 신청
리소스 서버에서 제공받을 정보를 체크해주고
현재 운용중인 서비스의 url을 적어준다. 우리는 로컬로 테스트할 것이므로 localhost로 해준다.
설정
의존성 추가
//oauth2 client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
application.properties
#registration
spring.security.oauth2.client.registration.naver.client-name=naver
spring.security.oauth2.client.registration.naver.client-id=
spring.security.oauth2.client.registration.naver.client-secret=
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email
#provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
google 등과 같이 스프링 시큐리티에서 제공하는 서비스의 경우에는 registration으로 등록하기 위해서는
client-id만 기본적으로 설정하면 등록되지만, 만약 시큐리티에서 기본적으로 제공하는 서비스가 아닌 경우(Naver, Kakao 등)에는 위와 같이 등록해주면 된다.
Spring Security Config
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
//AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;
private final OAuth2UserSerivce oauth2UserSerivce;
private final SuccessHandler successHandler;
//OAuth2
http
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(oauth2UserSerivce))
.successHandler(successHandler)
);
...
}
리소스 서버에서 받아온 사용자 정보를 저장하는 OAtuthUserService,
로그인 성공시 JWT를 발급하는 SuccessHandler를 등록하고
oauth2Login()을 활성화 시키면 OAuth2LoginConfigurer가 활성화된다.
AbstractAuthenticationFilterConfigurer를 상속받으며, Filter로는 OAuth2LoginAuthenticationFilter를 사용한다.
중요한 부분은 다음과 같다.
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
/**
* The default base {@code URI} used for authorization requests.
*/
public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
...
}
/oauth2/authorization으로 들어오는 요청에 대해서
이 필터가 작동한다는것을 알 수 있다.
따라서 .../oauth2/authorization/...을 입력하여 요청을 보냈다고 가정한다면,
처음으로 OAuth2관련된 필터가 적용되는 것이
OAuth2AuthorizationRequestRedirectFilter이다.
OAuth2UserSerivce
OAuth2 로그인에 성공했을때 필요한 OAuth2UserService를 구현해보자.
DefaultOAuth2UserService를 상속받고
loadUser를 오버라이드하여 구현하면 된다.
@RequiredArgsConstructor
@Service
@Slf4j
public class OAuth2UserSerivce extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final AuthenticationRepository authenticationRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
InfoSet infoSet;
log.info("OAuth2User : " + oAuth2User);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
infoSet = InfoSet.NAVER;
oAuth2Response = new OAuth2NaverResponseDto(oAuth2User.getAttributes());
}
else if (registrationId.equals("google")) {
infoSet = InfoSet.GOOGLE;
oAuth2Response = new OAuth2GoogleResponseDto(oAuth2User.getAttributes());
}
else if (registrationId.equals("kakao")) {
infoSet = InfoSet.KAKAO;
oAuth2Response = new OAuth2KakaoResponseDto(oAuth2User.getAttributes());
}
else {
return null;
}
//리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디값을 만듬
String username = oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId();
User existData = userRepository.findByUserId(username);
if (existData == null) {
Authentication authentication = Authentication.builder()
.userId(username)
.email(oAuth2Response.getEmail())
.infoSet(infoSet)
.build();
authenticationRepository.save(authentication);
User user = User.builder()
.username(oAuth2Response.getName())
.role(Role.USER)
.build();
user.setAuthentication(authentication);
userRepository.save(user);
OAuth2UserDto oAuth2UserDto = OAuth2UserDto.builder()
.name(oAuth2Response.getName())
.username(username)
.role(Role.USER.toString())
.build();
return new CustomOAuth2User(oAuth2UserDto);
}
else {
existData.getAuthentication().setEmail(oAuth2Response.getEmail());
existData.setUsername(oAuth2Response.getName());
OAuth2UserDto oAuth2UserDto = OAuth2UserDto.builder()
.username(existData.getAuthentication().getUserId())
.name(oAuth2Response.getName())
.role(Role.USER.toString())
.build();
return new CustomOAuth2User(oAuth2UserDto);
}
}
}
- 사용자 정보를 기반으로 OAuth 2.0 프로바이더를 식별하고, 각 프로바이더에 따라 다른 처리를 수행한다.
- 사용자 정보를 바탕으로 사용자를 특정하는 아이디 값을 생성한다.
- 데이터베이스에서 해당 아이디 값을 가진 사용자가 이미 존재하는지 확인한다.
- 사용자가 존재하지 않는 경우, 사용자 정보를 데이터베이스에 저장하고 새로운 사용자를 생한다..
- SuccessHandler의 onAuthenticationSuccess에서 JWT 발급에 사용하기 위해 생성된 사용자 정보를 CustomOAuth2User 객체에 담아 보낸다.
oAuth2User에 담겨오는 json data가 외부 소셜 로그인 API마다 다르므로 해당 API의 reponse 명세서를 잘보고 response 객체를 만들어야한다.
public class OAuth2NaverResponseDto implements OAuth2Response {
private final Map<String, Object> attribute;
public OAuth2NaverResponseDto(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getProviderId() {
return attribute.get("id").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
Naver의 경우 최상위에 response key가 있으므로 해당 key로 map을 만들고 map에서 get()을 하는 방식으로 getter를 만들었다.
SuccessHandler
로그인에 성공하고 JWT를 발급하기 위한 SuccessHandler다
@Component
@RequiredArgsConstructor
public class SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JWTUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
String username = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String token = jwtUtil.createJwt(username, role, 60*60*60L);
response.addHeader("Authorization", "Bearer " + token);
response.sendRedirect("https://localhost:8080/auth");
}
//쿠키로 JWT 발급
// private Cookie createCookie(String key, String value) {
//
// Cookie cookie = new Cookie(key, value);
// cookie.setMaxAge(60*60*60);
// cookie.setSecure(true);
// cookie.setPath("/");
// cookie.setHttpOnly(true);
//
// return cookie;
// }
}
우리는 Header에 Authorization key로 JWT를 발급하기로 했으므로 Header에 JWT를 담아 response 해준다.
JWT 발급이후 JWT 검증 필터는 기존에 있던 JWTFilter를 그대로 쓰면 된다!
카카오와 구글 로그인도 위와 같은 방식으로 하면 되지만
구글은 API 요청시 SSL 인증서를 필요로 하므로 https로 요청해주어야 오류가 나지 않는다.
※주의사항
application.properties에 client-id와 clien-secret 값을 저장하므로 이것을 그대로 서버에 올리지 않도록 주의해야한다. 환경변수를 사용하거나 Google이나 AWS의 Secret Manager를 사용하여 값이 노출되지 않게 하자.
로그인 테스트
로그인에 성공하고 Response Header의 Authorization에 JWT가 담겨오는 모습이다.
출처
https://ttl-blog.tistory.com/97#OAuth2LoginConfigurer-1
[Security] Spring Security - OAuht2 로그인 작동원리
요즘은 OAuth2를 사용하여 소셜 로그인을 통해 가입이 가능한 서비스들이 굉장히 많다. 스프링 시큐리티에서도 OAuth2를 이용한 소셜 로그인 방식을 지원하는데, 필자는 맨 처음 이를 사용할때 너
ttl-blog.tistory.com
OAuth 2.0 개념과 동작원리
2022년 07월 13일에 작성한 글을 보충하여 새로 포스팅한 글이다. OAuth 등장 배경 우리의 서비스가 사용자를 대신하여 구글의 캘린더에 일정을 추가하거나, 페이스북, 트위터에 글을 남기는 기능을
hudi.blog
https://www.youtube.com/watch?v=xsmKOo-sJ3c&list=PLJkjrxxiBSFALedMwcqDw_BPaJ3qqbWeB&index=1&t=23s
'Spring' 카테고리의 다른 글
효율적인 JDBC 프로그래밍을 위한 DataSource, Connection Pool 기술 - 1 (0) | 2024.03.13 |
---|---|
API마다 다르게 예외처리하는 방법 (0) | 2024.03.09 |
Spring Security를 활용하여 JWT 발급, 자체 로그인, OAuth2 구현하기 - 2 (0) | 2024.03.06 |
Spring Security를 활용하여 JWT 발급, 자체 로그인, OAuth2 구현하기 - 1 (2) | 2024.03.05 |
공통 관심사를 처리하기 위한 Filter와 Interceptor (0) | 2024.03.02 |

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!