728x90
개요
시큐리티를 사용해 OAuth2 로그인을 진행하고자 한다. 나는 그중 카카오와 네이버에 대해 해보겠다.
앞에서 진행한 코드를 그대로 사용해서 이어서 진행한다.
Spring Security + Session 를 사용한 회원가입, 로그인, 로그아웃 구현 :: 미정 (tistory.com)
OAuth2 에 대한 설명은
Spring : OAuth2 로그인 방식 :: 미정 (tistory.com)
이걸 보면 된다.
네이버, 카카오 소셜 로그인 신청
나는 네이버와 카카오에 대해서 신청을 했고
이 분의 영상을 보고 네이버에 대한 신청은 했다.
https://youtu.be/L8yAtjjOhDo?feature=shared
카카오 신청은 이 분의 글을 보고 신청 했다.
[Spring Boot] 로그인 기능 구현 (6) - 카카오 로그인 (OAuth 2.0) — 공대생의 코딩 일기 (tistory.com)
나의 환경
Window 11
intelliJ
java 21
spring Boot 3.3.0
spring Security 6
oauth2
프로젝트 파일 구조
의존성 설치
- Build.gradle
// OAuth2 Client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//타임리프에서 스프링시큐리티를 쓸수있게.
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
implementation 'org.jsoup:jsoup:1.14.3'
//validation사용
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
- Build.gradle 에 security와 OAuth2 의존성을 추가해준다.
- Application.yml
spring:
application:
name: [projectname]
datasource:
url: jdbc:mysql://localhost:3306/[db]
username: [usernmae]
password: [password]
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
security:
oauth2:
client:
registration:
kakao:
client-id: [설정]
client-secret: [설정]
scope:
- account_email
- profile_nickname
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
client-name: Kakao
client-authentication-method: client_secret_post
naver:
client-id: [설정]
client-secret: [설정]
redirect-uri: http://localhost:8080/login/oauth2/code/naver
client-name: Naver
authorization-grant-type: authorization_code
scope:
- name
- email
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
server:
port: 8080
- application.yml 파일에 OAuth2 설정을 해준다.
- 위에서 신청한 네이버와 카카오 설정들을 맞게 설정해준다.
1. 엔티티 수정
- User.java
@Entity
@Table(name = "users")
@Getter @Setter
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String usernick;
@Column(name = "registrationDate", nullable = false)
private LocalDateTime registrationDate = LocalDateTime.now();
private String filename;
private String filepath;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
/**
* Oauth2 관련
*/
// providerId : 구굴,카카오,네이버 로그인 한 유저의 고유 ID가 들어감
private String loginId; // Unique identifier for OAuth users
// provider : google, kakao, naver 이 들어감
private String provider;
}
- 아래 2개의 칼럼을 추가해준다.
- 그리고 위의 엔티티는 최종이고 아래 부분은 이전 엔티티에서 닉네임 컬럼 부분을 보면 usernick이 중복 불가능하게 했지만 그 부분을 수정해줬다.
- 하지만 현재 디비에 값들이 저장된 상태라서 엔티티에서 변경을 한후 sql 구문을 통해 수정해줬다.
// 변경 전
@Column(nullable = false, unique = true)
private String usernick;
// 변경 후
@Column(nullable = false)
private String usernick;
# 1
SHOW INDEX FROM users WHERE Column_name = 'usernick';
# 2 Key_name 의 값을 확인
# 3
ALTER TABLE users DROP INDEX UK5rqu0cqcxxar2go1qlddeia02;
2. Oauth2 응답 객체
- OAuth2Response.java
public interface OAuth2Response {
//제공자 (Ex. naver, google, ...)
String getProvider();
//제공자에서 발급해주는 아이디(번호)
String getProviderId();
//이메일
String getEmail();
//사용자 실명 (설정한 이름)
String getName();
}
- 각 사이트마다 인증 데이터 규격이 다르기 때문에 OAuth2Response 인터페이스를 만들어 구별한다.
- 카카오, 구글, 네이버, 깃허브 등등 이 인터페이스를 상속받아 각각의 규격에 맞게 구현한다.
- 현재 나는 사용자 이메일, 설정 이름만 가져왔고 프로필 이미지, 생일 등등 가져올 수 있다.
Q. 여기서 잠깐!
- 네이버와 카카오, 구글의 사용자 정보 response 를 보자.
getAttributes :
{
id=12345678,
connected_at=2024-07-26T09:56:54Z,
properties={
nickname=엉이
},
kakao_account={
profile_nickname_needs_agreement=false,
profile={nickname=엉이, is_default_nickname=false},
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=엉이@kakao.com
}
}
provider : kakao
getAttributes :
{
resultcode=00,
message=success,
response={
id=12345678,
email=엉이@naver.com,
name=엉이
}
}
provider : naver
getAttributes :
{
resultcode=00,
message=success,
id=12345678,
email=엉이@naver.com,
name=엉이
}
}
provider : google
- 각 사이트마다 출력 값들이 다르다.
- 그러기 때문에 기본적인 형태가 다르기 때문에 위에서 만든 OAuth2Response 인터페이스를 만들어서 네이버, 카카오 응답 객체에 맞게 고칠 수 있도록 기본적인 틀을 만들어 둔다.
- 그럼 카카오, 네이버, 구글 등 OAuth2Response 인터페이스를 상속받아서 구현하면 된다.
- KakaoResponse.java
getAttributes :
{
id=12345678,
connected_at=2024-07-26T09:56:54Z,
properties={
nickname=엉이
},
kakao_account={
profile_nickname_needs_agreement=false,
profile={nickname=엉이, is_default_nickname=false},
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=엉이@kakao.com
}
}
provider : kakao
위의 출력값을 보면서 아래 코드를 이해하자.
public class KakaoResponse implements OAuth2Response{
private Map<String, Object> attribute;
public KakaoResponse(Map<String, Object> attribute) {
this.attribute = attribute;
System.out.println("Kakao attributes: " + attribute);
}
@Override
public String getProvider() {
return "kakao";
}
@Override
public String getProviderId() {
return attribute.get("id").toString();
}
@Override
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>) attribute.get("kakao_account");
return kakaoAccount.get("email").toString();
}
@Override
public String getName() {
Map<String, Object> properties = (Map<String, Object>) attribute.get("properties");
return properties.get("nickname").toString();
}
}
- 카카오는 출력 데이터 내부 안에 kakao_account 라는 데이터가 존재하고 그 안에 사용자 이메일, 이름 등에 대한 값이 있기 때문에 get()으로 값을 꺼내온다.
- NaverResponse.java
getAttributes :
{
resultcode=00,
message=success,
response={
id=12345678,
email=엉이@naver.com,
name=엉이
}
}
provider : naver
위의 출력값을 보면서 아래 코드를 이해하자.
public class NaverResponse implements OAuth2Response{
private final Map<String, Object> attribute;
public NaverResponse(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
System.out.println("naver attributes: " + attribute);
}
@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();
}
}
- 네이버는 데이터 내부 안에 response 라는 데이터가 또 있고 그 안에 사용자의 정보에 대한 값이 저장되어 있기 때문에 get으로 가져와준다.
- googleResponse.java
getAttributes :
{
resultcode=00,
message=success,
id=12345678,
email=엉이@naver.com,
name=엉이
}
}
provider : google
public class GoogleResponse implements OAuth2Response{
private final Map<String, Object> attribute;
public GoogleResponse(Map<String, Object> attribute) {
this.attribute = attribute;
System.out.println("google attributes: " + attribute);
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getProviderId() {
return attribute.get("sub").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
구글에 대해서는 카드 인증이 필요해서 귀찮기 때문에.. 하지는 않았지만 정리를 위해 작성했다. 실제 구글 oauth2는 신청하지 않았기에 사용하지 않았다.
3. OAuth2User
- CustomOAuth2User.java
public class CustomOAuth2User implements OAuth2User {
private final OAuth2Response oAuth2Response;
private final String role;
public CustomOAuth2User(OAuth2Response oAuth2Response, String role) {
this.oAuth2Response = oAuth2Response;
this.role = role;
}
// 현재는 null 했지만 이 값을 지정하면
// 카카오,네이버 등 로그인했으면 거기에 맞는 프로필 사진이나 가져올 수 있음
@Override
public Map<String, Object> getAttributes() {
return null;
}
// role에 해당하는 값
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new SimpleGrantedAuthority(role));
return collection;
}
@Override
public String getName() {
return oAuth2Response.getName();
}
// 사용안함
public String getUsername() {
return oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId();
}
}
- 카카오, 네이버, 구글 서비스로부터 받은 특정 사이트의 응답 값과 롤에 대한 값을 받는 클래스이다.
- 이 클래스를 통해 특정 값과 롤에 대해 정의한다.
4. SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService; // 기본 세션 로그인
private final CustomOauth2UserService customOAuth2UserService; // OAuth2 등록
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.requestMatchers("/api/users/signup", "/api/users/login").permitAll()
.requestMatchers("/oauth-login/admin").hasRole("ADMIN")
.requestMatchers("/oauth-login/info").authenticated()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/api/users/login") // 로그인 페이지의 경로
.loginProcessingUrl("/login") // 로그인 폼이 제출되는 URL
.defaultSuccessUrl("/api/users/home")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
)
.sessionManagement(sessionManagement -> sessionManagement
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
)
.userDetailsService(customUserDetailsService)
.csrf(csrf -> csrf.disable())
.cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(configurationSource()))
// OAuth 등록
.oauth2Login(oauth2 -> oauth2
.loginPage("/api/users/login") // 커스텀 로그인 방식 지정
// customOAuth2UserService 등록
.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))
.failureUrl("/loginFailure")
.defaultSuccessUrl("/api/users/info")
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorization")
)
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
public CorsConfigurationSource configurationSource(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowedMethods(List.of("GET","POST","DELETE"));
source.registerCorsConfiguration("/**",config);
return source;
}
}
- Oauth2 설정을 해준다.
5. OAuth2UserService 구현
- OAuth2UserService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class CustomOauth2UserService extends DefaultOAuth2UserService {
// DefaultOAuth2UserService는 OAuth2UserService의 구현체라서
// DefaultOAuth2UserService 또는 OAuth2UserService 아무거나 상속해도 된다.
private final UserRepository userRepository;
private final RoleRepository roleRepository;
// loadUser --> 네이버나 카카오의 사용자 인증 정보를 받아오는 메서드
// userRequest 를 통해 카카오, 네이버, 구글 등등 인증 데이터가 넘어 올 것이다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println("OAuth2User attributes: " + oAuth2User.getAttributes());
// 네이버, 구글, 깃허브 등등 어떤 어떤 인증 값인지 구별하기 위해 인증 제공자를 구분
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// 각 사이트마다 인증 데이터 규격이 다르기 때문에 OAuth2Response 를 만들어 구별
// 인증 제공자에 따라 OAuth2Response를 생성
OAuth2Response oAuth2Response = null;
// 그 값이 네이버면
if (registrationId.equals("naver")) {
log.info("naver 로그인");
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
// 그 값이 카카오면
} else if (registrationId.equals("kakao")) {
log.info("kakao 로그인");
oAuth2Response = new KakaoResponse(oAuth2User.getAttributes());
// 둘다 아니면
} else {
System.out.println("로그인 실패");
throw new IllegalArgumentException("지원하지 않는 로그인 제공자입니다.");
}
// 데이터 베이스 저장 관련 구현
String provider = oAuth2Response.getProvider();
String providerId = oAuth2Response.getProviderId();
String username = provider + " " + providerId;
Optional<User> userOptional = userRepository.findByUsername(username);
String roleName = "USER";
Optional<Role> roleOptional = roleRepository.findByName(roleName);
Role role;
if (roleOptional.isEmpty()) {
role = new Role(roleName);
role = roleRepository.save(role); // Save the new role and get the persisted instance
} else {
role = roleOptional.get();
}
User user;
if (userOptional.isEmpty()) {
user = User.builder()
.username(username)
.email(oAuth2Response.getEmail())
.roles(Set.of(role))
.loginId(oAuth2Response.getProviderId())
.provider(oAuth2Response.getProvider())
.password("defaultPassword")
.registrationDate(LocalDateTime.now())
.usernick(oAuth2Response.getName())
.build();
userRepository.save(user);
} else {
user = userOptional.get();
user.setUsername(username);
user.setEmail(oAuth2Response.getEmail());
user.setLoginId(oAuth2Response.getProviderId());
user.setProvider(oAuth2Response.getProvider());
user.getRoles().add(role);
user.setRegistrationDate(LocalDateTime.now());
user.setUsernick(oAuth2Response.getName());
userRepository.save(user);
roleName = user.getRoles().iterator().next().getName();
}
System.out.println("User saved: " + user);
// 특정 사이트의 응답 값과 역할을 받는 CustomOAuth2User 클래스
// 인증된 사용자 정보를 반환
return new CustomOAuth2User(oAuth2Response, roleName);
}
}
- Spring Security의 OAuth2 인증을 처리하는 커스텀 서비스이다.
- 주로 네이버, 카카오, 구글 등의 OAuth2 제공자에서 사용자 인증 정보를 받아와 데이터베이스에 저장하거나 업데이트하는 역할을 한다.
- loadUser는 네이버나 카카오의 사용자 인증 정보를 받아오는 메서드이다.
- 외부 사이트로부터 사용자 정보를 받아오고 그 값을 디비에 저장하는 클래스이다.
6. 컨트롤러
- UserController.java
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/users")
@Slf4j
public class UserController {
private final UserServiceImpl userServiceimpl;
private final CustomUserDetailsService customUserDetailsService;
@GetMapping("/signup")
public String signup(Model model) {
model.addAttribute("user", new UserDto());
return "/user/singupform";
}
@PostMapping("/signup")
public String signup(@ModelAttribute("user") UserDto userDto,
RedirectAttributes redirectAttributes) {
try {
userServiceimpl.signUp(userDto);
redirectAttributes.addFlashAttribute("success", " 성공적으로 회원가입 됐습니다.");
return "redirect:/api/users/login";
} catch (Exception e) {
log.error("회원가입 오류 : {} " , e.getMessage());
redirectAttributes.addFlashAttribute("error", "회원가입에 실패했습니다." + e.getMessage());
e.printStackTrace();
}
return "redirect:/api/users/login";
}
@GetMapping("/login")
public String showLoginForm() {
return "/user/loginform";
}
// 일반 로그인 회원의 정보 가져오기
@GetMapping("/home")
public String index(Model model, Authentication authentication) {
String username = authentication.getName();
Optional<User> userOptional = userServiceimpl.findByEmail(username);
if (userOptional.isPresent()) {
model.addAttribute("user", userOptional.get());
return "user/home"; // user/home.html 템플릿으로 이동
}
return "redirect:/api/users/login";
}
// oauth2 유저의 정보 가져오기
@GetMapping("/info")
public String info(Model model, Authentication authentication) {
CustomOAuth2User userDetails = (CustomOAuth2User) authentication.getPrincipal();
model.addAttribute("name", userDetails);
return "/user/info";
}
}
- 해당 컨트롤러에서 /api/users/info앤트포인트의 값으로 화면이 보여질 것이다.
7. 로그인 폼 및 로그인 한 회원의 정보를 보여주기
- loginform.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<!-- 시큐리티는 x-www-form-url-encoded 타입만 인식 -->
<form action="/login" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button>로그인</button>
</form>
<a href="/oauth2/authorization/kakao">카카오 로그인</a>
<a href="/oauth2/authorization/naver">네이버 로그인</a>
<a href="/oauth2/authorization/google">구글 로그인</a>
<a href="/api/users/signup">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
- info.html
- OAuth2 로그인 후 보여지는 화면이다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>메인 홈 입니다.</title>
</head>
<body>
<h2>로그인에 성공하셨어요 축하합니다.</h2>
<h1> 당신은 OAUth2 로그인을 하셨습니다. </h1>
<p>안녕하세요, <span th:text="${name}"></span>님</p>
<p>안녕하세요, 당신이 로그인한 플랫폼 : <span th:text="${name.username}"></span>님</p>
<p>안녕하세요, 플랫폼 이름 : <span th:text="${name.name}"></span>님</p>
<p>안녕하세요, 당신의 등급 : <span th:text="${name.authorities}"></span>님</p>
</body>
</html>
- home.html
- 일반 로그인 후 보여지는 화면이다.
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>메인 홈 입니다.</title>
</head>
<body>
<h2>로그인에 성공하셨어요 축하합니다.</h2>
<p>안녕하세요, 로그인 : <span th:text="${user.email}"></span>님</p>
<p>이름: <span th:text="${user.username}"></span></p>
</body>
</html>
이렇게 하면 기본 Oauth2 세션 로그인은 성공할 것이고 이제 여기에 jwt를 사용해서 구현해볼 것이다.
728x90
'Spring > Security' 카테고리의 다른 글
Spring Security + jwt 토큰 (with 쿠키) :: http-only 설정방법 (0) | 2024.08.02 |
---|---|
Spring : JWT를 왜 사용해야 할까? (0) | 2024.07.27 |
Spring : OAuth2 로그인 방식 (0) | 2024.07.26 |
Spring Security + Session 를 사용한 회원가입, 로그인, 로그아웃 구현 (0) | 2024.07.26 |
Spring Security : SecurityConfig 설정 정리 (2) | 2024.07.23 |