요구사항
- OAuth2를 이용한 소셜 로그인 처리
OAuth2 : 외부 서비스로 소셜 로그인을 이용해 사용자 연동 처리를 하는 프로토콜
프로젝트를 위한 설정
- 카카오 로그인을 위한 설정
- OAuth2가 카카오 소셜 로그인을 처리하는 과정
- 소셜 로그인 후처리
1. 카카오 로그인을 위한 설정
- 애플리케이션 등록 : https://developers.kakao.com/
- 등록한 애플리케이션에 생성된 REST API 키 복사
- 플랫폼 : Web, 사이트 도메인 : http://localhost:8080 설정
- Redirect URI 지정 : http://localhost:8080/login/oauth2/code/kakao
- 로그인 활성화 후, 카카오 로그인 보안 항목에서 'Client Secret'으로 생성한 키 복사
2. OAuth2가 카카오 소셜 로그인을 처리하는 과정
- REST 방식
- 문자열로 된 토큰을 주고 받는 방식으로 이루어진다.
- 발행 후, 발행한 토큰이 맞는지 검사하는 방식으로 서비스 간 데이터를 교환
- 카카오에서 발급받은 REST API 키를 애플리케이션에 전달해, 인가코드를 받는다.
- 받은 인가코드를 '리다이렉트 URI'로 지정된 곳으로 전달 (정해진 패턴의 리다이렉트 URI)
- 인가코드 + 비밀키 = Access Token (접근 토큰)생성
- 접근 토큰으로 사용자 인증 (사용자의 정보를 요청)
스프링 부트에서 로그인 연동 설정
- build.gradle에 spring-boot-starter-oauth2-client 라이브러리 추가
- application.properties에 연동을 위한 설정 추가
- 카카오 공급자에 대한 정보
- 카카오 인증에 필요한 애플리케이션의 정보
- 카카오 인증시에 사용하는 메서드와 범위 그리고 사용자 정보
- CustomSecurityConfig OAuth2 사용 설정
- 로그인 페이지에 카카오 로그인 링크 추가
- OAuth2UserService 인터페이스 구현
1-1. build.gradle에 spring-boot-starter-oauth2-client 라이브러리 추가
//스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
//OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2-1. 소셜 로그인 공급자인 카카오에 대한 URI정보
- 인증에 필요한 카카오 URI
- 사용자 구분 정보
- 카카오 토큰 URI
- 카카오 유저 관련 URI
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
2-2. 카카오 인증에 필요한 애플리케이션의 정보
- 인증에 사용하는 소셜 로그인 대상
- 인가시 필요한 타입 : 인가코드
- 인증시 리다이렉트 할 URI
- 카카오 설정 과정에서 만들어진 REST API 키 값
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.redirect_uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-id=REST API 키
2-3. 카카오 인증시에 사용하는 메서드와 전달할 사용자 정보 범위
- 보안 설정시 생성된 값 (비밀키)
- 인증시 사용하는 HTTP 메서드
- 인증 완료시 전달할 사용자 정보
spring.security.oauth2.client.registration.kakao.client-secret=설정된 비밀키
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email
3. CustomSecurityConfig OAuth2 사용 설정
-카카오 로그인 방식 추가
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//remember-me 기능을 위한 쿠키 생성
http.rememberMe()
.key("12345678")
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60*60*24*30);
//csrf 토큰 비활성화
http.csrf().disable();
//403 에러를 커스텀 핸들러로 예외처리
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
//카카오로 로그인 추가
http.oauth2Login().loginPage("/member/login");
return http.build();
}
4. login.html 변경
-추가한 카카오로 로그인하는 링크 추가
<div class="card align-self-center">
<div class="card-header">
LOGIN Page
</div>
<div class="card-body">
<!-- 문자열 로그아웃이 null이 아니면 로그아웃 메시지 출력-->
<th:block th:if="${param.logout != null}">
<h1>Logout........</h1>
</th:block>
<!-- 문자열 로그아웃이 null이면 로그인 화면 출력-->
<form id="registerForm" action="/member/login" method="post" th:if="${param.logout==null}">
<div class="input-group mb-3">
<span class="input-group-text">아이디</span>
<input type="text" name="username" class="form-control" placeholder="USER ID">
</div>
<div class="input-group mb-3">
<span class="input-group-text">패스워드</span>
<input type="text" name="password" class="form-control" placeholder="PASSWORD">
</div>
<div class="input-group mb-3 ">
<input class="form-check-input" type="checkbox" name="remember-me">
<label class="form-check-label">
자동 로그인
</label>
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
</div>
</div>
</form>
<!-- 카카오로 로그인하는 링크-->
<a href="/oauth2/authorization/kakao">KAKAO</a>
5. 로그인 연동해 이메일 구하는 기능
-OAuth2UserService 인터페이스 구현
-OAuth2UserService를 쉽게 구현하기 위해, 하위 클래스인 DefaultOAuth2UserService를 상속해서 구현
(1) DefaultOAuth2UserService를 상속하는 CustomOAuth2UserService 클래스 선언
-loadUser() 메서드 오버라이딩
- 메서드 명 : loadUser
- 리턴 타입 : OAuth2User
- 파라미터 : OAuth2UserRequest
- 예외처리 : OAuth2AuthenticationException
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
//상속한 클래스의 생성자 호출
return super.loadUser(userRequest);
}
}
(2) loadUser()에서 연동된 결과를 파라미터인 OAuth2UserRequest로 전달
- loadUser() 메서드 역할 :
유저이름 출력, userRequest를 OAuth2User타입으로 변환, OAuth2User의 사용자 정보를 Map으로 담는다.
기능 순서
- 유저이름 출력
- 파라미터로 getClientRegistration() 호출해서 인증 정보 저장
- 인증정보로 getClientName() 호출해서 유저이름 출력
- userRequest를 OAuth2User타입으로 변환
- loadUser() 상속한 클래스 생성자 호출해 리턴타입인 OAuth2로 변환
- 파라미터에 getAttribute() 호출해 사용자 정보를 Map으로 생성
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.Map;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
log.info("oauth2 user...");
//유저 인증 정보 변수
ClientRegistration clientRegistration=userRequest.getClientRegistration();
//유저 이름 변수
String clientName = clientRegistration.getClientName();
log.info("NAME : "+ clientName);
//상속한 클래스의 생성자 호출
//return super.loadUser(userRequest);
OAuth2User oAuth2User=super.loadUser(userRequest);
//사용자 정보를 Map으로 저장
Map<String, Object> paramMap=oAuth2User.getAttributes();
paramMap.forEach((k,v)->{
log.info("---");
log.info(k+":"+v);
});
return oAuth2User;
}
}
(3) 다른 소셜 로그인에서도 사용할 수 있도록 처리
-clientRegistration.getClientName()를 통해 각각의 소셜 서비스에 연결한다.
변경 전,
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.Map;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
log.info("oauth2 user...");
//유저 인증 정보 변수
ClientRegistration clientRegistration=userRequest.getClientRegistration();
//유저 이름 변수
String clientName = clientRegistration.getClientName();
log.info("NAME : "+ clientName);
//상속한 클래스의 생성자 호출
//return super.loadUser(userRequest);
OAuth2User oAuth2User=super.loadUser(userRequest);
//사용자 정보를 Map으로 저장
Map<String, Object> paramMap=oAuth2User.getAttributes();
paramMap.forEach((k,v)->{
log.info("---");
log.info(k+":"+v);
});
return oAuth2User;
}
}
변경 후,
-email 정보를 연동된 서비스에서 가져온다.
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
log.info("oauth2 user...");
//유저 인증 정보 변수
ClientRegistration clientRegistration=userRequest.getClientRegistration();
//유저 이름 변수
String clientName = clientRegistration.getClientName();
log.info("NAME : "+ clientName);
OAuth2User oAuth2User=super.loadUser(userRequest);
//사용자 정보를 Map으로 저장
Map<String, Object> paramMap=oAuth2User.getAttributes();
// 다른 소셜 서비스도 사용 가능하도록 변경
//email 변수를 선언 및 null로 초기화
String email = null;
switch(clientName){
case "kakao":
email=getKakaoEmail(paramMap);
break;
}
log.info("===");
log.info(email);
log.info("===");
return oAuth2User;
}
//카카오 소셜 로그인시 이메일 정보가져오기
private String getKakaoEmail(Map<String, Object> paramMap){
log.info("KAKAO---");
//kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
Object value = paramMap.get("kakao_account");
log.info(value);
//순서를 유지하는 해시맵으로 정보 저장
LinkedHashMap accountMap = (LinkedHashMap) value;
String email = (String) accountMap.get("email");
log.info("email..."+email);
return email;
}
}
3. 소셜 로그인 후처리
- 소셜 로그인한 사용자에 대한 처리
- 소셜 로그인에 사용한 이메일이 존재하는 경우
- 소셜 로그인으로 로그인 완료 : MemberSecurityDTO가 UserDetails와 OAuth2User를 함께 구현
- 소셜 로그인에 사용한 이메일이 존재하지 않는 경우
- 새로운 회원으로 간주하고 회원가입 처리 : 생성한 Member 도메인 객체로 MemberSecurityDTO를 생성해 반환
(social 속성을 true로) - 소셜 변수가 필요한 이유
- 사용자의 이메일을 알아도 로그인시에 false 인 경우에만 조회되므로 로그인 X
- 하지만, 소셜 로그인한 사용자는 일반 로그인을 하기 위해서는 일반 회원으로 전환하는 화면이 필요
- 새로운 회원으로 간주하고 회원가입 처리 : 생성한 Member 도메인 객체로 MemberSecurityDTO를 생성해 반환
- 소셜 로그인에 사용한 이메일이 존재하는 경우
소셜 로그인한 사용자 처리를 위한 구현 순서
- 리포지토리
- DTO
- 서비스
1. 리포지토리에서 email 정보로 회원 정보를 찾을 수 있는 메소드 추가
-@EntityGraph : 지연 로딩 사용시 여러번의 성능 저하를 막기 위해 DB 접근을 최소화하기 위해 사용하는 어노테이션
Lazy 로딩과 @EntityGraph
지연로딩의 경우, 일대다 연관관계에서 게시물 조회시 에러 발생
성능저하 관계없이 문제 해결하는 방법
성능 저하가 발생하지만 간단하게 해결하는 방법
- fetchType.EAGER로 변경
- @Transational : 필요할 때마다 메소드 내에서 쿼리를 여러번 실행
@EntityGraph
지연 로딩 사용시 여러번의 성능 저하를 막기 위해 DB 접근을 최소화하기 위해
attributePaths 속성으로 필요한 데이터들을 한번의 조인으로 select가 이루어지도록 한다.
package org.zerock.b01.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.zerock.b01.domain.Member;
import javax.persistence.Entity;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, String> {
//@EntityGraph로 회원 구분을 가져와서 일대다 연관관계의 지연로딩이어도 최소한의 DB 접근 구현
@EntityGraph(attributePaths = "roleSet")
@Query("select m from Member m where m.mid= :mid and m.social = false")
Optional<Member> getWithRoles(String mid);
//@EntityGraph로 회원 구분을 가져와서 일대다 연관관계의 지연로딩이어도 최소한의 DB 접근 구현
//JpaRepository를 상속해서 쿼리메서드 사용가능
@EntityGraph(attributePaths = "roleSet")
Optional<Member> findByEmail(String email);
}
2. MemberSecurityDTO에 소셜 로그인에서도 사용할 수 있도록 OAuth2User 인터페이스 구현
- 클래스명 : MemberSecurityDTO
- 상속한 클래스 : User
- 구현한 인터페이스 : OAuth2User
- 구현해야할 메서드 : getAttribute(), getName()
- 소셜 로그인 정보를 props 멤버 변수로 선언해 처리
변경 전, User 클래스만 상속한 MemberSecurityDTO
package org.zerock.b01.security.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Getter
@Setter
@ToString
public class MemberSecurityDTO extends User {
private String mid;
private String mpw;
private String email;
private boolean del;
private boolean social;
public MemberSecurityDTO(String username, String password, String email, boolean del, boolean social, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.mid=username;
this.mpw=password;
this.email= email;
this.del=del;
this.social=social;
}
}
변경 후, OAuth2User인터페이스 구현 + 클래스 상속한 MemberSecurityDTO
package org.zerock.b01.security.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Map;
@Getter
@Setter
@ToString
public class MemberSecurityDTO extends User implements OAuth2User {
private String mid;
private String mpw;
private String email;
private boolean del;
private boolean social;
//소셜 로그인 정보를 담은 Map
private Map<String, Object> props;
public MemberSecurityDTO(String username, String password, String email, boolean del, boolean social, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.mid=username;
this.mpw=password;
this.email= email;
this.del=del;
this.social=social;
}
//OAuth2 인터페이스 구현을 위한 메서드 오버라이딩
@Override
public Map<String, Object> getAttributes() {
return this.getProps();
}
@Override
public String getName() {
return this.mid;
}
}
3. CustomOAuth2UserService에 카카오 서비스로부터의 받은 이메일 중복확인
-이메일 중복확인 후, 같은 이메일 가진 사용자 없을 때 : 자동 회원가입 후, MemberSecurityDTO를 반환
-이메일 중복확인 후, 같은 이메일 가진 사용자 있을 때 : 일반 회원으로 로그인하도록 처리
변경 전, CustomOAuth2UserService
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
log.info("oauth2 user...");
//유저 인증 정보 변수
ClientRegistration clientRegistration=userRequest.getClientRegistration();
//유저 이름 변수
String clientName = clientRegistration.getClientName();
log.info("NAME : "+ clientName);
OAuth2User oAuth2User=super.loadUser(userRequest);
//사용자 정보를 Map으로 저장
Map<String, Object> paramMap=oAuth2User.getAttributes();
// 다른 소셜 서비스도 사용 가능하도록 변경
//email 변수를 선언 및 null로 초기화
String email = null;
switch(clientName){
case "kakao":
email=getKakaoEmail(paramMap);
break;
}
log.info("===");
log.info(email);
log.info("===");
return oAuth2User;
}
//카카오 소셜 로그인시 이메일 정보가져오기
private String getKakaoEmail(Map<String, Object> paramMap){
log.info("KAKAO---");
//kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
Object value = paramMap.get("kakao_account");
log.info(value);
//순서를 유지하는 해시맵으로 정보 저장
LinkedHashMap accountMap = (LinkedHashMap) value;
String email = (String) accountMap.get("email");
log.info("email..."+email);
return email;
}
}
변경 후, 카카오 서비스에서 얻은 이메일로 같은 이메일을 가진 사용자 찾기는 메서드 작성
- 메서드 명 : generateDTO
- 리턴타입 : MemberSecurityDTO
- 파라미터 : email, Map<String, Object> paramMap
- 역할 : 소셜 로그인에서 얻은 아이디가 존재하는지 여부 확인 후,
아이디가 존재하지 않으면 : 회원가입 처리 후 MemberSecurity 구성
아이디가 존재하면 : member를 이용해 MemberSecurity 구성
package org.zerock.b01.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.zerock.b01.domain.Member;
import org.zerock.b01.domain.MemberRole;
import org.zerock.b01.repository.MemberRepository;
import org.zerock.b01.security.dto.MemberSecurityDTO;
import org.zerock.b01.service.MemberService;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
//의존성 주입
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
log.info("userRequest...");
log.info(userRequest);
log.info("oauth2 user...");
//유저 인증 정보 변수
ClientRegistration clientRegistration=userRequest.getClientRegistration();
//유저 이름 변수
String clientName = clientRegistration.getClientName();
log.info("NAME : "+ clientName);
OAuth2User oAuth2User=super.loadUser(userRequest);
//사용자 정보를 Map으로 저장
Map<String, Object> paramMap=oAuth2User.getAttributes();
// 다른 소셜 서비스도 사용 가능하도록 변경
//email 변수를 선언 및 null로 초기화
String email = null;
switch(clientName){
case "kakao":
email=getKakaoEmail(paramMap);
break;
}
log.info("===");
log.info(email);
log.info("===");
//카카오에서 얻은 사용자 정보인 이메일을 이용해 회원가입할지, 로그인할지 처리
return generateDTO(email, paramMap);
}
//카카오에서 얻은 이메일로 사용자 회원가입여부 확인
private MemberSecurityDTO generateDTO(String email, Map<String, Object> paramMap){
//카카오에서 얻은 이메일가 리포지토리에 존재여부
Optional<Member> result = memberRepository.findByEmail(email);
//아이디가 존재하지 않는다면 회원 가입 후 MemberSecurityDTO 구성하고 반환한다.
if(result.isEmpty()){
//소셜 회원 추가
//소셜 회원은 mid가 email이고, 비밀번호가 1111 고정 된 채로 회원가입
Member member= Member.builder()
.mid(email)
.mpw(passwordEncoder.encode("1111"))
.email(email)
.social(true)
.build();
//회원 구분 설정
member.addRole(MemberRole.USER);
memberRepository.save(member);
MemberSecurityDTO memberSecurityDTO=
new MemberSecurityDTO(email, "1111", email, false, true,
Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
//카카오에서 받은 사용자 정보를 이용해 소셜 로그인 정보 설정
memberSecurityDTO.setProps(paramMap);
return memberSecurityDTO;
}
//아이디가 존재하면
else{
Member member=result.get();
MemberSecurityDTO memberSecurityDTO=
new MemberSecurityDTO(
member.getMid(),
member.getMpw(),
member.getEmail(),
member.isDel(),
member.isSocial(),
member.getRoleSet()
.stream().map(memberRole -> new SimpleGrantedAuthority("ROLE_"+memberRole.name())).collect(Collectors.toList())
);
return memberSecurityDTO;
}
}
//카카오 소셜 로그인시 이메일 정보가져오기
private String getKakaoEmail(Map<String, Object> paramMap){
log.info("KAKAO---");
//kakao_account라는 키로 접근하는 정보에 이메일 관련 정보를 가지고 있다.
Object value = paramMap.get("kakao_account");
log.info(value);
//순서를 유지하는 해시맵으로 정보 저장
LinkedHashMap accountMap = (LinkedHashMap) value;
String email = (String) accountMap.get("email");
log.info("email..."+email);
return email;
}
}
-> 소셜 로그인은 일반 로그인 불가능
-> 일반 로그인하기 위해서는 로그인 후, 사용자 정보를 일반 회원으로 수정해야 한다.
4. 소셜 로그인 성공시 사용자 패스워드에 따라 사용자 정보 수정 혹은 특정 페이지 이동 처리
-AuthenticationSuccessHandler / AuthenticationSuccessHandler :
스프링 시큐리티에서 로그인 성공과 실패를 커스터마이징하도록 제공하는 인터페이스
(1) 로그인 성공시 처리하기 위해 AuthenticationSuccessHandler 인터페이스를 구현하는 CustomSocialLoginSuccessHandler 클래스 작성
-소셜 로그인 성공 && 기본 비밀번호면 : 비밀번호 수정하는 페이지로 이동
-소셜 로그인 성공만 : 목록 페이지로 이동
- 구현해야할 메서드 명 : onAuthenticationSuccess
- 파라미터 : HttpServletRequest, HttpServletResponse, Authentication
- 예외처리 : IOException, ServletException
- 멤버 변수
- memberSecurityDTO : 파라미터인 Authentication로 getPrincipal() 메서드 호출해 MemberSecurityDTO 변수 선언 및 초기화
- endcodePw : MemberSecurityDTO에서 인코딩한 패스워드 변수 선언 및 초기화
- 역할
- 소셜 로그인 성공 && 기본 비밀번호면 -> 수정페이지로 이동
- 소셜 로그인이 아니거나, 기본 비밀번호가 아니면 -> 목록페이지로 이동
package org.zerock.b01.security.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.zerock.b01.security.dto.MemberSecurityDTO;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Log4j2
@RequiredArgsConstructor
public class CustomSocialLoginSuccessHandler implements AuthenticationSuccessHandler {
private final PasswordEncoder passwordEncoder;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("---");
log.info("CustomLoginSuccessHandler onAuthenticationSuccess ...");
log.info(authentication.getPrincipal());
MemberSecurityDTO memberSecurityDTO = (MemberSecurityDTO) authentication.getPrincipal();
String encodePw = memberSecurityDTO.getMpw();
//소셜 로그인과 기본 패스워드 확인
if(memberSecurityDTO.isSocial() && (memberSecurityDTO.getMpw().equals("1111") || passwordEncoder.matches("1111", memberSecurityDTO.getMpw()))){
log.info("Should Change Password");
log.info("Redirect to Member Modify");
response.sendRedirect("/member/modify");
//return;
}
else{
response.sendRedirect("/board/list");
}
}
}
(2) 시큐리티 설정에 해당 핸들러 추가
-CustomSecurityConfig에 OAuth2를 이용한 로그인 성공 처리시 CustomSocialLoginSuccessHandler 이용하도록 추가
변경 전, filterChain()
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//remember-me 기능을 위한 쿠키 생성
http.rememberMe()
.key("12345678")
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60 * 60 * 24 * 30);
//csrf 토큰 비활성화
http.csrf().disable();
//403 에러를 커스텀 핸들러로 예외처리
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
//카카오로 로그인 추가
http.oauth2Login()
.loginPage("/member/login");
return http.build();
}
변경 후, filterChain()
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//remember-me 기능을 위한 쿠키 생성
http.rememberMe()
.key("12345678")
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60 * 60 * 24 * 30);
//csrf 토큰 비활성화
http.csrf().disable();
//403 에러를 커스텀 핸들러로 예외처리
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
//카카오로 로그인 추가
//+) 카카오로 로그인시 로그인 성공했을 때, 핸들러 호출
http.oauth2Login()
.loginPage("/member/login")
.successHandler(authenticationSuccessHandler());
return http.build();
}
(3) MemberRepository 패스워드 수정 메서드 작성
-소셜 로그인시 기본 패스워드가 인코딩한 값으로 저장된다.
-리포지토리에 패스워드 수정하는 메서드 작성
//패스워드 수정 메서드
@Modifying
@Transactional
@Query("update Member m set m.mpw =:mpw where m.mid =:mid")
void updatePassword(@Param("mpw") String password, @Param("mid") String mid);
- @Query + @Modifying : DML 처리 (insert/update/delete)
(@Query는 기본적으로 select시 사용하지만, @Modifying과 함께 사용해 DML문을 처리한다.)
(4) 패스워드 수정하는 테스트 코드 작성
//멤버 패스워드 수정하는 테스트
@Commit
@Test
public void testUpdate(){
String mid="cookie_00naver.com";
String mpw= passwordEncoder.encode("54321");
memberRepository.updatePassword(mpw, mid);
}
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
8장 -4. 회원 가입 처리 (0) | 2022.12.15 |
---|---|
8장-3. 회원 데이터 처리 (+ UserDetailsService, enum, MSA) (1) | 2022.12.14 |
8장-2. 로그아웃과 자동 로그인 처리 (+ 인증된 사용자 처리, currentUser, AccessDeniedHandler) (0) | 2022.12.14 |
8장-1. 스프링 시큐리티 (+ SecurityFilterChain, webSecurityCustomizer(), @EnableGlobalMethodSecurity, CSRF 토큰) (0) | 2022.12.13 |
7장-5. 이미지 추가를 위한 컨트롤러와 화면 처리 (+ 파일명에 언더바가 들어간 경우 에러 발생) (0) | 2022.12.11 |