프로젝트를 진행하며 OAuth 로그인과 일반 로그인을 동시에 구현하게 되었다. Spring Security의 formLogin과 oauth2Login 기능을 동시에 사용하는 방법은 아래와 같다.
package org.example.catch_line.config;
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록된다.
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
// secured 어노테이션 사용 가능(특정 메서드에 간단하게 걸고 싶을 때 사용) "ROLE_ADMIN"
// @PreAuthorize 어노테이션 사용 가능 "hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')" 함수가 실행되기 전에 권한을 검사
@Configuration
@RequiredArgsConstructor
public class SecurityConfig{
private final PrincipleOAuth2DetailsService principleOAuth2DetailsService;
private final PrincipalDetailsService principalDetailsService;
@Bean
public static BCryptPasswordEncoder bCryptPasswordEncoder() { // for hash encrypt
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, PrincipleOAuth2DetailsService principleOAuth2DetailsService) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정을 최신 방식으로 변경
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable())) // frameOptions 설정
.authorizeHttpRequests(requests ->requests
.requestMatchers("/**", "/static/**", "/images/**", "/signup", "/login", "/restaurants/**", "/owner").permitAll()
.requestMatchers("/members", "/history").authenticated() // 인증이 필요
.anyRequest().permitAll()
)
// 일반 로그인
.formLogin(formLogin -> formLogin
.loginPage("/login") // 로그인 페이지 URL
.loginProcessingUrl("/loginProcess") // 로그인 처리 URL (여기에 POST 요청이 와야 함)
.defaultSuccessUrl("/")
.failureUrl("/login?error=true")
.permitAll()
)
// Oauth 로그인
.oauth2Login(login -> login
.loginPage("/login/oauth")
.defaultSuccessUrl("/loginSuccess")
.userInfoEndpoint()
.userService(principleOAuth2DetailsService) // OAuth 사용자 로그인 처리
)
.userDetailsService(principalDetailsService); // 일반 사용자 로그인 처리
// .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080", "http://localhost:8081"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
두 개의 생성자를 통해 일반 로그인 사용자와 OAuth 로그인 사용자를 구분했다. 일반 로그인을 한 경우 UserDetails가 생기며, OAuth로 로그인을 할 경우 OAuth2User가 생성된다.
package org.example.catch_line.config.auth;
import lombok.Data;
import org.apache.logging.log4j.util.Strings;
import org.example.catch_line.common.constant.Role;
import org.example.catch_line.user.member.model.entity.MemberEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@Data
public class PrincipalDetail implements UserDetails, OAuth2User {
private MemberEntity member;
private Map<String,Object> attributes;
// 일반 로그인 생성자
public PrincipalDetail(MemberEntity member) {
this.member = member;
}
// Oauth 로그인 생성자
public PrincipalDetail(MemberEntity member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
// 잘 사용하지 않는다.
@Override
public String getName() {
return Strings.EMPTY;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return Role.USER.getDescription();
}
});
return collect;
}
@Override
public String getPassword() {
return member.getPassword() != null ? member.getPassword().getEncodedPassword() : "";
}
@Override
public String getUsername() {
return member.getEmail().getEmailValue();
}
}
PrincipalDetailsService
package org.example.catch_line.config.auth;
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
// 시큐리티 session (내부 Authentication (내부 UserDetails ))
private final MemberDataProvider memberDataProvider;
// 함수 종료 시 @AuthenticationPrincipal 어노테이션이 만들어진다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemberEntity member = memberDataProvider.provideMemberByEmail(new Email(username));
return new PrincipalDetail(member);
}
}
package org.example.catch_line.config.auth;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.catch_line.common.constant.Role;
import org.example.catch_line.common.model.vo.Email;
import org.example.catch_line.user.member.model.entity.MemberEntity;
import org.example.catch_line.user.member.model.provider.MemberDataProvider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.thymeleaf.util.StringUtils;
import java.util.List;
import java.util.Map;
@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipleOAuth2DetailsService extends DefaultOAuth2UserService {
private final MemberDataProvider memberDataProvider;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
// Role generate
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(Role.USER.getDescription());
Map<String, Object> attributes = oAuth2User.getAttributes();
log.info("attributes: {}", attributes);
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
String name = (String) properties.get("nickname"); // properties에서 닉네임 가져오기
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
String email = (String) kakaoAccount.get("email"); // kakao_account에서 이메일 가져오기
String provider = userRequest.getClientRegistration().getRegistrationId(); // kakao
Long providerId = ((Number) attributes.get("id")).longValue();
String nickname = provider + "_" + StringUtils.substring(providerId, 0, 7); // kakao_이름
if (memberDataProvider.isNotDuplicateKakaoMember(providerId, new Email(email))) {
MemberEntity member = MemberEntity.builder()
.name(name)
.nickname(nickname)
.email(new Email(email))
.kakaoMemberId(providerId)
.build();
memberDataProvider.saveMember(member);
}
MemberEntity member = memberDataProvider.provideMemberByKakaoMemberId(providerId);
// 어떤 OAuth2 공급자를 통해 로그인하는지, 해당 공급자에서 사용자의 고유 식별자를 나타내는 필드명이 무엇인지를 반환한다.
// 지금 kakao login만 사용하기 때문에 필요없지만, 추후 구현 위해 남겨 놓는다.
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
return new PrincipalDetail(member, oAuth2User.getAttributes());
}
}
일반 로그인과 OAuth2 로그인을 동시에 사용하기 위해 Security 설정을 어떻게 할 수 있는지 알아봤다.
스프링 시큐리티에 대해 공부하고 있었는데 좋은 글 감사합니다~