일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 코드스테이츠 백엔드 과정 39기
- 알고리즘
- jpa
- springboot
- annotation
- 문제풀이
- 자바 알고리즘
- 프로세스 불연속 할당
- Segmentation with Paging
- 웹 프로그래밍
- 스프링부트
- 프로세스 동기화
- Shared Page
- 메모리의 불연속적 할당
- Page Table의 구현
- 2단계 Page Table
- Inverted Page Table
- 프로세스 할당
- 자바 문제풀이
- Effective Access Time
- spring
- 메모리 관리
- 운영체제
- linux
- 스프링
- Allocation of Physical Memory
- 다단계 페이지 테이블
- CS
- 웹개발
- 리눅스
- Today
- Total
GrowMe
OAuth2를 스프링부트 프로젝트에 적용하기(with JWT 토큰) 본문
OAuth2를 스프링부트 프로젝트에 적용하기
# OAuth2란?
# OAuth2의 동작방식
# OAuth2를 적용해보기
*OAuth2란?

- 위키백과 정의 : OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.
- Google, Naver, Kakao 등 외부 소셜 계정을 기반으로 간편히 회원가입 및 로그인할 수 있으며 해당 소셜 계정과 연동되어있는 기능도 간편하게 사용할 수 있습니다. (ex : Google로 간편로그인 후, 연동된 계정의 Google Calendar 정보를 가져와 사용자에게 보여주기)
*들어가기 전에
https://grow-myself.tistory.com/37
[Spring] 스프링 시큐리티의 개념과 구조
Spring Security # Spring Security 개념 # 필터 # Spring Security 특징 # Spring Security 구조 # 필터 별 기능 *스프링 시큐리티란? 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 ..
grow-myself.tistory.com
https://grow-myself.tistory.com/38
[Spring] 스프링부트에서의 스프링 시큐리티 기본 세팅
Spring Security 기본 세팅 # Gradle 추가 및 SecurityConfig 파일 생성 # 예외처리 # 로그인 화면을 커스텀 페이지로 # id, pw 인증 처리 # 비밀번호 암호화 방식 커스텀 *Gradle 추가 / SecurityConfig - bu..
grow-myself.tistory.com
OAuth2를 이해하기 위해서는 시큐리티에 대한 기본적인 이해와, 사용법을 알고있을 필요가 있습니다. 이를 위해 위 글을 참조해보세요.🧐
그리고
https://grow-myself.tistory.com/49
JWT를 SpringBoot 프로젝트에 적용하기(2) - Refresh Token 적용
JWT(Json Web Token) 적용 - Refresh Token # 들어가기 전에 # Refresh Token # Refresh 토큰을 적용한 JWT 로그인 인증 절차 # JWT - Refresh Token 적용하기 # 동작 테스트 *들어가기 전에 https://gro..
grow-myself.tistory.com
기본 코드 토대가 위 글을 기반으로 이어서 작성되니 겹치는 코드들은 생략할 예정입니다. 해당 포스팅을 참고해서 글을 읽어주시면 감사하겠습니다. 또한 Google 기준으로 먼저 작성하였는데, 추후 네이버, 카카오 등도 추가 포스팅 예정입니다. 🙏
*OAuth2의 동작 방식 (Authorization Code Grant 방식 기준)

- 용어
Resource Owner : 리소스를 가지고 있는 주인. 즉 Bob이라는 사용자가 구글 계정으로 로그인해서 Google의 서비스(Resource)를 이용하고 있다면 Bob이 Google 서비스라는 Resource에 대한 Resource Owner가 됩니다.
Client : Resource Owner를 대신해 보호된 Resource에 액세스하는 애플리케이션. 예를 들어, Bob이라는 사용자가 A라는 애플리케이션을 통해서 Google의 소셜로그인을 이용한다면 애플리케이션 A가 Client가 됩니다.
Authorization Server : Resource Server에 접근 가능하도록 권한을 부여하는 서버(여기서는 Google의 서버 중 하나)
Resource Server : Resource Owner의 Resource를 제공하는 서버(여기서는 Google의 서버 중 하나)
- 용어 정리
Bob(사용자) 로그인 요청 -> 클라이언트(프론트 담당) -> 서비스 운영 서버(백엔드 담당) : (구글입장에서는 클라이언트)
-> Authorization Server(구글 서버1) -> Resource Server(구글 서버2)
- 동작 과정
- Resource Owner는 소셜 로그인 버튼을 누르는 등의 서비스 요청을 Client(애플리케이션)에게 전송합니다.
- Client는 Authorization Server에 Authorization Code를 요청합니다. 이 때 미리 생성한 Client ID, Redirect URI, 응답 타입을 함께 전송합니다.
- Resource Owner는 로그인 페이지를 통해 로그인을 진행합니다.
- 로그인이 확인되면 Authorization Server는 Authorization Code를 Client에게 전달합니다. (이 전에 요청과 함께 전달한 Redirect URI로 Code를 전달합니다.)
- Client는 전달받은 Authorization Code를 이용해 Access Token 발급을 요청합니다. AccessToken을 요청할 때 미리 생성한 Client Secret, Redirect URI, 권한 부여 방식, Authorization Code를 함께 전송합니다.
- 요청 정보를 확인한 후 Redirect URI로 Access Token을 발급합니다.
- Client는 발급받은 Access Token을 이용해 Resource Server에 Resource를 요청합니다.
- Access Token을 확인한 후 요청 받은 Resource를 Client에게 전달합니다.
*OAuth2를 적용해보기(SpringBoot Gradle 기준)
1. Google API Console에 서비스 등록
- 구글 API Console로 이동합니다.

- 새로운 프로젝트를 만듭니다.
- 사용 설정된 API 및 서비스 상단에 프로젝트 선택(or 이미지에 보이는 최근 프로젝트 선택)을 눌러줍니다.

생성된 프로젝트로 선택 시 API 및 서비스가 보이게 됩니다.

- 왼쪽 목록에서 OAuth 동의 화면을 클릭합니다.
- OAuth 동의 화면에서 User Type은 외부로 해줍니다.
- 앱 등록 수정 페이지에서 앱 이름을 작성해주시고 본인 이메일을 선택 후 저장 후 계속을 눌러 진행합니다.
- 나머지는 별도 설정 해줄 것이 현재 없어서 저장 후 계속을 눌러서 진행합니다.
- 왼쪽 목록에서 사용자 인증 정보를 클릭합니다.
- 사용자 인증 정보 만들기 - OAuth 클라이언트 ID 클릭
- 애플리케이션 유형은 웹 애플리케이션입니다.
- 이름을 입력합니다.
- 승인된 리디렉션 URI에 http://localhost:8080/login/oauth2/code/google 를 입력합니다.
- 서비스에서 파라미터로 인증 정보를 주었을 때, 인증이 성공하면 Google에서 리다이렉트할 URL입니다.
- 스프링부트 2버전의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있습니다.
- 사용자가 별도로 리다이렉트 URL과 매핑되는 Controller를 만들 필요가 없습니다. 시큐리티에서 이미 구현해 놓은 상태.
- 지금은 적용해보기만 하는 것이기에, 로컬 도메인만 추가하였습니다. 추후 AWS 등에 배포 시에는 해당 도메인으로 추가하면 됩니다.
- 만들기 클릭 시 OAuth 클라이언트 ID와 보안 비밀번호가 발급됩니다.
- 해당 정보를 다른 사람에게 노출/공유해서는 안됩니다.
- 반드시 정보를 별도로 저장/기록 해두어야 합니다.
2. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-mustache'
- oauth2를 사용하기 위한 의존성을 추가해줍니다.
- 테스트를 위한 간단한 페이지를 만들어 이용할 것이기에 이때 사용할 mustache를 추가해줍니다.
3. application.properties(환경 설정)
spring.security.oauth2.client.registration.google.client-id=62650287293-9d9l1m28qe92h0ek0d0ul6ghovkf834s.apps.googleusercontent.com
spring.security.oauth2.client.registration.google.client-secret=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
spring.security.oauth2.client.registration.google.scope=profile,email
- 구글의 유저 정보를 가져오기 위해서는 구글에게 먼저 우리의 서버(구글입장에서는 클라이언트)가 인증된 사용자임을 알려야합니다.
- 이를 위해 위에서 구글에 애플리케이션을 등록 후 지급되었던 client-id, client-secret을 해당 설정 파일에 올려두면 스프링 시큐리티에서 이 값을 기반으로 구글에 인가코드를 받고, 액세스 토큰을 요청하여 발급받고 그 토큰으로 다시 유저정보를 받아오는 동작을 자동으로 처리해줍니다.
- scope : 유저정보를 받을 정보의 범위입니다. default에는 openid, profile, email인데, openid라는 scope가 포함되어 있으면 Open Id Provider로 인식하게 됩니다. 이렇게 되면 OpenId Provider인 서비스(구글)과 그렇지 않은 서비스(네이버/카카오 등)로 나눠서 각각 처리할 OAuth2Service를 별도로 만들어야합니다. 하나의 OAuth2Service로 처리하기 위해 openid scope를 빼고 등록하게하였습니다.
4. OAuthAttributes
@Getter
@NoArgsConstructor
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String memberName;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String memberName, String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.memberName = memberName;
this.email = email;
this.picture = picture;
}
// of 메서드 사용 이유 : OAuth2User에서 반환하는 정보 : Map이며, 이용하는 소셜마다 프로퍼티값이 다르기에 일일이 넣어줘야함.
public static OAuthAttributes of(String registrationId, String nameAttributeKey, Map<String, Object> attributes) {
if("naver".equals(registrationId)) {
return ofNaver(nameAttributeKey, attributes);
}
if("kakao".equals(registrationId)) {
return ofKakao(nameAttributeKey, attributes);
}
return ofGoogle(nameAttributeKey, attributes);
}
private static OAuthAttributes ofGoogle(String nameAttributeKey, Map<String, Object> attributes) {
HashMap<String, Object> hashAttributes = new HashMap<>();
String memberName = (String) attributes.get("name");
String email = (String) attributes.get("email");
String profileImg = (String) attributes.get("picture");
hashAttributes.put("profileImg", profileImg);
hashAttributes.put("memberName", memberName);
hashAttributes.put("email", email);
hashAttributes.put("sub",nameAttributeKey);
return OAuthAttributes.builder()
.memberName(memberName)
.email(email)
.picture(profileImg)
.attributes(hashAttributes)
.nameAttributeKey(nameAttributeKey)
.build();
}
private static OAuthAttributes ofNaver(String nameAttributeKey, Map<String, Object> attributes) {
// naver 로그인 요청이 왔을 때 유저정보 저장 처리
retun null;
}
private static OAuthAttributes ofKakao(String nameAttributeKey, Map<String, Object> attributes) {
// kakao 로그인 요청이 왔을 때 유저정보 저장 처리
retun null;
}
public Member toEntity() {
return Member.builder()
.memberName(memberName)
.email(email)
.memberRole(Member.MemberRole.ROLE_USER)
.profileImg(picture)
.build();
}
}
- OAuthAttributes 클래스는 OAuth2 인증이 성공하여 전달된 유저정보들을 통일된 형식으로 저장하여 사용하기 위한 클래스입니다.
- 각 소셜들은 Map 타입인 attrubutes에 유저정보를 담고 있는데, 이 때 Map의 Key 값을 소셜 별로 다르게 지정하고 있기 때문에 소셜 별로 of 메서드를 따로 지정하여 담아야합니다.
- 또한 인자로 들어온 Map을 그대로 넣어서 담지 않고, HashMap을 별도로 만들어서 값을 일일이 추가한 이유는 각 소셜에서 보내준 정보가 담겨있는 Map은 엔트리를 추가하거나 변경이 불가능한 unmodifiableMap입니다. 따라서 put 메서드를 사용할 수 없습니다.
- 아래에서 로그인 성공 후 처리 중, 토큰을 헤더에 담는 부분이 있는데 거기서 여기서 만들고 있는 OAuthAttributes의 attributes 필드를 꺼내와 사용하게 될 겁니다. 인자로 들어온 Map을 그대로 넣어버리게되면 각 소셜 로그인 별 Map이 그대로 들어가 Key 값이 그때 그때 달라지게 됩니다. 이를 모두 같은 key 값으로 꺼내서 쓸 수 있게 만들기 위해 별도로 HashMap을 만들어 필요한 값을 별도로 저장되게 하였습니다.
5. CustomOAuth2UserService
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
private final StringIdGenerator stringIdGenerator;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 소셜 로그인 사전 작업
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
// 소셜 로그인한 사용자의 주체를 oAuth2User에 저장
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// registrationId : 현재 로그인 진행중인 서비스 구분(네이버/구글/카카오)
// nameAttributeKey : 로그인 진행 시, 키가되는 필드 값 : 구글 'sub', 네이버 'response', 카카오 'id'
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String nameAttributeKey = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuthAttributes : OAuth2UserService를 통해 가져온 OAuth2User(소셜 로그인 유저)의 attribute(속성)을 담을 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, nameAttributeKey, oAuth2User.getAttributes());
// 저장된 유저의 정보를 실제 우리 서비스의 회원으로 업데이트
Member member = saveOrUpdate(attributes);
// Collections.singleton : 단 한개의 객체만 저장 가능한 컬렉션을 만들고 싶을 때 사용
// SimpleGrantedAuthority : 인증 개체에 부여된 권한을 저장해둠
// DefaultOAuth2User : OAuth2User의 구현 클래스
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(member.getMemberRole().getRole())),
attributes.getAttributes(),
attributes.getNameAttributeKey()
);
}
// 우리 서비스에 등록되있는 유저가 아니면 회원가입 처리
// 이미 등록되있는 유저이면 변동된 값만 업데이트
private Member saveOrUpdate(OAuthAttributes attributes) {
Member member = memberRepository.findByEmail(attributes.getEmail())
.map(entity ->
entity.updateMember(
Member.builder()
.memberName(attributes.getMemberName())
.profileImg(attributes.getPicture())
.build()))
.orElse(attributes.toEntity()
.addMemberId(stringIdGenerator.createMemberId()));
return memberRepository.save(member);
}
}
- OAuth2UserService는 소셜 인증이 완료된 유저의 정보를 받아와 처리하는 역할의 인터페이스입니다. 이 인터페이스를 구현한 CustomOAuth2UserService를 통해 소셜 인증 후의 처리를 커스텀합니다.
- OAuth2UserRequest : OAuth2UserService가 유저정보 엔드포인트를 요청할 때 사용하는 요청 (소셜로그인한 사용자 정보가 담겨있음)
- OAuth2User : OAuth 2.0 Provider(구글)에 등록된 사용자의 주체 표현
- 기타 자세한 사항은 모두 주석처리하여 설명하였습니다.
6. CustomOAuth2SuccessHandler
@Component
@RequiredArgsConstructor
public class CustomOAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) authentication.getPrincipal();
String memberId = (String) oAuth2User.getAttributes().get("memberName");
String email = (String) oAuth2User.getAttributes().get("email");
String accessToken = tokenProvider.createAccessToken(memberId, email);
String refreshToken = tokenProvider.renewalRefreshToken(memberId, email);
System.out.println("소셜 로그인 성공 유저 : " + email);
response.addHeader("Authorization", "Bearer " + accessToken);
response.addHeader("refresh", "Bearer " + refreshToken);
getRedirectStrategy().sendRedirect(request, response, "http://localhost:8080");
}
}
- SimpleUrlAuthenticationSuccessHandler : 서버에서 인증이 성공 후의 처리를 담당하는 클래스
- 해당 클래스의 onAuthenticationSuccess()를 오버라이딩하면 인증 성공 후처리를 커스터마이징할 수 있다.
- 인자에 담긴 Authentication 객체의 principal 필드가 바로 위의 CustomOAuth2UserService 클래스에서 생성하여 반환한 DefaultOAuth2User 객체이다. 저장되있는 유저정보를 꺼내와서 토큰을 생성하여 응답 헤더에 추가해주었다.
- 그리고나서 최초 로그인을 요청했던 컴퓨터의 8080포트 기본 페이지로 리다이렉트하게끔 하였다.
- 리다이렉트되어 도착한 그곳에 바로 우리의 토큰이 확인되면 구현완료라는 뜻이다.
- TokenProvider의 내용은 *들어가기전에 쪽의 JWT 포스팅 내용을 읽어보면 이해가 될 것이라고 생각한다.
7. SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// ... 이전 포스팅에서 추가했던 변수명
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// ... 이전 포스팅에서 추가했던 필터 체인
.and()
.oauth2Login() // OAuth2 로그인 기능에 대한 여러 설정의 진입점
.successHandler(customOAuth2SuccessHandler) // 우리 서비스 내에서의 유저 인증 성공 후처리 커스텀 핸들러 등록
.userInfoEndpoint() // OAuth2 로그인 성공 이후 사용자 정보에 대한 엔드포인트 진입
.userService(customOAuth2UserService); // 소셜 로그인 성공 시 후속 조치 진행할 구현체 등록
return httpSecurity.build();
}
}
- 만들어 둔 CustomOAuth2UserService와 CustomOAuth2SuccessHandler를 주입받는다.
- 실제로 필터체인에서 사용이 되도록 OAuth2와 구현체들을 등록한다.
8. index.mustache
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>Social Login Test Page</h1>
</div>
<div>
<a href="http://localhost:8080/oauth2/authorization/google" role="button">Google Login</a>
</div>
</body>
</html>
- 로그인 테스트를 위한 페이지이다.
- {도메인}/oauth2/authorization/{소셜 서비스명} : 스프링 시큐리티에서 제공하는 기본 제공 OAuth2 로그인 요청 URL이다. 컨트롤러를 별도로 추가해주지 않아도 알아서 동작시켜 준다. 본래는 scope, clientId, redirectUrl 을 필수 파라미터로 추가하여 요청해야하지만, 이를 시큐리티에서 자동으로 지정해준다. (application.properties에 설정된 값을 이용)
- 특히 redirectUrl이 중요한데, 이 또한 기본제공 로그인 요청 Url로 애초에 요청했기 때문에 redirectUrl 또한 시큐리티에서 기본적으로 제공해주는 Url로 자동 설정을 해주어 리다이렉트를 시켜주는 것 같다. 리다이렉트Url을 매핑시키는 컨트롤러 또한 작성할 필요가 없다. 위의 구글 API 콘솔에서 기본 리다이렉트Url을 추가해주었기에 문제없이 작동될 것이다.
- 리다이렉트Url을 커스텀하여 컨트롤러에서 매핑시키면, 구글에서 인가코드를 발급받고 액세스 토큰 요청을 한 뒤에 받은 토큰으로 다시 유저정보를 요청해야한다. 이를 모두 자동으로 해준다고 생각하면 된다. (굉장히 편리하다.. 스프링 시큐리티.. ㅠㅠ)
9. IndexController
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
- 위에서 만들어준 페이지를 사용하기 위해 컨트롤러를 만들어 주었다. 로그인 성공 후 리다이렉트도 여기로 되도록 지정했다.
- 그렇다면 로그인 후에?? 페이지는 새로고침한번 한것처럼 변화가 없어보일 것임.
*OAuth2를 적용해보기(SpringBoot Gradle 기준)
1. 인덱스 페이지 확인

2. 버튼을 누르면?

- 우리가 발급한 토큰이 잘 도착해 있는 것을 확인 가능!
'Security' 카테고리의 다른 글
JWT를 SpringBoot 프로젝트에 적용하기(2) - Refresh Token 적용 (0) | 2022.09.19 |
---|---|
JWT를 SpringBoot 프로젝트에 적용하기(1) - Access 토큰 적용 (0) | 2022.08.24 |
[Spring] 스프링부트에서의 스프링 시큐리티 기본 세팅 (0) | 2022.07.05 |
[Spring] 스프링 시큐리티의 개념과 구조 (0) | 2022.07.05 |