
저는 이 곳에서 학습을 위한 목적으로 우선 구현을 사용자 인증 정보를 세션에 저장하는 방식을 사용하고 있습니다. 이 방식은 서버에서 클라이언트의 상태를 유지해야 하므로, 서버의 부하 증가 등 여러 문제를 야기할 수 있기 때문에 추후 토큰 기반의 인증 방식을 사용할 것을 권장드립니다!
https://console.cloud.google.com










spring.security.oauth2.client.registration.google.client-id=(클라이언트 ID)
spring.security.oauth2.client.registration.google.client-secret=(클라이언트 보안 비밀번호)
spring.security.oauth2.client.registration.google.scope=profile,email
spring.profiles.include=oauth
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // CSRF(Cross-Site Request Forgery) 보호를 비활성화
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/", "/css/**", "/images/**", "/js/**", "/profile").permitAll() // 해당 URL 패턴들은 모든 사용자가 접근 가능
.requestMatchers("/api/v1/**").hasRole(Role.USER.name()) // "/api/v1/**" 패턴의 URL은 USER 권한을 가진 사용자만 접근 가능
.anyRequest().authenticated() // 나머지 모든 요청은 인증된 사용자만 접근 가능
)// 요청 URL에 따른 권한을 설정
.logout(logout -> logout.logoutSuccessUrl("/")) //로그아웃 시 리다이렉트될 URL을 설정
.oauth2Login(oauth2Login -> oauth2Login
.defaultSuccessUrl("/sweetodo/todo/todoMain")// OAuth 2 로그인 설정 진입점
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(customOAuth2UserService) // OAuth 2 로그인 성공 이후 사용자 정보를 가져올 때의 설정
)
);
return http.build();
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
//파라미터에 @Login 어노테이션이 붙어 있고, 파라미터 클래스 타입이 UserDTO.class인 경우 true를 반환한다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = UserDTO.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
//파라미터에 전달할 객체를 생성한다.
//여기선 세션에서 객체를 가져온다.
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// 이미 세션이 있다면 그 세션을 돌려주고, 세션이 없으면 null을 돌려준다.
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute("user");
}
}
@Getter
@Entity
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Column
private String provider; //공급자 (google, facebook ...)
@Column
private String providerId; //공급 아이디
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserRole userRole;
@Builder
public UserEntity(String name, String email, String picture, UserRole userRole, String provider, String providerId) {
this.name = name;
this.email = email;
this.picture = picture;
this.userRole = userRole;
this.provider = provider;
this.providerId = providerId;
}
public UserEntity() {
}
public UserEntity update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.userRole.getKey();
}
}@Getter
public class UserDTO implements Serializable {
private String name;
private String email;
private String picture;
private String provider;
private String providerId;
public UserDTO(UserEntity userEntity) {
this.name = userEntity.getName();
this.email = userEntity.getEmail();
this.picture = userEntity.getPicture();
this.provider = userEntity.getProvider();
this.providerId = userEntity.getProviderId();
}
}@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
private String provider;
private String providerId;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture, String provider, String providerId) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
this.provider = provider;
this.providerId = providerId;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
return ofGoogle(registrationId, userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.provider(registrationId)
.providerId((String) attributes.get("sub"))
.nameAttributeKey(userNameAttributeName)
.build();
}
public UserEntity toEntity() {
return UserEntity.builder()
.name(name)
.email(email)
.picture(picture)
.userRole(UserRole.GUEST)
.provider(provider)
.providerId(providerId)
.build();
}
} public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByEmail(String email);
} @RequiredArgsConstructor
@Service
@Transactional
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 현재 로그인 진행 중인 서비스를 구분하는 코드 (네이버 로그인인지 구글 로그인인지 구분)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 진행 시 키가 되는 필드 값 (Primary Key와 같은 의미)을 의미
// 구글의 기본 코드는 "sub", 후에 네이버 로그인과 구글 로그인을 동시 지원할 때 사용
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2UserService를 통해 가져온 OAuthUser의 attribute를 담을 클래스 ( 네이버 등 다른 소셜 로그인도 이 클래스 사용)
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
UserEntity userEntity = saveOrUpdate(attributes);
// UserEntity 클래스를 사용하지 않고 SessionUser클래스를 사용하는 이유는 오류 방지.
httpSession.setAttribute("user", new UserDTO(userEntity)); // UserDTO : 세션에 사용자 정보를 저장하기 위한 Dto 클래스
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(userEntity.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
// 구글 사용자 정보 업데이트 시 UserEntity 엔티티에 반영
private UserEntity saveOrUpdate(OAuthAttributes attributes) {
// 이메일을 기준으로 사용자를 찾아 업데이트하거나, 사용자를 새로 생성합니다.
UserEntity userEntity = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(userEntity);
}
}
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserArgumentResolver);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/oauth2/authorization/google">google login</a>
</body>
</html>


안녕하세요. 글 잘봤습니다! 혹시 해당 프로젝트 코드를 볼 수 있을까요?