Coding History/project

OAuto2 인증 받아 구글 로그인 구현중(시큐리티로 로컬로그인 구현으로 변질 됨.)

BlackBirdIT 2024. 9. 6. 10:17

자 이제 정보를 firebase에 가져오는 것 까지 성공했으니, 내 프로젝트까지 이 데이터를 받아와서 가공하고 사용해야되는 단계다.
OAuto2 인증을 사용할거고, 처음엔 다들 하니까 이유가 있겠거니 였는데 이런식으로 접근하면 안되고 이유를 명확히 알아냈다.

크게 딱 두가지인데 보안편의성 때문이다. 가져온 정보를 리소스를 통해서 직접적으로 보는게 아닌 간접적으로 가져올 수 있기 때문이다.

편의성을 위해 또 돌아가야한다니 아이러니하다..

아무튼 한번 해보자.

앞서 한번 더 말하는데 일단 해보는거라 정답이 아닐 확률이 굉장히 높다 흐름만 봐라..

나중에 성공하면 어떻게 했는지 올리겠다.


일단 의존성 추가가 제일 처음 해야할 일이다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

위가 OAuto2고 아래가 서큐리티인데 이건 보안 프레임워크다. 그냥 OAuto2 사용하려면 있어야된다고 생각하면 편하다.

의존성 추가를 했으면,,,,

아 그전에 해당 블로그 참고해서 클라우드 설정부터 하고 오자. 안쓸뻔.

여기서 설정을 마치면

이런게 있다. 이걸 가져가서 이제 구현하면 되는데 나는 그 전에 필요한 것들을 우선 프로젝트에 만들어둬야한다.

일단 앞서 말한 의존성 추가 완료했고, 다음으로 application.yml프로퍼티 설정을 해야한다.

여기 이렇게 설정했다. 가린 부분은 각주가 있으니까 이해했찌?

그리고 config패키지 안에 SecurityConfig 클래스 선언해주고 코드를 넣었는데,

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
                                .antMatchers("/", "/login").permitAll() // 홈과 로그인 페이지는 누구나 접근 가능
                                .anyRequest().authenticated() // 그 외의 요청은 인증된 사용자만 접근 가능
                )
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .loginPage("/login") // 커스텀 로그인 페이지 설정
                                .defaultSuccessUrl("/home") // 로그인 성공 시 리다이렉트할 URL 설정
                                .failureUrl("/login?error=true") // 로그인 실패 시 리다이렉트할 URL 설정
                );

        return http.build();
    }
}

여기서 오류가 났다 그래서 알아보니까

Spring Security의 authorizeRequests 메서드가 더 이상 사용되지 않고, authorizeHttpRequests로 대체되었기 때문이란다. 그리고 아래 오류가 난 antMatchers 부분도 requestMatchers로 대체했다.
그래서 해당부분 수정.

오케이 오류 안난다.

다음으로


JPA 도입 하고 다시 이 글에 복귀.

SecurityConfig 클래스는 도중에 이렇게 변경했다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // CSRF 비활성화
                .csrf(AbstractHttpConfigurer::disable)

                // 포워딩 허용
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() // 포워딩에 대해 인증을 허용
                                // 로그인 페이지와 회원가입 페이지에 대한 접근을 허용
                                .requestMatchers("/usr/member/login", "/usr/member/join", "/usr/member/doLogin", "/usr/member/doJoin").permitAll()
                                // 루트 페이지 및 /usr/home은 인증 필요 없음
                                .requestMatchers("/", "/usr/home/**").permitAll()
                                // 정적 리소스에 대한 접근 허용
                                .requestMatchers("/js/**", "/css/**", "/img/**", "/fontawesome-free-6.5.1-web/**").permitAll()
                                // 나머지 요청은 인증 필요
                                .anyRequest().authenticated()
                )

                // 로컬 로그인 폼 설정
                .formLogin(form -> form
                        .loginPage("/usr/member/login") // 커스텀 로그인 페이지 경로
                        .loginProcessingUrl("/usr/member/doLogin") // 로그인 처리 URL
                        .defaultSuccessUrl("/", true) // 로그인 성공 후 리디렉션될 기본 URL
                        .failureUrl("/usr/member/login?error=true") // 로그인 실패 시 이동할 URL
                        .permitAll() // 로그인 페이지는 인증 없이 접근 가능하게 설정
                )

                // OAuth2 로그인 설정
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/usr/member/login") // 같은 로그인 페이지 사용
                        .defaultSuccessUrl("/", true)  // 로그인 성공 후 리디렉션될 기본 URL
                        .failureUrl("/usr/member/login?error=true")  // 로그인 실패 시 이동할 URL
                )

                // H2 콘솔을 위한 헤더 설정 (프레임을 허용)
                .headers(headersConfigurer ->
                        headersConfigurer.frameOptions(frameOptionsConfig -> frameOptionsConfig.sameOrigin())
                )

                // 로그아웃 설정
                .logout(logout ->
                        logout
                                .logoutUrl("/logout")
                                .logoutSuccessUrl("/usr/member/login?logout")
                                .invalidateHttpSession(true)
                                .deleteCookies("JSESSIONID")
                );

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers(
                        PathRequest.toStaticResources().atCommonLocations()
                );
    }
}

아 잘 안되네..

일단 구현도중 꽤 오래 고민했다. 기존의 로컬로그인이 망가져서 도대체 뭐가 문제인지 몰라 몇시간동안 헤맷다. (사실 지금 이 블로그를 이틀째 쓰고 있다면 믿겠는가?) 몇시간이 아니고 지금 꼬박 이틀이다. 내일 아마 업로드 할거니까 사흘이 되겠구나. 그래도 뭔가 답이 슬슬 보인다. 음.. 생각의 흐름으로 가자고.

생각의 흐름

시큐리티가 문제는 확실하다. 얘가 지금 내 기존 로그인 방식을 가로채서 뭔가 계속 검증하려고 한다. 그래서 내가 만들어 놓은 doLogin 메서드 자체를 진입하지 못한다. 여기서 정말 .. 정말 많이 헤매고 고민했다. 처음에는 포기하고 아예 로컬을 없앨까도 생각하고, 로컬 로그인은 시큐리티와 상관없게 구현되게 할까도 했다.
근데 이게 학원끝나고 음악도 좀 듣고 밥도 먹고 잠시 쉬고 오니까, 먼 발치에 한번 서서 생각해보니까 약간은 해결 방법이 보이는 것 같다. 일단

  1. 메서드 분리. 기존 로컬 로그인과 지금 만들려는 구글 로그인의 메서드를 분리하자.
  2. form태그 분리, 이걸로 submit도 나눠서 보내자. 지금은 통합되어있다.

일단 이 두개를 확실하게 컴퓨터한테 명시해주는게 관리측면도 코드를 보는 사람의 입장에서도 좋은 것 같다는 생각이 들었다. 그래서 기존의 통합방식이 아닌 로컬로그인도 시큐리티의 보안을 거치게 하면서 메서드를 분리하자는 결론이였다.

일단 해보자. 해봐야 알지.

<form action="${pageContext.request.contextPath}/usr/member/doGLogin" method="post">
   <button type="button" class="splicecombyhtmltodesign-fre-eversion3008202485712gmt-form-button1" onclick="googleLogin()">
   </button>
</form>

<%--<form action="${pageContext.request.contextPath}/usr/member/doSLogin" method="post">--%>
       <button type="button" class="splicecombyhtmltodesign-fre-eversion3008202485712gmt-form-button2">
    </button>
<%--</form> 추후 Spotify form태그 미리 나눠서 각주 처리함.--%>

<form action="${pageContext.request.contextPath}/usr/member/doLocalLogin" method="post" id="loginForm">

    <input
        type="email"
        name="email"
        placeholder="Email address"
         class="splicecombyhtmltodesign-fre-eversion3008202485712gmt-input1"    required/>
    <input
        type="password"
        name="loginPw"
        placeholder="password"
        class="splicecombyhtmltodesign-fre-eversion3008202485712gmt-input2"    required/>
    <button type="submit" class="splicecombyhtmltodesign-fre-eversion3008202485712gmt-button2">
        <span>Continue</span>
    </button>
</form>

원래 엄청긴데 다 짤라내고 핵심만 보여줬다. JSP는 이렇게 분리해줬고,

일단 지금의 목표는 Local 로그인을 고치는 거기 때문에,

//  로컬 로그인 메서드
    @RequestMapping("doLocalLogin")
    @ResponseBody
    public String doLocalLogin(HttpServletRequest req, @RequestParam("username") String email, @RequestParam("password") String loginPw, String afterLoginUri) {
        System.err.println("+++++++++++++DO 로컬 로그인 실행.+++++++++");
        rq = (Rq) req.getAttribute("rq");

        Member member = memberService.getMemberByEmail(email);
        System.out.println("Fetched Member: " + member);
        System.out.println("Fetched Password: " + member.getLoginPw());

        if (member == null) {
            return Ut.jsHistoryBack("F-3", Ut.f("%s는(은) 존재하지 않습니다.", email));
        }

        if (!member.getLoginPw().equals(loginPw)) {
            return Ut.jsHistoryBack("F-4", Ut.f("비밀번호가 틀렸습니다."));
        }

        rq.login(member);

        if (afterLoginUri != null && !afterLoginUri.isEmpty()) {
            return Ut.jsReplace("S-1", Ut.f("%s님 환영합니다", member.getNickname()), afterLoginUri);
        }

        return Ut.jsReplace("S-1", Ut.f("%s님 환영합니다", member.getNickname()), "/");
    }

    //구글 로그인 메서드
    public String doGLogin(HttpServletRequest req, @RequestParam("username") String email, @RequestParam("password") String password) {
        return "google";
    }

구글은 명시만 해뒀다.

음 이렇게 했는데 여전히 시큐리티가 가로챈다. 필터 체인 문제라는데, 한번 검색해봐야겠다.

검색해보니까 시큐리티에서 로그인 페이지에서 로그인을 할때 url호출없이 service를 이용할수 있는 로직을 제공해준다고 한다.


다양한 시도를 하다가 왔는데, 일단 결론은 성공시켰다.

메서드를 나눈 방법으로는 여전히 해결하지 못했었다. 이유는 여전히 내가 매핑한 메서드로 들어가지 못하고 시큐리티가 정보를 가로채갔기 때문에 메서드 접근조차 하지 못했다. 이때부터 정신이 다시 나가기 시작했고 다양한 키워드로 검색하며 다른사람들의 글을 계속 보았다.

해당 블로그를 참고해서 내 문제를 해결할 유익한 정보를 얻었다.

UserDetailsService 인터페이스라는 것이였는데, 이걸 갖고 가서 GPT에게 물어봤다. 이걸로 하면 시큐리티가 가로챈 정보를 사용해서 로그인을 구현할 수 있느냐고, 그래서 얻은 받은 YES였고 이제부터는 얘를 사용하면서 내 로컬 로그인 기능을 고쳐보려고 시도했다.

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        System.err.println("로그인 시도 (시큐리티 커스텀)");
        Member member = memberRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return new User(member.getEmail(), member.getLoginPw(), new ArrayList<>());  // Spring Security에서 인증할 User 객체 반환
    }
}

생성한 클래스는 이러했고 여기에 바로 리포지토리를 불러와 이메일을 통한 유저 데이터 검증을 했다.

또, 비밀번호 암호화를 하는김에 했다.

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 비밀번호 암호화를 위한 Encoder
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(customUserDetailsService);  // UserDetailsService 등록
        provider.setPasswordEncoder(passwordEncoder());  // 암호화 방식 등록
        return provider;
    }

회원가입할 때 암호화도 활성화 하고, 다시 회원가입후에 로그인을 시도해봤다.

처음으로 오류 페이지가 떴고 로그인 성공시에 리다이렉트 하는 곳을 정확하게 메인 페이지로 지정 후에 로그인을 시도 했더니

드디어 떴다..

로그인 성공~!! 로컬로그인 시큐리티를 거쳐서 하기 집착으로 성공시켰다.

여기서 핵심은

로컬 로그인을 버리지 않고 가져가면서 시큐리티를 사용하려면 기존의 본인이 만든 컨트롤러로 정보를 가져가지말고 시큐리티에서 제공하는 UserDetailsService를 사용하는 것이 문제 해결에 훨씬 도움이 되고 편안할 것이라는 거.

분명히 내 컨트롤러로 해결하는 방법도 있긴할 것이다. 그래도 3일동안 어떻게든 시큐리티 버림없이 이까지 온 내가 대견하다고 느껴진다.

글을 며칠에 나누어서 써서 맥락이 이상할지도 모르겠다. 일단 로컬로그인을 시큐리티를 통해 성공시켰다는거에 한잔해~

다음으로는 진짜 구글 로그인 시도다!!!