■ 환경
이 프로젝트는 (세션방식과 + JW T 방식) 두가지를 복합적으로 적용한 프로젝트이다.
- spring boot 3.2.3
- JPA, gradle
- h2, mariadb , redis
- 프론트엔드 : thymelef, react 18.2
■ 소스
https://github.com/braverokmc79/spring-boot-react-oauth2
코드 보기
스프링부트 && 리액트
https://github.dev/braverokmc79/spring-boot-react-oauth2
[Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)
=>
https://deeplify.dev/back-end/spring/oauth2-social-login
Spring Boot Github 소셜 로그인 구현하기 (RestTemplate · WebClient)
https://inkyu-yoon.github.io/docs/Language/SpringBoot/GithubLogin
※ 1. Github OAuth 인증 흐름과 사전 준비
1. https://github.com/login/oauth/authorize?client_id={발급받은 client_ID} 주소를 사용자에게 띄워준다.
2. 사용자는 깃허브 로그인을 통해서 인증을 한다.
3. 인증을 성공할 시, 깃허브는 {우리가 설정한 콜백 URL}?code={인증코드} 로 code값을 쿼리 파라미터 형태로 보내준다.
4. 받은 code를 이용해서 https://github.com/login/oauth/access_token 로 {client_ID,client_Secret,code}를 POST요청으로 전송한다.
5. 깃허브는 access_token을 응답해서 서버측으로 보내준다.
6. 서버는 access_token을 https://api.github.com/user 로 담아서 GET 요청을 보낸다.
7. 깃허브는 로그인한 사용자의 정보를 서버측으로 보내준다.
8. 받은 사용자 정보를 사용한다.(회원가입 혹은 로그인)
Settings > Developer settings > New GitHub App 으로 OAuth 인증 기능 구현을 위한 Client Id 와 Client Secret 을 발급받아야 한다.

Homepage URL은 일단 로컬 환경에서 구현해볼것이기 때문에 localhost:8080 으로 해두었다.
Callback URL 은 어떤 사용자가 깃허브 로그인을 성공하면 깃허브 측에서 Code 를 쿼리 파라미터로 보내주는데, 그 파라미터를 받을 주소이다.
나는 http://localhost:8080/oauth2/redirect 로 설정하였다.
http://localhost:8080/oauth2/redirect?code={코드~~} 이런식으로 리다이렉트 될 것이다.
정상적으로 등록을 마치면 Client ID와 Client Secret정보를 얻을 수 있다.
Client Secret은 민감정보 이므로 노출되지 않도록 주의하자.
※ 2. 흐름
1) oauth2 로그인 페이지 -> oauth2 github 에서 인증후 로그인되면 callback 호출
2)oauth2 callback 로 호출
.redirectionEndpoint(rE ->rE.baseUri("/oauth2/callback/*"))
3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다 .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(oAuth2UserService))
4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다. .successHandler(oAuth2SuccessHandler)
5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면
//http://localhost:8080/oauth2/authorization/github url 주소와 동일하게
//http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다.
.authorizationEndpoint(anf -> anf.baseUri("/api/auth/authorize"))
oauth2 기본 설정 확인 방법 :
http://localhost:포트번호/oauth2/authorization/github
스프링부트에서 기본적으로 oauth2 설정만 해주면 다음과 같이 localhost:포토번호/oauth2/authorization/github 를 입력하면
github 로그인화면으로 이동 처리 된다.
이것을 baseUri("/api/auth/authorize") 으로 임의 url 주소만 설정만 해주면 해당
/oauth2/authorization/ 주소는 작동이 안되며 다음 /api/auth/authorize 주소로
http://localhost:5000/api/auth/authorize 주소로 oauth2 인증 진행처리 된다.
즉, baseUri 디폴트 값은 /oauth2/authorization 이다.
http://localhost:5000/oauth2/authorization/github
http://localhost:5000/api/auth/authorize
1. 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
2.properties 설정
1) application.properties
개발 로컬에 적용
spring.profiles.active=dev
배포 실질 운영시 보통 prod 를 사용하나 여기서는 real 이라는 이름으로 사용했다.
spring.profiles.active=real
2)개발 properties
application-dev.properties
#애플리케이션 포트 설정
server.port = 5000
spring.output.ansi.enabled=always
spring.jpa.open-in-view=false
#redis 설정
spring.data.redis.lettuce.pool.max-active=10
spring.data.redis.lettuce.pool.max-idle=10
spring.data.redis.lettuce.pool.min-idle=2
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=test1234
#MariaDB server
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/shop?serverTimezone=UTF-8&allowPublicKeyRetrieval=true&useSSL=false
spring.datasource.username=shop
spring.datasource.password=1111
#접근토큰시간: 5시간 #60*60*5*1000 =18000000 #샘플테스트 :30초 ==>30*1000=30000
spring.jwt.token.access-expiration-time=60000
#갱신토큰시간: 7일 #60*60*24*7*1000=604800000 #샘플테스트: 1분 ===>60000
spring.jwt.token.refresh-expiration-time=120000
#oauth2 frontend 주소
serverRedirectFrontUrl=http://localhost:3000
#깃허브
#http://localhost:5000/oauth2/authorization/github
spring.security.oauth2.client.registration.github.client-id=752037935070eb6d2dc3
spring.security.oauth2.client.registration.github.client-secret=e0f5cde1030674aa98308a8ca9475692e310514a
spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/oauth2/callback/{registrationId}
spring.security.oauth2.client.registration.github.scope=user:email, read:user
#provider 는 리소스 제공자인 github 에 대한 정보를 명시한 것, 따라서 Todo 애플리케이션이 소셜 로그인요청을 할때 해당 주소로 리다이렉트 한다.
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
#token-uri 는 깃허브에 액세스 가능한 엑세스 토큰을 받아오기 위한 주소
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
#유저의 정보를 가져오기 위해서는 액세스 토큰이필요하므로 우리는 token-uri 를 이용해 먼저 액세스 토큰을 받은후, user-info-uri 로 사용자의 정보를 요청할때 토큰을 함께 보낸다.
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
#구글
spring.security.oauth2.client.registration.google.client-id=dsa
spring.security.oauth2.client.registration.google.client-secret=313123
spring.security.oauth2.client.registration.google.scope=profile,email
#네이버
# registration
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/oauth/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email.profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
# 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
## 카카오 ##
# registration
spring.security.oauth2.client.registration.kakao.client-id=클라이언트아이디
spring.security.oauth2.client.registration.kakao.client-secret=클라이언트시크릿
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email,profile_image
spring.security.oauth2.client.registration.kakao.client-name=Kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
# provider
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
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
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
#실행되는 쿼리 콘솔 출력
spring.jpa.properties.hibernate.show_sql=true
#콘솔창에 출력되는 쿼리를 가독성이 좋게 포맷팅
spring.jpa.properties.hibernate.format_sql=true
#쿼리에 물음표로 출력되는 바인드 파라미터 출력
logging.level.org.hibernate.type.descriptor.sql=trace
#create , update , none
spring.jpa.hibernate.ddl-auto=update
#파일 한 개당 최대 사이즈
spring.servlet.multipart.maxFileSize=20MB
#요청당 최대 파일 크기
spring.servlet.multipart.maxRequestSize=100MB
#상품 이미지 업로드 경로
itemImgLocation=/uploads/shop/item
#리소스 업로드 경로
uploadPath=file:/uploads/shop/
#기본 batch size 설정
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
3)운영 properties
application-real.properties
리다이렉트할 주소 적어준다.
serverRedirectFrontUrl=https://ma7front.p-e.kr
spring.security.oauth2.client.registration.github.redirect-uri 에서 콜백할 주소도 직접 적어준다.
로컬에서는 다음과 같이설정해도 적용이 되나, 운영서버에서는 작동이 안된다.
spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/oauth2/callback/{registrationId}
{baseUrl}/oauth2/callback/{registrationId} 과 같이 설정할경우 nginx 와 연동되 실질적인 운영에서는
invalid_request 에러가 발생한다.
참조 : [OAuth2] 구글 로그인 400 오류 : invalid_request 에러 해결책
따라서, 직접적으로 frontend 주소를 적도록 하자
spring.security.oauth2.client.registration.github.redirect-uri=https://ma7server.p-e.kr/oauth2/callback/{registrationId}
#oauth2 frontend 주소
serverRedirectFrontUrl=https://ma7front.p-e.kr
#깃허브
#http://localhost:5000/oauth2/authorization/github
spring.security.oauth2.client.registration.github.client-id=깃허브Client_ID
spring.security.oauth2.client.registration.github.client-secret=깃허브Client_SECRET
spring.security.oauth2.client.registration.github.redirect-uri=https://ma7server.p-e.kr/oauth2/callback/{registrationId}
spring.security.oauth2.client.registration.github.scope=user:email, read:user
#provider 는 리소스 제공자인 github 에 대한 정보를 명시한 것, 따라서 Todo 애플리케이션이 소셜 로그인요청을 할때 해당 주소로 리다이렉트 한다.
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
#token-uri 는 깃허브에 액세스 가능한 엑세스 토큰을 받아오기 위한 주소
spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
#유저의 정보를 가져오기 위해서는 액세스 토큰이필요하므로 우리는 token-uri 를 이용해 먼저 액세스 토큰을 받은후, user-info-uri 로 사용자의 정보를 요청할때 토큰을 함께 보낸다.
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user
3.SecurityConfig 설정
securityCofig 에 다음 코드를 추가해 준다.
1~ 5번 코드 순서 처럼 oauth2 인증처리 된다.
//oauth2Login 로그인 처리
http.oauth2Login(oauth2Configurer -> oauth2Configurer
//1) oauth2 로그인 페이지 -> oauth2 github 에서 인증후 로그인되면 callback 호출
//.loginPage("/oauth2/login")
//2)oauth2 callback 로 호출
.redirectionEndpoint(rE ->rE.baseUri("/oauth2/callback/*"))
//3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(oAuth2UserService))
//4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다.
.successHandler(oAuth2SuccessHandler)
//5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면
//http://localhost:8080/oauth2/authorization/github url 주소와 동일하게
//http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다.
.authorizationEndpoint(anf -> anf.baseUri("/api/auth/authorize"))
)
//인증 실패시 Oauth2 흐름으로 넘어가는 것을 막고 응답코드 403을 반환처리
.exceptionHandling(exceptionConfig->exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));
전체 securityCofig
package com.shop.config;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.shop.config.filter.JwtAuthenticationFilter;
import com.shop.config.oauth2.OAuth2SuccessHandler;
import com.shop.exception.CustomAuthenticationEntryPoint;
import com.shop.service.api.oauth2.OAuth2UserService;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
//@EnableGlobalMethodSecurity(prePostEnabled = true)//spring boot 3 이상 부터 기본값 true 업데이트 됩
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final OAuth2UserService oAuth2UserService;
//OAuth2 로그인 성공시 토큰 생성
private final OAuth2SuccessHandler oAuth2SuccessHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public JPAQueryFactory queryFactory(EntityManager em){
return new JPAQueryFactory(em);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//JWT 를 사용하면 csrf 보안이 설정 필요없다. 그러나 여기 프로젝트에서는 세션방식과 jwt 방식을 둘다적용 중이라 특정 페이지만 제외 처리
http.csrf(c -> {
c.ignoringRequestMatchers("/admin/**","/api/**", "/oauth2/**" ,"/error/**");
});
//1.csrf 사용하지 않을 경우 다음과 같이 설정
//http.csrf(AbstractHttpConfigurer::disable);
//2. h2 접근 및 iframe 접근을 위한 SameOrigin (프레임 허용) & 보안 강화 설정 할경우
//http.headers((headers) -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));
//3.HTTP 기본 인증 만 할경우 다음과 같이 처리
//httpSecurity.formLogin(AbstractHttpConfigurer::disable);
//3.jwt token 만 인증 처리 할경우 basic 인증을 disable 한다. 그러나 이 프로젝트는 세션+jwt 이라 disable 설정은 하지 않는다.
//httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
//4.JWT 만 사용할경우 세션 관리 상태 없음 설정 그러나, 이 프로젝트는 세션 + JWT 를 사용하지 때문에 주석
//http.sessionManagement(sessionManagementConfigurer ->sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
//세션방식의 form 로그인 처리
http.formLogin(login-> login
.loginPage("/members/login")
.defaultSuccessUrl("/", true)
.usernameParameter("email")
.failureUrl("/members/login/error"))
.logout(logoutConfig ->logoutConfig.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")).logoutSuccessUrl("/"))
.exceptionHandling(exceptionConfig-> exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));
http.authorizeHttpRequests(request->request
.requestMatchers("/css/**", "/js/**", "/img/**","/images/**").permitAll()
.requestMatchers("/", "/members/**", "/item/**", "/main/**", "/error/**" ).permitAll()
//JWT 일반 접속 설정
.requestMatchers("/auth/**", "/oauth2/**").permitAll()
.requestMatchers("/api/todo/**" ,"/api/auth/**", "/api/oauth2/**" ).permitAll()
//JWT 관리자 페이지 설정
.requestMatchers( "/api/admin/**").hasAuthority("ADMIN")
//세션방식 --> 관리자 페이지는 설정
.requestMatchers("/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
);
//api 페이지만 JWT 필터 설정(jwtAuthenticationFilter 에서 shouldNotFilter 메서드로 세션 페이지는 필터를 제외 시켰다.)
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
//에외 처리
.exceptionHandling(exceptionConfig->exceptionConfig.authenticationEntryPoint(new CustomAuthenticationEntryPoint()) );
//oauth2Login 로그인 처리
http.oauth2Login(oauth2Configurer -> oauth2Configurer
//1) oauth2 로그인 페이지 -> oauth2 github 에서 인증후 로그인되면 callback 호출
//.loginPage("/oauth2/login")
//2)oauth2 callback 로 호출
.redirectionEndpoint(rE ->rE.baseUri("/oauth2/callback/*"))
//3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(oAuth2UserService))
//4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다.
.successHandler(oAuth2SuccessHandler)
//5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면
//http://localhost:8080/oauth2/authorization/github url 주소와 동일하게
//http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다.
.authorizationEndpoint(anf -> anf.baseUri("/api/auth/authorize"))
)
//인증 실패시 Oauth2 흐름으로 넘어가는 것을 막고 응답코드 403을 반환처리
.exceptionHandling(exceptionConfig->exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));
return http.build();
}
}
위와 같이 시큐리티를 설정했다면 http://localhost:포토번호/oauth2/authorization/github 로 입력해서 들어가보면
http://localhost:포토번호 으로 정상적으로 디라이렉트 하는 것을 확인할 수 있다.
4.OAuth2UserService
Member
package com.shop.entity;
import com.shop.constant.Role;
import com.shop.dto.MemberFormDto;
import com.shop.entity.base.BaseEntity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
@Entity
@Table(name = "member")
@Getter
@Setter
@ToString
public class Member extends BaseEntity {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String name;
@Column(unique = true)
private String email;
private String password;
private String address;
@Enumerated(EnumType.STRING)
private Role role;
private String authProvider; //이후 OAuth 에서 사용할 유저 정보 제공자 : github
private String authProviderId;
public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) {
Member member = new Member();
member.setName(memberFormDto.getName());
member.setUsername(memberFormDto.getUsername());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.USER);
//member.setRole(Role.ADMIN);
return member;
}
public static Member createMember(MemberFormDto memberFormDto) {
Member member = new Member();
member.setName(memberFormDto.getName());
member.setUsername(memberFormDto.getUsername());
member.setEmail(memberFormDto.getEmail());
member.setAddress(memberFormDto.getAddress());
member.setPassword(memberFormDto.getPassword());
member.setRole(Role.USER);
return member;
}
}
OAuth2UserService
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
final OAuth2User oAuth2User =super.loadUser(userRequest);
final String username=oAuth2User.getAttributes().get("login").toString();
final String authProviderId=oAuth2User.getAttributes().get("id").toString();
String email=null;
if(oAuth2User.getAttributes().get("email")!=null){
email=oAuth2User.getAttributes().get("email").toString();
}
final String authProvider=userRequest.getClientRegistration().getClientName();
String OAUTH2_ID= authProvider+"_"+authProviderId;
Member member=null;
if(memberRepository.existsByAuthProviderId(OAUTH2_ID)){
member = memberRepository.findMemberByAuthProviderId(OAUTH2_ID);
}else{
if(StringUtils.hasText(email)){
Member memberEntity = memberRepository.findByEmail(email);
if( memberRepository.findByEmail(email)!=null){
memberEntity.setAuthProvider(authProvider);
memberEntity.setAuthProviderId(OAUTH2_ID);
member=memberEntity;
}
}
}
if(member==null) member=createOauth2Member( authProvider , username, OAUTH2_ID, email);
return new PrincipalDetails(member, oAuth2User.getAttributes());
}
package com.shop.service.api.oauth2;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.shop.config.auth.PrincipalDetails;
import com.shop.constant.Role;
import com.shop.dto.MemberDto;
import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
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;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class OAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("★★★★★★★★★★ OAuth2UserService loadUse");
//1.DefaultOauth2UserService 의 기존 loadUser 를 호출한다. 이 메서드가 user-info-uri 를 이용해 사용자 정보를 가져오는 부분이다.
final OAuth2User oAuth2User =super.loadUser(userRequest);
try{
//2.디버깅을 돕기 위해 OAuth2User 사용자 정보를 출력
log.info("******** OAuth2User attributes : {}", new ObjectMapper().writeValueAsString(oAuth2User.getAttributes())) ;
}catch (Exception e){
e.printStackTrace();
}
log.info("******** OAuth2UserService loadUser : {} , getAttributes() : {}", oAuth2User , oAuth2User.getAttributes());
//3.login 필드를 가져온다.
final String username = oAuth2User.getAttributes().get("login").toString();
final String authProviderId = oAuth2User.getAttributes().get("id").toString();
String email =null;
if(oAuth2User.getAttributes().get("email")!=null){
email = oAuth2User.getAttributes().get("email").toString();
}
final String authProvider=userRequest.getClientRegistration().getClientName();
//OAUTH2_ID = Oauth2(naver,github,google)_authProviderId ex) Github_1234
String OAUTH2_ID= authProvider+"_"+authProviderId;
/**
각 소셜 로그인 제공자가 반환하는 유저 정보, 즉 attributes 에 들어 있는 내용은 제공자 마다 각각 다르다.
email을 아이디로 사용하는 제공자는 email 필드가 있을 것이고, 깃허브의 경우에는 login 필드가 있다.
따라서 여러 소셜 로그인과 통홥하려면 이 부분을 알맞게 파싱해야 한다.
*
*/
//authProvider :GitHub
Member member=null;
//1.OAUTH2_ID 를 통해 , DB 회원 테이블에 OAUTH2 로 가입된 회원이 존재하면 해당 정보를 가져온다.(바로 로그인 처리)
if(memberRepository.existsByAuthProviderId(OAUTH2_ID)){
log.info("======>1.기존에 가입된 OAUTH2 회원 OAUTH2_ID 확인후 바로 로그인 처리");
member = memberRepository.findMemberByAuthProviderId(OAUTH2_ID);
}else{
//2.신규가입처리 - 1) DB 회원 테이블에 OAUTH2 로 가입된 정보가 없다. 따라서 신규가입처리 진행하는데, 이메일이 있을 경우를 다음과 같이 확인 가입처리 한다.
//OAUTH2 에서 가져온 email 정보가 존재 한다. 해당 이메일이 DB 에 존재하는지 확인 ,
//만약에 존재 할경우 해당 정보를 가져와서 AuthProvider , AuthProviderId 컬럼값만 업데이트 처리한다.
if(StringUtils.hasText(email)){
// DB 에서 해당 이메일로 회원정보를 가져온다.
Member memberEntity = memberRepository.findByEmail(email);
if(memberEntity!=null){
//1)널이 아니면 DB 데이터에 업데이트 처리
log.info("======>2-1.OAUTH 이메일 로 업데이트 후 회원가입 처리");
memberEntity.setAuthProvider(authProvider);
memberEntity.setAuthProviderId(OAUTH2_ID);
member=memberEntity;
}
}
}
if(member==null){
//2.신규가입처리 - 2) 그냥 전부 새롭게 신규 등록
log.info("======>2-2.OAUTH 새롭게 신규 등록");
member=createOauth2Member( authProvider , username, OAUTH2_ID, email);
}
PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes());
log.info("========================2 Oauth2User 토큰 반환시 정보값 ================ {}", principalDetails.getMember().getId());
return principalDetails;
}
//oauth2 DB 등록
private Member createOauth2Member(String authProvider ,String username, String OAUTH2_ID, String email){
MemberDto memberDto = MemberDto.builder()
.username(authProvider+"_"+username)
.email(email)
.authProvider(authProvider)
.authProviderId(OAUTH2_ID)
.role(Role.USER)
.build();
Member member = MemberDto.oauth2CreateMember(memberDto);
return memberRepository.save(member);
}
}
1. application.properties 에 설정한 user-info-uri 값들의 사용자 정보를 가져온다.
2. oAuth2User 에 로그인이 성공하면 다음과 같이 oAuth2User.getAttributes().get("login") 과 oAuth2User.getAttributes().get("id") 를 통해
정보를 가져올수 있다.
여기서는
username 은 github 가입시 아이디값이고,
authProvider 은 github 등록된 123456 의 pk 와 같은 아이디 값이다. 그리고 authProvider은 GitHub 값이다.
final String username = oAuth2User.getAttributes().get("login").toString();
final String authProviderId = oAuth2User.getAttributes().get("id").toString();
String email =null;
if(oAuth2User.getAttributes().get("email")!=null){
email = oAuth2User.getAttributes().get("email").toString();
}
final String authProvider=userRequest.getClientRegistration().getClientName();
//OAUTH2_ID = Oauth2(naver,github,google)_authProviderId ex) Github_1234
String OAUTH2_ID= authProvider+"_"+authProviderId;ex)GitHub_18599682
OAUTH2_ID =GitHub+"_"+123456
따라서, OAUTH2_ID 값과
username(authProvider+"_"+username)
username =GitHub_깃허브등록시 아이디값으로 저장 처리 한다.
굳이 이와 같이 아이디값에 oauth2 를 넣어주면서 저장처리를 하지않아도 되지만 이프로젝트에서는 이렇게 진행하였다.
중요한것은 아이디값이 unique 한값으로 저장 되야 한다는 것이다.
5. PrincipalDetails
package com.shop.config.auth;
import com.shop.constant.Role;
import com.shop.entity.Member;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.io.Serial;
import java.util.*;
import java.util.stream.Collectors;
//시큐리티가 login 주소 요청이 오면 낚아채서 로그인을 진행시킨다
//로그인을 진행이 완료가 되면 시큐리티 session 을 만들어 줍니다. (security ContectHolder) 세션 정보 저장
//오브젝트 => Authentication 타입 객체
//Authentication 안에 User 정보가 있어야 함.
//User 오브젝트타입 => UserDetails 타입 객체
//Security Session => Authentication => UserDetails
@Getter
@Log4j2
public class PrincipalDetails implements UserDetails, OAuth2User {
@Serial
private static final long serialVersionUID = 1L;
private final Member member; // 콤포지션
private final long id;
private final String idStr; //아이디를 문자열로 반환
private final String email;
private final String username;
/** 다음은 OAuth2User 를 위한 필드 */
private String authProviderId;
private Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
public PrincipalDetails(Member member, Map<String, Object> attributes){
this.authProviderId=member.getAuthProviderId();
this.attributes=attributes;
this.authorities= Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
this.id=member.getId();
this.idStr=member.getId().toString();
this.username=member.getUsername();
this.email = member.getEmail();
this.member = member;
}
// 일반로그인
public PrincipalDetails(Member member) {
this.id=member.getId();
this.idStr=member.getId().toString();
this.username=member.getUsername();
this.email = member.getEmail();
this.member = member;
this.authorities=getAuthorities();
}
/**
* 사용자에게 부여된 권한을 반환합니다. null을 반환할 수 없습니다.
*/
// 해당 User 의 권한을 리턴하는 곳!!
//권한:한개가 아닐 수 있음.(3개 이상의 권한)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collector=new ArrayList<>();
log.info("********* 시큐리티 로그인 :" +member.getRole().toString());
collector.add(()-> member.getRole().toString());
return collector;
}
/**
* 사용자를 인증하는 데 사용된 암호를 반환합니다.
*/
@Override
public String getPassword() {
return member.getPassword();
}
/**
* 사용자를 인증하는 데 사용된 사용자 이름을 반환합니다. null을 반환할 수 없습니다.
*/
@Override
public String getUsername() {
return member.getUsername();
}
/**
* 사용자의 계정이 만료되었는지 여부를 나타냅니다. 만료된 계정은 인증할 수 없습니다.
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 사용자가 잠겨 있는지 또는 잠금 해제되어 있는지 나타냅니다. 잠긴 사용자는 인증할 수 없습니다.
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 사용자의 자격 증명(암호)이 만료되었는지 여부를 나타냅니다. 만료된 자격 증명은 인증을 방지합니다.
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 사용자가 활성화되었는지 비활성화되었는지 여부를 나타냅니다. 비활성화된 사용자는 인증할 수 없습니다.
*/
@Override
public boolean isEnabled() {
// 우리 사이트 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 함.
// 현재시간-로긴시간=>1년을 초과하면 return false;
return true;
}
public boolean isWriteEnabled() {
if (member.getRole().equals(Role.USER))
return false;
else
return true;
}
public boolean isWriteAdminAndManagerEnabled() {
if (member.getRole().equals(Role.ADMIN )|| member.getRole().equals(Role.USER))
return true;
else
return false;
}
/**
* 다음 OAuth2User 를 위한 메소드
* @return
*/
@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}
@Override
public String getName() {
return this.authProviderId; //name 대신 id를 리턴한다.
}
}
6. OAuth2SuccessHandler OAuth2 로그인 성공시 토큰 생성 처리
package com.shop.config.oauth2;
import com.shop.dto.api.jwt.TokenDto;
import com.shop.service.api.jwt.JwtTokenProviderService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* OAuth2 로그인 성공시 토큰 생성
*
*/
@Component
@Log4j2
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProviderService jwtTokenProviderService;
@Value("${serverRedirectFrontUrl}")
private String serverRedirectFrontUrl ;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
TokenDto tokenDto= jwtTokenProviderService.create(authentication);
String redUrlPath=serverRedirectFrontUrl+"/sociallogin?accessToken=" +tokenDto.getAccessToken()
+"&refreshToken="+tokenDto.getRefreshToken();
response.sendRedirect(redUrlPath);
}
}
RedirectUrlCookieFilter 로 redirect 값을 쿠키에 저장해서 개발할경우
nginx 연동 운영서버에서는 에러가 나니 주의
다음과 같이 RedirectUrlCookieFilter 을 생성후
package com.shop.config.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Log4j2
@Component
public class RedirectUrlCookieFilter extends OncePerRequestFilter {
public static final String REDIRECT_URI_PARAM = "redirect_url";
private static final int MAX_AGE =180;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if(request.getRequestURI().startsWith("/api/auth/authorize")){
try{
log.info("*** RedirectUrlCookieFilter url {}", request.getRequestURI());
//프론트엔드에서 전송한 리퀘스트 파라미터에서 redirect_url 을 가져온다.
String redirectUrl = request.getParameter(REDIRECT_URI_PARAM);
Cookie cookie = new Cookie(REDIRECT_URI_PARAM, redirectUrl);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(MAX_AGE);
}catch(Exception ex){
log.error("Could not set user authentication in security context :", ex);
log.info("unauthorized request");
}
}
filterChain.doFilter(request, response);
}
}
OAuth2SuccessHandler 설정 하면 로컬에서 는 정상적으로 작동되나
nginx 로 실제 운영시에는 redirect_uri 값을 가져오지 못한다.
따라서. propertis 설정으로 할것.
@Component
@Log4j2
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String LOCAL_REDIRECT_URL = "http://localhost:3000";
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
TokenDto tokenDto= jwtTokenProviderService.create(authentication);
// response.getWriter().write(tokenDto.toString());
String redUrlPath="/sociallogin?accessToken=" +tokenDto.getAccessToken() +"&refreshToken="+tokenDto.getRefreshToken();
//response.sendRedirect("http://localhost:3000"+redUrlPath);
//RedirectUrlCookieFilter 에서 리다렉트시에 쿠키에 저장한 redirectUrl 값 가져오기
Optional<Cookie> optionalCookie =
Arrays.stream(request.getCookies()).filter(cookie -> cookie.getName().equals(REDIRECT_URI_PARAM)).findFirst();
Optional<String> redirectUrl = optionalCookie.map(Cookie::getValue);
log.info(" 쿠키값 redirectUrl 저장 : {} ", redirectUrl);
response.sendRedirect( redirectUrl.orElseGet(()->LOCAL_REDIRECT_URL)+redUrlPath);
}
}
7. 리액트 프론트엔드 구현
Sociallogin
import { Navigate } from "react-router-dom";
const Sociallogin = (props) => {
const getUrlParameters=(name)=>{
let search=window.location.search;
let params=new URLSearchParams(search);
return params.get(name);
};
const accessToken=getUrlParameters('accessToken');
const refreshToken=getUrlParameters('refreshToken');
console.log("토큰 파싱 :"+ accessToken, refreshToken);
if(accessToken && refreshToken){
console.log("로컬스토리지에 저장 accessToken:", accessToken);
console.log("로컬스토리지에 저장 refreshToken:", refreshToken);
localStorage.setItem("ACCESS_TOKEN", accessToken);
localStorage.setItem("REFRESH_TOKEN",refreshToken);
return ( <Navigate to={{pathname:'/', state:{from: props.location}}} /> )
}else{
return (<Navigate to={{pathname:'/login', state:{from: props.location}}} />)
}
}
export default Sociallogin
ApiServie.js
//oauth2 로그인
export function socialLogin(provider){
let frontedUrl;
const hostname =window && window.location && window.location.hostname;
if(hostname==="localhost"){
frontedUrl="http://localhost:3000";
}else{
frontedUrl="https://ma7front.p-e.kr";
}
window.location.href=API_BASE_URL+"/api/auth/authorize/"+provider +"?redirect_url="+frontedUrl;
}

















댓글 ( 0)
댓글 남기기