dev-course day32

2rlokr·2025년 4월 16일

dev-course

목록 보기
32/43
post-thumbnail

오늘 배운 것

📋 폼 기반 인증 방식 실습

1. 권한을 주는 방법 (Role & Authority)

// SecurityFilterChain
http.requestMatchers("/user/**")
        // .hasAnyRole("MEMBER", "MANAGER", "ADMIN")
		.hasAnyAuthority("MEMBER", "MANAGER", "ADMIN");

// UserDetailsService
manager.createUser(
    	User.withUsername("user")
        	.password(encoded)
            // .roles("ADMIN")
            .authorities("ADMIN")
            .build()
);
  • hasAnyRole() : 가변인자로 권한을 받는다. 인자 중 하나의 권한만 가져도 URL에 접근할 수 있다. Role로 권한을 주면 내부에서 ROLE_을 붙여서 처리한다.
  • hasAnyAuthority() : 가변인자로 권한을 받는다.
  • .roles() : 역할을 지정해줄 수 있다. ROLE_이 붙은채로 만들어진다.
  • .authorities() : 역할을 지정해줄 수 있다.

Roles와 Authorities를 함께 쓰기

.authorities("ADMIN")으로 권한을 설정해주고, requestMatchers에서 hasAnyRole("ADMIN") 으로 설정해두었다고 가정해보자.

둘 다 ADMIN으로 권한을 부여했기 때문에 접근할 수 있을 것을 예상하지만, 접근이 되지 않아 403 에러가 뜬다. -> 내부에서 ROLE_ADMIN만 접근할 수 있도록 해뒀기 때문에 접근할 수 없는 것이다.

2. @PostMapping 회원가입

@PostMapping("/signup")
public String doSignUp(SignUpForm signUpForm) {
	log.info("signUpForm = {}", signUpForm);
    memberService.save(signUpForm);
    return "redirect:/";
}
  • 회원가입으로 받아오는 정보는 아이디, 비밀번호, 이메일 정보이다. 여기서 매개변수를 Member 그 자체로 둘 수도 있지만, 컨트롤러에서 엔티티 자체를 사용하는 것은 정보 보호를 위해 권장하지 않는다. -> 그래서 DTO를 만들어서 전달한다.
  • redirect:/" 로 로그인 후 index 페이지로 리다이렉트 되도록 한다.

3. DB에 회원가입 정보 저장

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final PasswordEncoder passwordEncoder;
    private final MemberRepository memberRepository;

    public void save(SignUpForm signUpForm) {

        Member member = Member.builder()
                .username(signUpForm.getUsername())
                .password(passwordEncoder.encode(signUpForm.getPassword()))
                .email(signUpForm.getEmail())
                .build();

        memberRepository.save(member);
	}
  • 비밀번호를 넣어줄 때는 인코딩한 비밀번호를 넣어줘야 한다.
  • SecurityConfig에서 @Bean으로 등록한 PasswordEncoder를 주입 받는다.
    • 인코딩 방식이 바뀌더라도 유연하게 받을 수 있도록 상위 타입으로 선언해둔다.
  • 이렇게 DB에 저장한 후, /login에서 로그인을 시도해도 로그인은 되지 않는다 ! ❌

🔗 UserDetailsService를 구현해주지 않았고, 로그인 시 Spring Security가 사용자 정보를 조회할 수 없어 인증에 실패하게 된다. 따라서 로그인을 가능하게 하려면 loadUserByUsername()을 구현해서 UserDetails 객체를 반환하도록 해야 한다 !

4. 🙋‍♀️ UserDetails & UserDetailsService 구현

UserDetails

public class MemberDetails implements UserDetails {

    private final String username;
    private final String password;
    private final String role;

    public MemberDetails(Member member) {
        this.username = member.getUsername();
        this.password = member.getPassword();
        this.role = member.getRole();
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        return List.of(new SimpleGrantedAuthority(this.role));
    }
  • MemberDetails라는 DTO를 만들어서 UserDetails를 구현해준다.
  • Getter 메서드를 오버라이딩해야 한다.
  • 생성자로 Member를 받고 그 멤버의 유저네임, 비밀번호, 권한을 넣어준다.
  • new SimpleGrantedAuthority() : GrantedAuthority를 구현한 구현체로서, 하나의 Role을 설정해준다.

UserDetailsService

public class MemberService implements UserDetailsService {
	
    // 중간 코드 생략
 
	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Optional<Member> memberOptional = memberRepository.findByUsername(username);
        Member findMember = memberOptional.orElseThrow(
                () -> new UsernameNotFoundException("Username " + username + " not found")
        );

        return new MemberDetails(findMember);
    }
}
  • MemberServiceUserDetailsService를 구현하도록 한다. -> loadUserByUsername을 오버라이딩 해야 한다.
  • loadUserByUsernameUserDetails를 반환해야 한다. (사용자 이름, 비밀번호, 권한을 포함)
  • MemberRepository에서 findByUsername 메서드를 추가해주고, 이 메서드를 이용해 입력받은 아이디(사용자명)의 유저를 DB에서 찾는다.
  • loadUserByUsername()은 사용자 정보 조회만 해준다고 이해하면 된다.
  • 비밀번호 비교는 loadUserByUsername()이 아니라 Spring Security 내부에서 자동으로 처리된다. 리턴된 UserDetails 객체에서 getPassword()로 비밀번호를 꺼내고, 사용자가 폼에 입력한 비밀번호와 비교한다.

🌐 OAuth 2.0 인증 방식 실습

1. SecurityConfig

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	return http
    	.csrf(AbstractHttpConfigurer::disable)
        .formLogin(formLogin -> formLogin.disable() )
        .oauth2Login(Customizer.withDefaults()) //
        .authorizeHttpRequests(auth -> {
        	auth.requestMatchers("/login", "/sign-up")
            		.anonymous()
                .requestMatchers("/user/**")
                	.hasAnyAuthority("USER")
                .requestMatchers("/admin/**")
                	.hasAnyAuthority("ADMIN")
                .anyRequest()
                	.authenticated();
        })
        .build();
}
  • .formLogin(formLogin-> formLogin.disable()) : 폼 로그인 방식을 사용하지 않는다고 설정한다.
  • oauth2Login(Customizer.withDefaults()) : 인증 방식으로 OAuth 인증 방식을 설정하는 코드이다. 별도의 커스터마이징 없이 Spring Security가 제공하는 기본 설정을 사용하는 것이다.

2. Client 등록

OAuth 방식을 사용하기 위해서는 Client를 Resource Server에 등록해줘야 한다.

  • 구글 : Google Cloud Platform
  • 카카오 : Kakao Developers
  • 네이버 : Naver Developers

원하는 플랫폼에서 Client를 등록하고, ClientIDClient 보안 비밀번호 를 받아와야 한다. 이 과정에서 RedirectURL도 반드시 설정해줘야 한다.

Redirect URL ❓

인증이 완료된 후 사용자를 다시 어디로 보내줄지 알려주는 주소이다. Authorization Server 가 인증을 마친 뒤 사용자를 다시 돌려보낼 주소이다. 외부 플랫폼은 redirect_uri로 사용자를 돌려보내면서 Authorization Code를 쿼리 파라미터에 붙여서 전송한다. 그렇게 되면 우리 서버는 이 code를 받아서 토큰을 얻는 다음 단계로 넘어가게 된다.

🎯 redirect url 이 중요한 이유

Authorization Server는 이 URL이 사전에 등록된 값인지 확인하고, 다르면 요청 자체를 거부한다. 임의의 URL로 리다이렉트 못하게 막는 것이다.

설정 파일에 Client 등록

security:
  oauth2:
    client:
      registration:
        google :
          client-id: {발급받은 클라이언트 아이디}
          client-secret: {발급받은 클라이언트 비밀 번호}
          scope:
            - email
            - profile
  • scope : Resource Server에서 얻어오고 싶은 데이터를 설정해준다.

3. MemberDetails & MemberSerivce 구현

MemberDetails 구현


@Getter
public class MemberDetails implements OAuth2User {

    private String name;
    private Map<String, Object> attributes;

    @Setter
    private String role;

    @Builder
    public MemberDetails(String name, Map<String, Object> attributes, String role) {
        this.name = name;
        this.role = role;
        this.attributes = attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(this.role));
    }

}
  • OAuth2AuthenticatedPrincipal를 상속한 OAuth2User를 구현한다.
  • getName(), getAttributes(), getAuthorities()를 오버라이딩 해줘야 한다.
  • 빌더 패턴으로 생성자를 만들어준다.

DefaultOAuth2UserService 상속

public class MemberService extends DefaultOAuth2UserService {
  • 폼 기반에서 UserDetailsService를 구현했던 것처럼, OAuth 방식에서는 DefaultOAuth2UserService 를 상속한다. OAuth2UserService도 있지만, Spring Security에서 제공하는 기본 OAuth2 서비스를 상속받았다. -> loadUser 를 오버라이딩 해야 한다.

loadUser 메서드 오버라이딩

loadUser 메서드는 사용자가 OAuth2로 로그인했을 때 호출된다 ❗️

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

	String provider = userRequest.getClientRegistration().getRegistrationId();

	OAuth2User oAuth2User = super.loadUser(userRequest); 

	Map<String, Object> attributes = oAuth2User.getAttributes();
    String findName = attributes.get("name").toString();
    String email = attributes.get("email").toString();

	Optional<Member> memberOptional = memberRepository.findByEmail(email);

	Member member = memberOptional.orElseGet( // 있으면 있는 거 반환해주고 없으면 람다식 안에 걸 반환해준다.
            () -> {
                Member saved = Member.builder()
                        .name(findName)
                        .email(email)
                        .provider(provider)
                        .build();
                return memberRepository.save(saved);
            }
    );

	MemberDetails memberDetails = MemberDetails.builder()
    		.name(member.getName())
            .attributes(attributes)
            .role(member.getRole())
            .build();

	return memberDetails;
}
  • OAuth2UserRequest : ClientRegistration(구글인지 네이버인지), AccessToken 등의 정보를 담은 객체이다.
  • userRequest.getClientRegistration().getRegistrationId() : 어떤 사이트에서 정보를 받아오는지 제공자의 정보를 가져온다.
  • OAuth2User : OAuth2User 는 google, kakao 등에서 가져온 정보를 담고 있는 객체이다. 즉, Resource Server에서 정보를 받아온 객체로, 요청의 scope에 설정해두었던 정보들이 담긴다.
  • 반환된 oAuth2User에는 attributes가 사용자 정보를 담고 있다. scope에 설정해두었던 정보들이 담긴다. (email, profile)
  • findByEmail() : Resource Server에서 제공한 attributes 중 UNIQUE하다고 판단되는 걸로 DB에서 유저를 찾는다. (이미 회원가입 했는지 여부를 확인하기 위해)
  • .orElseGet() : Optional 내에 객체가 있으면 객체를 반환해주고, 없으면 안의 람다식에서 리턴해서 넣어준다. 즉, 이미 회원가입한 멤버면 그 객체를 반환해주고, 신규 멤버라면 멤버 객체를 DB에 저장한 후 반환해준다.
  • MemberDetails를 빌더 패턴으로 만들면서, 권한도 부여해주고, 이를 반환해준다.

오늘 궁금했던 것 ❓

Q1. RedirectForward가 뭔지 잘 모르겠다. 이것들이 뭔지, 차이점은 뭔지 알아보자.

Forward

  • Web Container 차원에서의 페이지 이동으로, 실제로 웹 브라우저는 다른 페이지로 이동했는지 알 수 없다.
  • 웹 브라우저에는 처음에 호출한 URL만 표시되고, 이동한 페이지의 URL 정보는 볼 수 없다.
  • 현재 실행중인 페이지와 forward에 의해 호출되는 페이지는 Request, Response 객체를 공유한다.
  • POST -> 그대로 POST로 유지된다.

↔️ Forward 방식은 다음 이동한 URL로 요청정보를 그대로 전달한다. 그렇기 때문에 사용자가 최초로 요청한 요청 정보는 다음 URL에서도 유효하다.

Redirect

  • Web Container는 Redirect 명령이 들어오면 웹 브라우저에게 다른 페이지로 이동하라는 명령을 내린다.
  • 웹 브라우저는 URL을 지시된 주소로 바꾸고 그 주소로 이동한다.
  • 새로운 페이지에서 Request, Response 객체는 새롭게 생성된다.
  • POST -> GET 으로 요청 방식이 바뀐다.

↪️ Redirect 방식은 최초 요청을 받은 URL1에서 클라이언트에 redirect 할 URL2를 리턴하고, 클라이언트에게 전혀 새로운 요청을 생성하여 URL2에 다시 요청을 보낸다. 따라서 처음 보냈던 최초의 요청정보는 더이상 유효하지 않게 된다.

정리

방식URL 변화여부객체의 재사용 여부
Forward✔️
Redirect✔️

Q2. loadUserByUsername, loadUser가 언제 호출되는 것인지,, 뭘 하는 메서드인지 한 번 더 정리하기

🔹 loadUserByUsername(String username)

⏱️ 호출 시점 (폼 로그인 과정 중):

  1. 사용자가 로그인 form을 제출함 → POST /login (또는 loginProcessingUrl)
  2. UsernamePasswordAuthenticationFilter가 동작해서 username & password를 추출
  3. AuthenticationManagerUserDetailsService를 사용해 사용자 정보 조회
    ⬇️ 이 시점에 바로
  4. loadUserByUsername() 호출됨

📌 역할

  • 사용자의 DB 정보 조회 (username, password, roles)
  • 그걸 UserDetails 객체로 감싸서 리턴함 → 이후에 비밀번호 체크 진행됨

🔹 loadUser(OAuth2UserRequest userRequest)

⏱️ 호출 시점 (OAuth2 로그인 과정 중):

  1. 사용자가 OAuth2 로그인 버튼 클릭 (예: 구글 로그인)
  2. 인증 서버(구글 등)에서 사용자에게 로그인 요청
  3. 사용자 인증 성공 → Authorization Code → Access Token 발급
  4. Access Token을 갖고 Spring Security가 사용자 정보 요청
  5. ✅ 이 시점에서 loadUser() 호출됨

📌 역할

  • Access Token으로 사용자 정보(Resource Owner Info) 요청
  • 응답 받은 사용자 정보(attributes)를 파싱
  • DB 조회 or 회원가입 처리 후 → 커스텀 OAuth2User 객체로 감싸 리턴

느낀 점

어제 잘 따라잡고 오늘 수업을 들어서인지 오늘 수업은 잘 이해가 됐다 !! 이해하고 보니 더 재밌는 너낌 ㅎ 근데 아직 익숙하진 않아서, 계속 다시 정리한 거 다시 읽어보고, 또 다시 이해하고 실습이랑 매칭하고.. 그랬던 것 같다. 뭔가 인증 방식 여러가지 나오니까 조금 헷갈리는 것도 있고 그랬다.. 익숙해지려면 반복이 답이겠지?

오늘도 절거운 수업이었다 ! :)

0개의 댓글