
네이버와 카카오는 구글, 깃허브, 페이스북과 달리 자동으로 설정되어 있지 않는 부분들이 많아 제공자인 네이버에 대한 정보와, 인증 방식, redirect-url 등을 직접 설정해줘야 한다.
security:
oauth2:
client :
provider :
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
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
registration :
naver :
client-name : Naver
client-id: {클라이언트 아이디}
client-secret: {클라이언트 비밀키}
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
scope :
- name
- nickname
- email
kakao :
client-name: Kakao
client-id: {클라이언트 아이디}
client-secret: {클라이언트 비밀키}
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
scope :
- profile_nickname
- profile_image
provider
authorization-uri : Authorization code(권한부여 승인코드)를 받기 위한 주소token-uri : AccessToken을 받기 위한 주소user-info-uri : 사용자 정보를 받기 위한 주소user-name-attribute : 요청 정보를 어디에 담아서 리턴하는지 지정해준다. 네이버는 response 에 사용자 정보를 반환해주기 때문에 response를 넣어준다. 카카오는 id에 정보를 담아 주기 때문에, id로 넣어준다.scope : scope를 넣어줄 때도, 각 제공자의 개발자 센터에서 주요 스코프 이름을 확인한 후, 맞게 넣어줘야 한다. 이전에는 구글 하나만 이용했기 때문에 별 문제가 없었다. 하지만, 이제 카카오, 네이버, 구글 이렇게 다양한 서버에서 사용자의 정보를 받아오는데, 그 정보가 저장되어 있는 형태가 다 동일하지 않다. 네이버는 OAuth2User의 attributes의 response 안에, 카카오는 properties 안에 저장되어 있다. 그렇기 때문에 Resource Server에 따라 다르게 처리해줄 필요가 있다.
간단하게, loadUser 메서드에서 if/else 문으로 서버를 구분해주어 다르게 처리해줄 수 있다.
public class MemberDetailsFactory {
public static MemberDetails memberDetails(String provider, OAuth2User oAuth2User) {
Map<String, Object> attributes = oAuth2User.getAttributes();
switch (provider.toUpperCase().trim()) {
case "KAKAO" -> {
Map<String, String> properties = (Map<String, String>) attributes.get("properties");
return MemberDetails.builder()
.name(attributes.get("nickname").toString())
.email(properties.get("id") + "@kakao.com")
.attributes(attributes)
.build();
}
case "GOOGLE" -> {
return MemberDetails.builder()
.name(attributes.get("name").toString())
.email(attributes.get("email").toString())
.attributes(attributes)
.build();
}
case "NAVER" -> {
Map<String, String> properties = (Map<String, String>) attributes.get("response");
return MemberDetails.builder()
.name(properties.get("name"))
// .email(properties.get("email"))
.email(properties.get("id") + "@naver.com")
.attributes(attributes)
.build();
}
default -> {
throw new IllegalArgumentException("지원하지 않는 제공자 : " + provider);
}
}
}
}
static 메서드
memberDetails()메서드는 MemberDetails 객체를 만들어주는 유틸리성 메서드이다. 이럴 땐 굳이 객체를 생성할 필요 없이, 클래스 차원에서 메서드를 바로 만들어줄 수 있도록 static으로 만들어야 한다. 또, static으로 만들어주지 않으면 new MemberDetailsFactory() 처럼 매번 인스턴스 객체를 만들어낼 수 있기 때문에 static 으로 만들어준다.
메서드 설명
provider 에 따라 MemberDetails를 빌더 방식으로 생성해서 반환해주도록 한다.properties 안에 nickname, profile_image 등이 담겨있기 때문에, attributes에서 properties를 또 get해줘야 한다.response 안에 name, email 등이 담겨있기 때문에, attributes에서 response를 또 get해줘야 한다.이제 제공자에 맞게 MemberDetails를 반환해주는 MemberDetailsFactory를 구현해줬기 때문에 MemberService 내부 로직도 바꿔줘야 한다.
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId().toUpperCase();
MemberDetails memberDetails = MemberDetailsFactory.memberDetails(provider, oAuth2User);
Optional<Member> memberOptional = memberRepository.findByEmail(memberDetails.getEmail());
Member member = memberOptional.orElseGet(
() -> {
Member saved = Member.builder()
.name(memberDetails.getName())
.email(memberDetails.getEmail())
.provider(provider)
.build();
return memberRepository.save(saved);
}
);
if (member.getProvider().equals(provider)) {
return memberDetails.setRole(member.getRole());
} else {
throw new RuntimeException();
}
}
oAuthUser (사용자 정보), provider (제공자, Resource Server) 를 받은 후, MemberDetailsFactory를 호출하여 MemberDetails를 받아온다.MemberDetails는 권한 정보를 가지고 있지 않다.findByEmail로 회원 가입 정보를 불러오고, 없으면 회원 가입을 시켜준다.MemberDetails에 권한 정보만 설정해주면 된다.memberDetails.setRole(member.getRole())✅ 그렇기 때문에, 해당 이메일으로 가입한 Member (DB에 저장된)의 provider와 현재 로그인을 시도한 계정의텍스트 provider를 비교한다. 즉, 이미 저장된 사람은 이전에 카카오로 로그인 했지만 이메일은 네이버인 사람, 그치만 이번엔 네이버로 네이버 이메일을 가지고 로그인을 시도한다.
❌ 예외 발생 시키기 !
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
jwt/io -> Libraries 에서 원하는 jwt를 선택하고, Github에서 Installation에 원하는 방식을 찾아 넣어준다.이번에는 프론트를 사용하여 데이터를 줄 것이기 때문에, http를 사용하지 않을 것이다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.httpBasic(httpB -> httpB.disable()) //
.csrf(csrf -> csrf.disable())
.cors( cors -> cors.disable())
.formLogin(form -> form.disable())
.sessionManagement(
session -> session.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
)
httpBasic() : Spring Security에서 제공하는 HTTP Basic 인증 방식을 설정하는 메서드이다. 이번 프로젝트에서는 프론트를 분리할 것이기 때문에 꺼주는 것이다. cors( cors -> cors.disable()) : 이제 프론트와 통신할 것이기 때문에, 출처가 다르더라도 허용해줘야 한다. formLogin(form-> form.disable()) : 이제 폼 방식 로그인을 사용하지 않을 것이기 때문에 설정을 꺼준다.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) : Spring Security의 세션 관리 전략을 설정하는 것이다. STATELESS로 두어 세션을 사용하지 않겠다고 하는 것이다.📝 ❓세션을 왜 안 쓸까?
기본적으로 Spring Security는 세션 기반 인증이다. 즉, 사용자가 로그인하면 서버가 세션을 생성해서 인증 정보를 거기 저장하고, 요청마다 세션 ID를 통해 인증된 사용자인지 확인한다.
하지만, JWT를 사용할 때는, 인증 정보를 세션에 저장하는 것이 아니라, 토큰 자체에 인증 정보를 들고 다닌다.
STATELESS로 두면, 서버가 세션을 생성하지 않고, 세션에 인증 정보를 저장하지 않으며, 매 요청마다 토큰 기반으로 인증을 처리한다.
Q1. JWT의 구조 중에 Signature 부분이 이해가 안된다. 다시 정리하기 !
✍️ 서명 생성 과정
예를 들어 HMAC-SHA256 알고리즘을 사용한다고 가정할 때 :
secret = "mySecretKey"
1. Header + Payload 인코딩:
BASE64(header) + "." + BASE64(payload)
2. 서명 생성 (Signature):
HMAC-SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
3. 이 결과값을 다시 base64로 인코딩해서 마지막에 붙인다!
✅ 정리
누군가 Payload를 조작해도, 서버는 비밀 키가 없으면 정상적인 Signature를 만들 수 없다. 즉, JWT 위조를 막아주는 결정적인 보안장치가 Signature이다.
와우.. 오늘 뭔가 샤라웃을 받은 것 같아서 너무 기분이 좋았다 하하 .. 좋은 말씀을 너무 많이 해주셔서 너무 감사했다. 그치만 뭔가 티내고 싶지 않아서 이 꽉 깨물고 입꼬리를 조절했다 .. ㅎ 알아봐주셔서 너무 너무 감사합니다..! 근데 그만큼 진짜 머리에 남는 것 + 실력도 많이 늘어야 하는데.. 하하 더 파이팅해야겠다 !! 🔥🔥
오늘은 정말 수업을 따라가기 수월했던 것 같다. 다행이다. 근데 이제 시작중인 JWT는 또 해봐야 알겠지만,, 무섭네 ㅎㅎㅎ 그래서 내일 또 JWT한테 맞을까봐 좀 헷갈리는 부분은 정리했다. 내일도 살려다오.
내일 벌써 금요일이잖아?!!? 너무 행복하군 ㅎㅎㅎ 내일도 파이팅하쟈 !!