build.gradle에 security 의존성 추가
//spring security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-test")
//spring security oauth2
implementation ("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation ("org.springframework.security:spring-security-oauth2-jose")
Member Entity
💰 MemberRole
@Getter
@RequiredArgsConstructor
public enum MemberRole {
USER("ROLE_USER"),
ADMIN("ROLE_USER"),
GUEST("ROLE_USER");
private final String value;
}
💰 Member
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String nickname;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "profile_img_url")
private String profileImageUrl;
@Enumerated(EnumType.STRING)
private MemberRole role;
@Column(name = "provider_type")
private String provider; // google, naver ...
@Column(name = "provider_id")
private String providerId; // 소셜 로그인 성공시 부여되는 고유한 id
@Builder(access = AccessLevel.PRIVATE)
private Member(String nickname, String email, String profileImageUrl, String provider, String providerId){
this.nickname = nickname;
this.email = email;
this.profileImageUrl = profileImageUrl;
this.role = MemberRole.USER;
this.provider = provider;
this.providerId = providerId;
}
public static Member of(String nickname, String email, String profileImageUrl, String provider, String providerId){
return Member.builder()
.nickname(nickname)
.email(email)
.profileImageUrl(profileImageUrl)
.provider(provider)
.providerId(providerId)
.build();
}
public Member updateNicknameAndEmailAndProfileImg(String nickname, String email, String profileImageUrl){
this.nickname = nickname;
this.email = email;
this.profileImageUrl = profileImageUrl;
return this;
}
public Member updateProvider(String provider){
this.provider = provider;
return this;
}
public String getRoleValue(){
return this.getRole().getValue();
}
}
Member Repository
public interface MemberRepository extends JpaRepository<Member,Long>, MemberRepositoryCustom {
Optional<Member> findByEmailAndProvider(String email, String provider);
}
이미 생성된 사용자인지 확인하기 위한 용도로 쓰일 예정입니다.
application.yml
위와 같이 구글에 서비스를 등록하고 발급 받은 key 값들을 아래와 같이 yml 파일에 입력해줍니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: 발급 받은 client id
client-secret: 발급 받은 client secret
scope:
- email
- profile
🌟 보안적인 내용이 담겨있으므로 .gitignore에 꼭 추가해주어야합니다. 🌟
Security 설정을 위한 SecurityConfig 파일 생성
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final OAuthService oAuthService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//CORS 설정
.cors()
.and()
//Cross-Site-Request-Forgery 웹 브라우저가 신뢰할 수 없는 악성 사이트에서 사용자가 원치않는 작업을 수행하는 공격
//쿠키에 의존하지 않고 OAuth2.0, JWT를 사용하는 REST API의 경우 CSRF 보호가 필요하지 않음
.csrf().disable()
//basic 인증방식은 username:password를 base64 인코딩으로 Authroization 헤더로 보내는 방식
.httpBasic().disable()
.formLogin().disable()
//요청에 대한 인가 처리 설정
.authorizeRequests()
.antMatchers("/","/oauth2/**").permitAll() // 로그인은 누구나 가능하도록
.antMatchers("/api/v1/**").hasRole(MemberRole.USER.name()) // 유저만 접속 가능
.anyRequest().authenticated() // 그 외엔 모두 인증 필요
.and()
//OAuth 2.0 기반 인증을 처리하기위해 Provider와의 연동을 지원
.oauth2Login()
.userInfoEndpoint()
//OAuth 2.0 인증이 처리되는데 사용될 사용자 서비스를 지정하는 메서드
.userService(oAuthService);
return http.build();
}
}
OAuth 로그인 성공 시 DB에 저장하는 OAuthService 생성
@Service
@RequiredArgsConstructor
public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService(); //DefaultOAuth2User 서비스를 통해 User 정보를 가져와야 하기 때문에 대리자 생성
OAuth2User oAuth2User = delegate.loadUser(userRequest); // OAuth 서비스(kakao, google, naver)에서 가져온 유저 정보를 담고있음
String registrationId = userRequest.getClientRegistration()
.getRegistrationId(); // OAuth 서비스 이름(ex. kakao, naver, google)
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName(); //OAuth2 로그인 진행시 키가 되는 필드값 (pk)
Map<String, Object> attributes = oAuth2User.getAttributes(); // OAuth 서비스의 유저 정보들
Member member = OAuthAttributes.extract(registrationId, attributes); // registrationId에 따라 유저 정보를 통해 공통된 Member 객체로 만들어 줌
member.updateProvider(registrationId);
member = saveOrUpdate(member);
Map<String, Object> customAttribute = customAttribute(attributes, userNameAttributeName, member, registrationId);
// 로그인 유저 리턴
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(member.getRoleValue())),
customAttribute,
userNameAttributeName);
}
private Map customAttribute(Map attributes, String userNameAttributeName, Member member, String registrationId) {
Map<String, Object> customAttribute = new LinkedHashMap<>();
customAttribute.put(userNameAttributeName, attributes.get(userNameAttributeName));
customAttribute.put("provider", registrationId);
customAttribute.put("nickname", member.getNickname());
customAttribute.put("email", member.getEmail());
customAttribute.put("picture", member.getProfileImageUrl());
return customAttribute;
}
private Member saveOrUpdate(Member member) {
Member newMember = memberRepository.findByEmailAndProvider(member.getEmail(), member.getProvider())
.map(m -> m.updateNicknameAndEmailAndProfileImg(member.getNickname(), member.getEmail(), member.getProfileImageUrl())) // OAuth 서비스 사이트에서의 유저 정보 변경사항 update
.orElse(Member.of(member.getNickname(), member.getEmail(), member.getProfileImageUrl(),
member.getProvider(), member.getProviderId()));
return memberRepository.save(newMember);
}
}
🤗 customAttribute를 사용한 이유
oAuth2User.getAttributes()를 통해 받아온 정보들은 unmodifableMap입니다 (수정할 수 없는 Map). 따라서 필요한 정보들만 추출해서 제공하기 위해 customAttribute를 사용하였습니다.
🤗 DefaultOAuth2User를 return 할 때 member의 role은 "ROLE_"로 시작해야합니다.
OAuthAttributes 생성
public enum OAuthAttributes {
GOOGLE("google", (attributes) -> {
return Member.of(
(String) attributes.get("name"),
(String) attributes.get("email"),
(String) attributes.get("picture"),
"google",
"google_"+ attributes.get("sub")
);
});
private final String registrationId;
private final Function<Map<String, Object>, Member> of;
OAuthAttributes(String registrationId, Function<Map<String, Object>, Member> of) {
this.registrationId = registrationId;
this.of = of;
}
// provider가 일치하는 경우에만 apply를 호출하여 (google) member를 반환
public static Member extract(String registrationId, Map<String, Object> attributes) {
return Arrays.stream(values())
.filter(provider -> registrationId.equals(provider.registrationId))
.findFirst()
.orElseThrow(IllegalArgumentException::new)
.of.apply(attributes);
}
}
extract : provider가 일치하면 attributes를 member로 변환 후 반환해주는 함수입니다. (일치하지 않으면 예외 발생)
현재는 google밖에 없지만 google, naver, kakao 등등 데이터를 제공해주는 형식이 다르기 때문에 따로 관리해줘야 합니다.
소셜 로그인 실행해보기
스프링부트 실행 후
localhost:8080/oauth2/authorization/google로 접속해줍니다.
그럼 로그인 창이 뜨게 되고 로그인을 하면
데이터 베이스에 잘 저장된 것을 확인해볼 수 있습니다 :D
💖 참고
https://codediary21.tistory.com/73
https://junuuu.tistory.com/415
'Server > Spring boot' 카테고리의 다른 글
[Spring boot] 이미지를 포함하는 글 작성 API 설계 방식 (1) | 2023.07.09 |
---|---|
[Spring boot] Google 로그인 유저 정보 가져오기 (0) | 2023.05.02 |
[Spring boot] security + Oauth2로 구글 로그인 구현하기 - OAuth 서비스 등록 (0) | 2023.04.29 |
docker-compose 로 Spring boot + MariaDB 개발 환경 구축하기 (0) | 2023.03.16 |
[Querydsl Expressions] Querydsl에서 date format 하기 (0) | 2023.02.07 |