
구글을 예로 들면
1. 서비스가 Oauth 사용등록함
2. 서비스와 구글이 서로의 정보를 저장
3. 사용자가 구글로 로그인 클릭
https://resource-server/?response_type=code&client_id=1&scope=B,C&redirect_uri=http://client/callback
4. 구글이 사용자에게 받은 URL과 자신이 가진 client_id, redirect_uri를 비교하고 일치하면 사용자에게 scope에 해당하는 권한을 서비스에 부여하는지 물어봄
5. 사용자가 권한 허용함
6. 구글은 사용자가 권한을 허용했다는 정보를 저장함
7. 구글은 서비스에서의 접근을 허가하기 위해 code를 저장하고 사용자에게 redirect_uri전송
https://client/calback?code=3
8. 사용자는 구글에서 받은 주소로 redirect
9. 서비스는 사용자에게 받은 주소로 Authorization code 저장
10. 서비스가 구글에 access token 요청
http://resource-server/token?Grant-type=authorization_code&code=3&redirect_uri=https://client/callback&client_id=1%client_secret=2
11. 인증과정이 다시 발생하지 않도록 Authorization code 삭제, 구글에서 access token과 refresh token 생성 발급
12. DB에 access token, refresh token 저장
13. 구글의 정보가 필요한 경우 accessToken과 함께 요청 전송
14. accessToken 확인 후 사용자 정보 전송
만약 accessToken이 유효하지않다면 Invalid Token Error전송
15. RefreshToken 구글에 전송, accessToken과 (optional)refreshToken 재발급
깃헙의 코드를 참고해도 좋다.
https://github.com/usingjun/OAuth2_login
다음과 같은 형식을 맞추면 된다.
spring:
security:
oauth2:
client:
registration:
kakao:
client-id:
client-secret:
client-name: kakao
client-authentication-method: POST
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
scope:
- profile_nickname
- account_email
google:
client-id:
client-secret:
scope: profile, email
naver:
client-id:
client-secret:
scope: name, email
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/naver
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.6.2'
https://console.cloud.google.com/apis/dashboard 접속
상단 프로젝트부분 클릭

새 프로젝트 클릭 > 만들기 클릭

방금 생성한 프로젝트 선택 > OAuth 동의화면 > 외부 > 만들기

1단계 필수부분 작성, 2단계 범위 설정 건들지 않고 다음, 3단계 테스트 사용자 건들지 않고 다음, 4단계 요약 확인



사용자 인증정보 > 사용자 인증정보 만들기 클릭

애플리케이션 유형 : 웹 애플리케이션 선택, 승인된 리디렉션URI 입력


승인된 리디렉션URI에는 "http://localhost:8080/login/oauth2/code/google" 을 입력한다. (빨간색은 정해진 내용이다)
다음 값을 yml파일이나 propeties파일에 입력하면 된다

네이버 개발자 센터 접속
https://developers.naver.com/main/
Application > 애플리케이션 등록

약관동의, 계정정보등록 후 애플리케이션 등록 > 애플리케이션 이름 작성 > 사용 API : 네이버 로그인 클릭 > 회원이름, 이메일주소 클릭 > 서비스환경 PC웹 클릭


서비스 URL : http://localhost:8080 입력
콜백 URL : http://localhost:8080/login/oauth2/code/naver 입력 후 등록

구글과 마찬가지로 입력하자

카카오 개발자 센터 접속
https://developers.kakao.com/
내 애플리케이션 > 애플리케이션 추가하기

애플리케이션 정보 입력 후 저장

REST API가 client-id이다. 확인하고 플랫폼 설정하기 클릭

Web 플랫폼 등록 클릭

사이트 도메인에 localhost:8080입력

Redirect URI 등록하러 가기 클릭

카카오 로그인 활성화 "ON"으로 변경, Redirect URI 등록 클릭

왼쪽 메뉴바를 눌러서 동의항목 클릭

가져오려는 정보 설정하여 사용으로 변경

왼쪽 메뉴 눌러서 보안 클릭

Client Secret 코드 생성 클릭하여 확인하고 입력하자

1. controller
UserController
package com.example.demo.controller;
@Controller
public class UserController {
@GetMapping("/")
public String helloNotice(){
return "login_form";
}
@GetMapping("/success")
public String successLogin(){
return "success_form";
}
}
2. entity
Member
package com.example.demo.entity;
@Entity
@Getter
@NoArgsConstructor
public class Member implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; //기본키
private String name; //유저 이름
private String password; //유저 비밀번호
private String email; //유저 구글 이메일
private String role; //유저 권한 (일반 유저, 관리지ㅏ)
private String provider; //공급자 (google, facebook ...)
private String providerId; //공급 아이디
@Builder
public Member(String name, String password, String email, String role, String provider, String providerId) {
this.name = name;
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
}
public String getRole() {
// 여기서 역할 정보를 가져오거나 반환하는 로직을 작성해야 합니다.
// 예를 들어, "ROLE_USER"를 반환하거나 사용자의 역할 정보를 반환하도록 구현합니다.
return "ROLE_USER";
}
}
3. repository
OAuth2MemberRepository
package com.example.demo.repository;
@Repository
public interface OAuth2MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByName(String name);//이름을 통해 찾는 방식을 활용할 것이기 때문
}
4. form
login_form.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:400,500,600,700&display=swap">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />
</head>
<body>
<div class="login-container">
<div class="login-card">
<div class="login-card-header" ><img th:src="@{/images/Logo.png}" style="height:60px; width:50%; padding: 10px" alt="Logo"/></div>
<form th:action="@{/user/login}" method="post">
<div class="login-form-group">
<label class="login-form-label" for="username">이름</label>
<input type="text" class="login-input" id="username" name="username" placeholder="사용자명을 입력하세요">
</div>
<div class="login-form-group">
<label class="login-form-label" for="password">패스워드</label>
<input type="password" class="login-input" id="password" name="password" placeholder="비밀번호를 입력하세요">
</div>
<button type="submit" class="login-button">로그인</button>
<!-- 소셜 로그인 버튼 -->
<div class="social-login">
<a href="/oauth2/authorization/kakao">
<img th:src="@{/images/kakaologin.png}" style="height:55px; width:100%" alt="Kakao Login"/>
</a>
<a href="/oauth2/authorization/naver">
<img th:src="@{/images/naverlogin.png}" style="height:55px; width:100%" alt="Naver Login"/>
</a>
<a href="/oauth2/authorization/google">
<img th:src="@{/images/googlelogin.png}" style="height:55px; width:100%" alt="Google Login"/>
</a>
</div>
</form>
<!-- 회원가입 링크 -->
<div class="login-links">
아직 계정이 없으신가요? <a href="/user/signup" class="signup-link">회원가입</a>
</div>
</div>
</div>
</body>
</html>
success_form.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login Success!</title>
</head>
<body>
<div><span>login success!!!</span></div>
</body>
</html>
5. security config
SecurityConfig파일을 만들어 OAuth2시큐리티 설정을 하자
package com.example.demo.config;
// 스프링 시큐리티 설정 클래스
@EnableMethodSecurity
@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
private final OAuth2MemberService oAuth2MemberService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.requestMatchers(new AntPathRequestMatcher("/private/**")).authenticated()
.requestMatchers(new AntPathRequestMatcher("/admin/**")).access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
/*.formLogin()
.loginPage("/user/login")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/success")
.failureUrl("/user/login")
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessUrl("/success")*/ //일반로그인
.and()
.oauth2Login()//oauth2로그인
.loginPage("/login_form")//로그인 페이지
.defaultSuccessUrl("/success")//로그인 성공시 이동할 url
.userInfoEndpoint()
.userService(oAuth2MemberService).and().and().build();
}
}
6. oauth파일
package com.example.demo.auth.userinfo;
public interface OAuth2UserInfo {
Map<String, Object> getAttributes();
String getProviderId();
String getProvider();
String getEmail();
String getName();
}
package com.example.demo.auth.userinfo;
public class GoogleUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getProviderId() {
return attributes.get("sub").toString();
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getEmail() {
return attributes.get("email").toString();
}
@Override
public String getName() {
return attributes.get("name").toString();
}
}
package com.example.demo.auth.userinfo;
public class NaverUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes; //OAuth2User.getAttributes();
private Map<String, Object> attributesResponse;
public NaverUserInfo(Map<String, Object> attributes) {
this.attributes = (Map<String, Object>) attributes.get("response");
this.attributesResponse = (Map<String, Object>) attributes.get("response");
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getProviderId() {
return attributesResponse.get("id").toString();
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getEmail() {
return attributesResponse.get("email").toString();
}
@Override
public String getName() {
return attributesResponse.get("name").toString();
}
}
package com.example.demo.auth.userinfo;
import java.util.Map;
public class KakaoUserInfo implements OAuth2UserInfo {
private Map<String, Object> attributes;
private Map<String, Object> attributesAccount;
private Map<String, Object> attributesProfile;
public KakaoUserInfo(Map<String, Object> attributes) {
/*
System.out.println(attributes);
{id=아이디값,
connected_at=2022-02-22T15:50:21Z,
properties={nickname=이름},
kakao_account={
profile_nickname_needs_agreement=false,
profile={nickname=이름},
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=이메일}
}
*/
this.attributes = attributes;
this.attributesAccount = (Map<String, Object>) attributes.get("kakao_account");
this.attributesProfile = (Map<String, Object>) attributesAccount.get("profile");
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getProviderId() {
return attributes.get("id").toString();
}
@Override
public String getProvider() {
return "Kakao";
}
@Override
public String getEmail() {
return attributesAccount.get("email").toString();
}
@Override
public String getName() {
return attributesProfile.get("nickname").toString();
}
}
google, naver, kakao의 userinfo를 모두 만들었다.
package com.example.demo.auth;
@Getter
public class PrincipalDetails implements OAuth2User, UserDetails {
private Member member;
private Map<String, Object> attributes;
private Collection<? extends GrantedAuthority> authorities;//사용자의 권한 정보를 저장하는 컬렉션
public PrincipalDetails(Member member, Collection<? extends GrantedAuthority> authorities) {
this.member = member;
this.authorities = authorities;
}
public PrincipalDetails(Member member, Map<String, Object> attributes, Collection<? extends GrantedAuthority> authorities) {
this.member = member;
this.attributes = attributes;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isEnabled() {
return true; // 사용자 계정이 활성화되어 있는지 여부를 반환
}
@Override
public String getPassword() {
return null; // 비밀번호를 반환
}
@Override
public String getUsername() {
return member.getName(); // 사용자의 이름을 반환
}
@Override
public boolean isAccountNonExpired() {
return true; // 계정이 만료되었는지 여부를 반환
}
@Override
public boolean isAccountNonLocked() {
return true; // 계정이 잠겨있는지 여부를 반환
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 비밀번호가 만료되었는지 여부를 반환
}
@Override
public String getName() {
return member.getName(); // 사용자의 이름을 반환
}
}
7. service
user request를 받아 사용자 정보를 추출
package com.example.demo.service;
@Service
@RequiredArgsConstructor
public class OAuth2MemberService extends DefaultOAuth2UserService {
private final BCryptPasswordEncoder encoder;
private final OAuth2MemberRepository oAuth2MemberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
OAuth2UserInfo memberInfo = null;
System.out.println(userRequest.getClientRegistration().getRegistrationId());
String registrationId = userRequest.getClientRegistration().getRegistrationId();
if (registrationId.equals("google")) {
memberInfo = new GoogleUserInfo(oAuth2User.getAttributes());
} else if (registrationId.equals("naver")) {
memberInfo = new NaverUserInfo(oAuth2User.getAttributes());
} else if(registrationId.equals("kakao")) {
memberInfo = new KakaoUserInfo(oAuth2User.getAttributes());
} else {
System.out.println("로그인 실패");
}
String provider = memberInfo.getProvider();
String providerId = memberInfo.getProviderId();
String username = provider + "_" + providerId; //중복이 발생하지 않도록 provider와 providerId를 조합
String email = memberInfo.getEmail();
String role = "ROLE_ADMIN"; //일반 유저
System.out.println(oAuth2User.getAttributes());
Optional<Member> findMember = oAuth2MemberRepository.findByName(username);
Member siteUser = null;
if (findMember.isEmpty()) { //찾지 못했다면
siteUser = Member.builder()
.name(username)
.email(email)
.password(encoder.encode("password"))
.role(role)
.provider(provider)
.providerId(providerId).build();
oAuth2MemberRepository.save(siteUser);
} else {
siteUser = findMember.get();
}
return new PrincipalDetails(siteUser, oAuth2User.getAttributes(), Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")));
}
}
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}

다음 로그인폼에서 각 소셜로그인 버튼을 클릭하면 다음과 같은 화면으로 이동한다.



로그인을 완료하면 다음과 같이 뜰 것이다.

이번 프로젝트를 통해 OAuth2로그인에 대해 집중적으로 공부해보게 되었다. OAuth2.0 로그인은 소셜로그인으로 편리하게 사용할 수 있다는 장점이 있는 옵션인데 이를 구현해보게 되어서 좋은 경험이 된 것 같다.
출처 및 참고 : https://lotuus.tistory.com/83