프로젝트

OAuth2, SpringSecurity를 활용한 로그인,회원가입 구현 프로젝트[Kakao, Google, Naver]

연향동큰손 2025. 2. 21. 00:32

카카오, 구글, 네이버 계정을 통해 회원으로 등록하여 로그인기능을 구현하는 웹 프로젝트를 진행 하였다.

 

🎯 프로젝트 구현 목표
1. 구글, 카카오, 네이버 계정으로 회원정보 DB에 등록
2. 일반 로그인, 회원가입(아이디,비밀번호 사용)구현
3. JWT와 쿠키를 활용한 로그인 유지

 

 

<프로젝트 깃허브 리포지토리>

https://github.com/yangwoohyeon/Oauth_Practice

 

GitHub - yangwoohyeon/Oauth_Practice: 구글,네이버,카카오,클라이언트 로그인 연습 토이 프로젝트

구글,네이버,카카오,클라이언트 로그인 연습 토이 프로젝트. Contribute to yangwoohyeon/Oauth_Practice development by creating an account on GitHub.

github.com

 

 

전체적인 구현 로직

 

  1. /oauth2/authorization/{provider} 경로로 이동, OAuth2 제공자(Naver,Kakao,Naver)의 로그인 화면으로 리다이렉트
  2. 소셜 계정으로 인증 후   PrincipalOauth2UserService가 호출되어 OAuth2User 정보를 가져옴
  3. OAuth2UserInfo 인터페이스를 구현한 각 소셜 클래스 (GoogleUserInfo, KakaoUserInfo, NaverUserInfo)를 통해 사용자 정보 추출
  4. 기존 사용자가 아니면 DB에 사용자 정보를 저장, DB에 있는 사용자면 그대로 진행
  5. JwtTokenProvider를 통해 토큰 발급 (만료 시간 : 30분)
  6. 쿠키에 저장 후 메인 페이지로 이동

 

이제부터 코드를 보며 구현과정을 알아보자!

 

소셜 로그인을 위한 키 발급

 

소셜 로그인을 위해 카카오,구글, 네이버에서 client-id와 client-secret키를 받아와야 한다.

 

키 발급 과정은 아래 블로그를 참고하여 진행하였다.

https://velog.io/@yso8296/Spring-Security%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%86%B5%ED%95%A9-OAuth2-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84#applicationyml

 

Spring Security를 이용한 통합 OAuth2 소셜 로그인 기능 구현(Kakao, Google, Naver)

이번 시간에는 이전 포스팅들에서 공부한 시큐리티 설정 build.gradle SecurityConfig

velog.io

 

각각의 키 값을 받았다면 application.yml에 등록해줘야 한다.

 

<application.yml>

spring:
  web:
    resources:
      static-locations: classpath:/static/
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML5

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/oauth?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: root
    password: 1234


  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: create
      naming:
       physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
    properties:
      hibernate.format_sql: true
      dialect: org.hibernate.dialect.MySQL8InnoDBDialect


  security:
    oauth2:
      client:
        registration:
          google:
            client-id: client id 넣기
            client-secret: 비밀키 넣기
            scope:
              - email
              - profile
          naver:
            client-id: client id 넣기
            client-secret: 비밀키 넣기
            scope:
              - name
              - email
            client-name: Naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
          kakao:
            client-name: kakao
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao
            client-id: 키값 넣기
            client-secret: 비밀키 넣기
            client-authentication-method: client_secret_post
            scope:
              - profile_nickname
              - account_email


        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
            user-name-attribute: id
            token-uri: https://kauth.kakao.com/oauth/token
            user-info-uri: https://kapi.kakao.com/v2/user/me

jwt:
  secret: ZGVmYXVsdC1zZWNyZXQta2V5LXRvLWNoYW5nZQ==

 

 

유저 객체 생성

 

각 소셜 로그인 사용자 정보를 저장하기 위한 유저 객체를 생성해줘야 한다.

 

<User>

package hello.Member_Management.User.Entity;



import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;

@Data
@Entity
@Table
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private long id;

    @Column(name="user_id", unique = true)
    private String userId; //유저 식별자
    private String  password; //비밀번호
    private String email; //이메일
    private String role; //유저 신분
    private String name; //이름

    @CreationTimestamp //INSERT 쿼리가 발생할 때, 현재 시간을 값으로 채워서 쿼리를 생성한다.
    private Timestamp timestamp; //가입일 기록

    private String provider;
    private String providerId;

}

 

<UserRepository>

package hello.Member_Management.User.Repository;

import hello.Member_Management.User.Entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Long> { //Spring Data JPA 사용
    User findByUserId(String userId); //이름으로 유저 찾기
}

 

 

하지만 여기서 한가지 문제가 있는데 

 

카카오, 구글, 네이버로 로그인 했을때 넘어오는 정보가 다 달라서 유저 객체에 매핑 시키는데 문제가 생긴다.

 

따라서  각 플랫폼에서 가져온 사용자 정보를 표준화된 형태로 변환해주는 Adapter가 필요하다.

 

 

 

<OAuth2UserInfo 인터페이스>

package hello.Member_Management.User.UserInfo;

public interface OAuth2UserInfo {

    String getProvider();

    String getProviderId();

    String getEmail();

    String getName();
}

 

<GoogleUserInfo>

package hello.Member_Management.User.UserInfo;

import java.util.Map;

public class GoogleUserInfo implements OAuth2UserInfo{

    private Map<String,Object> attributes;
    public GoogleUserInfo(Map<String,Object> attributes){
        this.attributes=attributes;
    }
    @Override
    public String getProvider() {
        return "google";
    }

    @Override
    public String getProviderId() {
        return (String)attributes.get("sub");
    }

    @Override
    public String getEmail() {
        return (String)attributes.get("email");
    }

    @Override
    public String getName() {
        return (String)attributes.get("name");
    }
}

 

 

<KakaoUserInfo>

package hello.Member_Management.User.UserInfo;

import java.util.LinkedHashMap;
import java.util.Map;

public class KakaoUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes()
    public KakaoUserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "kakao";
    }

    @Override
    public String getProviderId() {
        return String.valueOf(attributes.get("id"));
    }

    @Override
    public String getEmail() {
        Object object = attributes.get("kakao_account");
        LinkedHashMap accountMap = (LinkedHashMap) object;
        return (String) accountMap.get("email");
    }

    @Override
    public String getName() {
        LinkedHashMap<String, Object> accountMap = (LinkedHashMap<String, Object>) attributes.get("kakao_account");
        LinkedHashMap<String, Object> profileMap = (LinkedHashMap<String, Object>) accountMap.get("profile");
        return (String) profileMap.get("nickname");
    }

}

 

<NaverUserInfo>

package hello.Member_Management.User.UserInfo;

import java.util.Map;

public class NaverUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes()
    public NaverUserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProvider() {
        return "naver";
    }

    @Override
    public String getProviderId() {
        return (String) attributes.get("id");
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}

 

 

받아온 정보를 이용하여 로그인 구현

 

이제 소셜계정을 통해 받아온 정보를 통해 User객체와 매팽을 시키고 DB에 저장하거나 로그인을 유지시켜야 한다.

 

<PrincipalDetail>

package hello.Member_Management.User;

import hello.Member_Management.User.Entity.User;
import lombok.Data;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user; // 콤포지션
    private Map<String, Object> attributes;

    // 일반 로그인
    public PrincipalDetails(User user) {
        this.user = user;
    }
    // OAuth 로그인
    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }


    // 해당 유저의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });

        return collect;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String getName() {
        return user.getName(); // ✅ DB에 저장된 이름 반환
    }


    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }
}

 

 

 

<PrincipalOauth2UserService>

package hello.Member_Management.User.Service;

import hello.Member_Management.User.*;
import hello.Member_Management.User.Entity.User;
import hello.Member_Management.User.Jwt.JwtTokenProvider;
import hello.Member_Management.User.Repository.UserRepository;
import hello.Member_Management.User.UserInfo.GoogleUserInfo;
import hello.Member_Management.User.UserInfo.KakaoUserInfo;
import hello.Member_Management.User.UserInfo.NaverUserInfo;
import hello.Member_Management.User.UserInfo.OAuth2UserInfo;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 java.io.IOException;
import java.util.Map;


@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private PasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserRepository userRepository;
    

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        System.out.println("getClientRegistration = " + userRequest.getClientRegistration());
        System.out.println("getAccessToken = " + userRequest.getAccessToken().getTokenValue());

        OAuth2User oAuth2User = super.loadUser(userRequest);
        System.out.println("getAttributes = " + oAuth2User.getAttributes());

        OAuth2UserInfo oAuth2UserInfo = null;
        if (userRequest.getClientRegistration().getRegistrationId().equals("google")) {
            System.out.println("구글 로그인 요청");
            oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
        } else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")){
            System.out.println("네이버 로그인 요청");
            oAuth2UserInfo = new NaverUserInfo((Map) oAuth2User.getAttributes().get("response"));
        } else if(userRequest.getClientRegistration().getRegistrationId().equals("kakao")){
            System.out.println("카카오 로그인 요청");
            oAuth2UserInfo = new KakaoUserInfo(oAuth2User.getAttributes());
        } else {
            System.out.println("지원하지 않는 로그인 방식입니다!");
        }

        String provider = oAuth2UserInfo.getProvider();
        String providerId = oAuth2UserInfo.getProviderId();
        String username = provider + "_" + providerId; //각 소셜 플랫폼을 구분하기 위해 플랫폼명 + ID로 설정
        String password = bCryptPasswordEncoder.encode("임의 비밀번호");
        String email = oAuth2UserInfo.getEmail();
        String name = oAuth2UserInfo.getName(); //사용자 이름 가져오기
        String role = "ROLE_USER";

        User userEntity = userRepository.findByUserId(username);

        if (userEntity == null) {
            System.out.println("로그인이 최초입니다.");
            userEntity = User.builder()
                    .userId(username)
                    .password(password)
                    .email(email)
                    .name(name) // ✅ 사용자 이름 저장
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            userRepository.save(userEntity);
        } else {
            System.out.println("이미 등록된 사용자입니다.");
        }


        // PrincipalDetails 생성
        PrincipalDetails principalDetails = new PrincipalDetails(userEntity, oAuth2User.getAttributes());

        // SecurityContext에 인증 정보 강제 등록
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);

        return principalDetails;

    }



}

 

userRequest.getClientRegistration()를 통해 어떤 소셜 플랫폼인지를 확인하고,

 

로그인을 시도한 소셜 플랫폼에 맞게 OAuth2UserInfo 인터페이스를 구현한 클래스( GoogleUserInfo, KakaoUserInfo, NaverUserInfo)를 사용한다.

 

그 후 소셜 로그인 사용자의 정보를 받아와서 DB에 있는지 비교 후 없으면 사용자 정보를 DB에 저장하고,

 

만약 이 전에 로그인 했던 유저라면 DB에 저장하지 않고 기존 정보를 사용한다.

 

 

 

JWT토큰 발급 후 쿠키에 저장

 

소셜 로그인을 통해 사용자 정보를 받아와서 인증이 완료 되었다면 클라이언트와 서버 간 인증 상태를 유지하기 위해 JWT 토큰을 발급받아야 한다.

 

<JwtTokenProvider>

package hello.Member_Management.User.Jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Base64;
import java.util.Date;

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String secretKey;

    private final long ACCESS_TOKEN_VALIDITY = 1000 * 60 * 30; // 30분
    private final long REFRESH_TOKEN_VALIDITY = 1000 * 60 * 60 * 24 * 7; // 7일

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // ✅ Access Token 생성
    public String createAccessToken(String userPk) {
        return createToken(userPk, ACCESS_TOKEN_VALIDITY);
    }

    // ✅ Refresh Token 생성
    public String createRefreshToken(String userPk) {
        return createToken(userPk, REFRESH_TOKEN_VALIDITY);
    }

    // ✅ JWT 생성 공통 메서드
    public String createToken(String userPk, long validity) {
        Claims claims = Jwts.claims().setSubject(userPk);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + validity))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    // ✅ 토큰에서 회원 정보 추출
    public String getUserPk(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        } catch (Exception e) {
            System.out.println("❌ 토큰 파싱 실패: " + e.getMessage());
            return null;
        }
    }


    public boolean validateToken(String token) {
        try {
            return !Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                    .getBody().getExpiration().before(new Date());
        } catch (Exception e) {
            System.out.println("❌ JWT 유효성 검사 실패: " + e.getMessage());
            return false;
        }
    }

}

 

 

<SecurityConfig>

package hello.Member_Management.User.Config;

import hello.Member_Management.User.Jwt.JwtTokenProvider;
import hello.Member_Management.User.Jwt.JwtAuthenticationFilter;
import hello.Member_Management.User.PrincipalDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
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.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity //  스프링 시큐리티 필터 활성화
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;
    private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .cors(cors -> cors.disable())
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/loginForm","joinForm").permitAll()
                        .requestMatchers("/images/**","/css/**","/js/**","/webjars/**").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/loginForm")
                        .loginProcessingUrl("/login") //  POST 요청 URL
                        .successHandler(customOAuth2SuccessHandler)  //  커스텀 SuccessHandler 등록
                        .failureUrl("/loginForm?error=true")
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/loginForm")
                        .successHandler(customOAuth2SuccessHandler)  //  커스텀 SuccessHandler 등록
                )
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
                        UsernamePasswordAuthenticationFilter.class)
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/")
                        .deleteCookies("JSESSIONID", "accessToken")
                        .invalidateHttpSession(true)
                );

        return http.build();
    }


    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 프론트엔드 주소
        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;
    }


   
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

 

 

<CustomOAuth2SuccessHandler>

package hello.Member_Management.User.Config;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.Member_Management.User.Jwt.JwtTokenProvider;
import hello.Member_Management.User.PrincipalDetails;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class CustomOAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {

        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        String username = principalDetails.getUsername();

        // ✅ JWT 생성
        String accessToken = jwtTokenProvider.createAccessToken(username);

        // ✅ SecurityContext에 인증 정보 수동 저장
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // ✅ JWT를 쿠키에 저장 (선택)
        Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
        System.out.println("accessToken = " + accessToken);
        accessTokenCookie.setHttpOnly(true);
        accessTokenCookie.setPath("/");
        accessTokenCookie.setMaxAge(60*30); //30분동안 유효
        response.addCookie(accessTokenCookie);

        // ✅ 홈으로 리다이렉트
        response.sendRedirect("/");
    }

}

 

소셜 로그인 성공시 CustomOAuth2SuccessHandler가 호출되어

 

JWT토큰을 생성하고 쿠키에 저장한다.

 

이 토큰을 이용해 사용자의 인증이 이루어진다.

 

서버는 클라이언트의 요청이 서버에 도달하기 전에 JWT토큰의 유효성 검사를 통해 사용자 인증을 수행해야한다.

 

이를 구현하기 위해서 JwtAuthenticationFilter를 생성했다.

 

<JwtAuthenticationFilter>

package hello.Member_Management.User.Jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
        this.jwtTokenProvider = jwtTokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = resolveToken(request);
        System.out.println("필터 실행됨. 추출된 토큰: " + token);

        // ✅ 이미 인증된 사용자인지 확인
        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        if (existingAuth != null && existingAuth.isAuthenticated()) {
            System.out.println("이미 인증된 사용자입니다: " + existingAuth.getName());
            filterChain.doFilter(request, response);
            return;
        }

        if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUserPk(token);
            System.out.println("username = " + username);

            try {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                if (userDetails != null) {
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    System.out.println("✅ SecurityContext에 인증 정보 저장 완료");
                } else {
                    System.out.println("❌ 사용자 정보를 찾을 수 없습니다. username: " + username);
                    clearTokenCookie(response); // ✅ 쿠키 초기화
                }
            } catch (UsernameNotFoundException e) {
                System.out.println("❌ 사용자 조회 실패: " + e.getMessage());
                clearTokenCookie(response); // ✅ 쿠키 초기화
            }
        } else {
            clearTokenCookie(response); // ✅ 쿠키 초기화
        }

        filterChain.doFilter(request, response);
    }


    // ✅ 쿠키 삭제 메서드
    private void clearTokenCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("accessToken", null);
        cookie.setHttpOnly(true);
        cookie.setMaxAge(0);  // 쿠키 삭제
        cookie.setPath("/");
        response.addCookie(cookie);
    }





    private String resolveToken(HttpServletRequest request) {
        // 1️⃣ 헤더에서 먼저 토큰을 찾음
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }

        // 2️⃣ 헤더에 없으면 쿠키에서 찾음
        if (request.getCookies() != null) {
            for (Cookie cookie : request.getCookies()) {
                if ("accessToken".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }

        return null;
    }


}

 

 

 

 

 

일반 로그인,회원가입 구현

 

유저가 직접 입력한 아이디, 패스워드를 통해 로그인을 하는 기능도 구현 하였다.

 

토큰을 받는것은 OAuth2로그인과 동일한다.

 

<UserService>

package hello.Member_Management.User.Service;


import hello.Member_Management.User.Entity.User;
import hello.Member_Management.User.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public User registraion(String username, String password, String email,String id){
        User user = new User();
        user.setUserId(id);
        user.setName(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setEmail(email);
        user.setRole("ROLE_USER");
        user.setProvider("Web");
        user.setProviderId("Web");
        userRepository.save(user);
        return user;
    }
}

 

 

<UserCreateForm>

package hello.Member_Management.User;


import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserCreateForm {

    @Size(min=3, max = 25)
    private String username;

    @NotEmpty(message = "아이디는 필수항목입니다.")
    private String id;

    @NotEmpty(message = "비밀번호는 필수항목입니다.")
    private String password1;

    @NotEmpty(message = "비밀번호는 필수항목입니다.")
    private String password2;

    @NotEmpty(message = "이메일은 필수항목입니다.")
    @Email
    private String email;
}

 

 

<IndexController>

package hello.Member_Management.User.Controller;


import hello.Member_Management.User.Entity.User;
import hello.Member_Management.User.Jwt.JwtTokenProvider;
import hello.Member_Management.User.PrincipalDetails;
import hello.Member_Management.User.Repository.UserRepository;
import hello.Member_Management.User.Service.UserService;
import hello.Member_Management.User.UserCreateForm;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
@RequiredArgsConstructor
public class IndexController {

    private final UserRepository userRepository;
    private final UserService userService;
    private final JwtTokenProvider jwtTokenProvider;


    @GetMapping({"","/"})
    public String index(){
        return "index";
    }

    @GetMapping("/user")
    public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails){
        System.out.println("principalDetails = "+principalDetails.getUser());
        return "user";
    }

    @GetMapping("/admin")
    public @ResponseBody String admin(){
        return "admin";
    }

    @GetMapping("/manager") //@ResponseBody ==> 자바객체를 HTTP요청의 바디 내용으로 매핑하여 클라이언트로 전송
    public @ResponseBody String manager(){
        return "manager";
    }

    @GetMapping("/loginForm") //로그인 폼
    public String loginForm() {
        return "loginForm";
    }

    @GetMapping("/joinForm") //회원가입 폼
    public String joinForm(Model model) {
        model.addAttribute("userCreateForm",new UserCreateForm()); //객체 생성후 모델에 담아준다.
        return "joinForm";
    }

    @PostMapping("/joinForm") //회원가입
    public String join(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
      if(bindingResult.hasErrors()){
          return "joinForm";
      }
      if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){
          bindingResult.rejectValue("passwerd2","passwordInCorrect","2개의 패스워드가 일치하지 않습니다.");
          return "joinForm";
      }
      userService.registraion(userCreateForm.getUsername(),userCreateForm.getPassword1(),userCreateForm.getEmail(),userCreateForm.getId());
      return "redirect:/";
    }


}

 

 

 

실행결과

 

데이터베이스에 OAuth2를 이용한 회원 등록이 잘 된것을 확인할 수 있다.

 

 

'프로젝트' 카테고리의 다른 글

BidZone_Front 작업  (0) 2024.12.28