LUMI_dev

Spring Security - #2. 로그인 (세션) // @AuthenticationPrinciple 본문

스파르타 코딩 클럽 | 자바 심화 과정/Spring Master (숙련 주차)

Spring Security - #2. 로그인 (세션) // @AuthenticationPrinciple

luminous_dev 2025. 2. 7. 01:14

로그인 처리 과정 이해

스프링 시큐리티 사용 전

 

스프링 시큐리티 사용 후

 

  • Client의 요청은 모두 Spring Security를 거침
  • Spring Security 역할
    • 인증/인가 
      • 성공 시 : Controller 로 Client 요청 전달 ( Client 요청 + 사용자 정보 (UserDetails))
      • 실패 시: Controller 로 Client 요청 전달되지 않음 (Client 에게 Error Response 보냄)

로그인 처리 과정

 

  1. Client
    • 로그인 시도
    • 로그인 시도할 username, password 정보 → HTTP body 로 전달 (POST 요청)
    • 로그인 시도 URL 은 WebSecurityConfig 클래스에서 변경 가능
      • "POST /api/user/login" 로 설정하는 법 
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // CSRF 설정
    http.csrf((csrf) -> csrf.disable());

    http.authorizeHttpRequests((authorizeHttpRequests) ->
            authorizeHttpRequests
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                    .anyRequest().authenticated() // 그 외 모든 요청 인증처리
    );

    // 로그인 사용
	 http.formLogin((formLogin) ->
            formLogin
                 // 로그인 처리 (POST /api/user/login)
                .loginProcessingUrl("/api/user/login").permitAll()
    );

    return http.build();
}

 

   2. 인증 관리자 (Authentication Manager)

      UserDetailsService 에게 username 전달 / 회원 상세 정보 요청 

 

   3. UserDetailsService

      1) 회원 DB에서 회원 조회 :  회원 정보가 존재하지 않을 시 → Error 발생

User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

 

      2) 조회된 회원 정보(user) 를 UserDetails 로 변환

     

UserDetails userDetails = new UserDetailsImpl(user)

 

    3) UserDetails 를 "인증 관리자"에게 전달

 

4. 인증관리자인증처리

1) 아래 2 개의 username, password 일치 여부 확인

Client 가 로그인 시도한 username, password
vs
UserDetailsService 가 전달해준 UserDetails 의 username, password

 

2) password 비교 시

Client 가 보낸 password 는 평문 /  UserDetails 의 password 는 암호문

→ Client 가 보낸 password 를 암호화해서 비교

 

3) 인증 성공하면 → 세션에 로그인 정보 저장 / 인증 실패 시 → Error 발생


로그인 구현 

직접 Filter를 구현해서 URL 요청에 따른 인가를 설정한다면 코드가 매우 복잡해지고 유지보수 비용이 많이 들 수 있음

Spring Security를 사용하면 이러한 인가 처리가 굉장히 편해짐 

 

WebSecurityConfig

package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용
        http.formLogin((formLogin) ->
                formLogin
                        // 로그인 View 제공 (GET /api/user/login-page)
                        .loginPage("/api/user/login-page") //로그인 페이지
                        
                        // 로그인 처리 (POST /api/user/login)
                        .loginProcessingUrl("/api/user/login") //우리가 컨트롤러에 만든게 아니고 시큐리티 앞단에서 처리

                        // 로그인 처리 후 성공 시 URL
                        .defaultSuccessUrl("/")

                        // 로그인 처리 후 실패 시 URL
                        .failureUrl("/api/user/login-page?error")

                        .permitAll()
        );

        return http.build();
    }
}


이 설정으로 시큐리티가 제공하는 기본 로그인 창 사라짐 (.loginPage)

 

람다식 

(parameter) -> expression
여기서:

parameter는 람다 함수의 입력값을 의미합니다.
expression은 그 함수가 실행할 작업을 나타냅니다.

 


UserDetailsServiceImpl, UserDetailsImpl

 

구현받아서 쓰므로 뒤에 Impl 붙이기

 

 

UserDetailsServiceImpl, UserDetailsImpl이 상속 받는 UserDetailService, UserDetails =시큐리티가 제공하는 인터페이스

 

UserDetailServiceImpl

package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username) //데이터와 연결해서 user 정보 가져오기
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user); //UserDetailsImpl
    }
}

 

 

UserDetailsImpl

package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private final User user;

    //UserDetailsServiceImpl에 user값 받아와서 다음 실행
    public UserDetailsImpl(User user) {
        this.user = user;
    }

    //user 필요할 때 가져갈 수 있도록
    public User getUser() {
        return user;
    }

    //실제로 우리가 사용할 것

    @Override
    public String getPassword() {
        return user.getPassword();
    } //비밀번호가 필요할 경우

    @Override
    public String getUsername() {
        return user.getUsername();
    }//username이 필요할 경우

    //접근 불가 페이지 만들기
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }
    //실제로 우리가 사용할 것

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

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

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

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

 

세션 Id가 없는 경우는 로그인 창으로 튕김

 

주석처리해주기 basic.js  

 

ProductController

이전

package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

//Filter에서 user Request 객체에 넣은 거 받아와볼 것임
@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(HttpServletRequest req) {
        System.out.println("ProductController.getProducts : 인증 완료");
        //AuthFilter에서 .setAttribute해준 user 가져올 때는 .getAttribute
        User user = (User) req.getAttribute("user");
        System.out.println("user.getUsername() = " + user.getUsername());

        return "redirect:/";
    }
}

 

이후

세션.getAttribute하는 것보다 편한 방법 @AuthenticationPrinciple

이 안의 Principle에는 UserDetail이 있는데 그걸 꺼내오는 것이 @AuthenticationPrinciple

package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

//Filter에서 user Request 객체에 넣은 거 받아와볼 것임
@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {

        //Authentication의 Principle -> Principle에 UserDetail 들어감 -> 그걸 @AuthenticationPrincipal로 가져옴
        User user = userDetails.getUser(); //UserDetailsServiceImpl에서 UserDetailsImpl에 user을 넘겨서 저장된 user 가져오기
        System.out.println("user.getUsername() = " + user.getUsername());
        System.out.println("user.getEmail() = " + user.getEmail());
        return "redirect:/";
    }
}

  

basic.js 롤백