데브코스 32일차 TIL

Heesu Song·2025년 4월 16일

데브코스 - 백엔드

목록 보기
30/32
post-thumbnail

와우~! 시큐리티 정말 재밌다!
재밌다고해서 안어렵다는건 아니지만 직접 짠 코드가 잘 동작하는게 눈으로 보이니까
훨씬 재밌는것 같은 느낌ㅎㅎ
그래도 아직 헷갈리는 부분이 많아서 혼자 간단한 프로젝트라도 해보면서 익혀야 될 것 같다..!
이전에 프로젝트 할 때, 시큐리티를 혼자 공부해볼려고 하다가 너무 어려워서 포기했는데
확실히 이해하기 쉽게 알려주시는 것 같다👍

Spring Security

Form 기반 실습


메인 화면의 인증 방법

  • id : user (기본)
  • password: Spring 로그에 찍히는 security password

인증과 인가 범위를 잘 설정해주어야함


시큐리티 설정


SecurityFilterChain

  • HttpSecurity 를 매개변수로 받음

CORS

http
	.cors( cors -> cors.disable() )
  • 검사를 해체 시킴

CSRF

http
	.cors( cors -> cors.disable() )

authorizeHttpRequests

어떤 URL에 대해 로그인 여부와 어떤 권한이 있는지 확인

.authorizeHttpRequests( auth -> {
                    auth.requestMatchers("/singup", "/login")
                            .anonymous()
                            .anyRequest()
                            .authenticated();
                })
  • requestMatchers()에 접근 URL 작성
  • 작성된 URL 이외의 URL로 접근하면 403 에러가 뜸 (→ 인가 검사 실패)
  • anyRequestrequestMatchers에 해당되는 URL을 제외한 모든 URL에 대한 설정

FORM기반 로그인

formLogin

.formLogin(Customizer.withDefaults())
  • 디폴트 설정으로 돌아감
.formLogin(form->{
                    form.loginPage("/signin");
                })
  • 로그인 페이지 경로 설정
.formLogin(form->{
                    form.defaultSuccessUrl("/")
				                    .usernameParameter("loginId")
                            .passwordParameter("loginPwd")
                            .loginProcessingUrl("/signin")
                            .loginPage("/signin")
                            .permitAll();
                })
  • loginProcessingUrl 진짜 로그인을 수행할 URL 설정 → POST Mapping
  • usernameParameterpasswordParameter 로 로그인 시 매칭될 id와 password의 명칭을 정해줄 수 있음
  • defaultSuccessUrl → 로그인이 성공되면 이동할 URL
  • successForwardUrl → 로그인 성공하고 후처리

로그아웃

.logout(logout->{
                    logout.logoutUrl("/signout")
                            .logoutSuccessUrl("/")
                            .clearAuthentication(true)
                            .invalidateHttpSession(true)
                            .deleteCookies("JSESSIONID");
                })
  • invalidateHttpSession → HTTP 세션 무효화
  • deleteCookies("JSESSIONID") → 쿠키 삭제

여기까지는 인증 🔐


Security Config

.authorizeHttpRequests(
      auth -> {
           auth.requestMatchers("/signup", "/signin")
               .anonymous()
               .requestMatchers("/user/**")
//                .hasRole("MEMBER")
               .hasAnyAuthority("MEMBER", "MANAGER", "ADMIN")
               .requestMatchers("/manager/**", "/user/**")
               .hasAnyAuthority("MANAGER", "ADMIN")
               .requestMatchers("/admin/**", "/manager/**", "/user/**")
               .hasAuthority("ADMIN")
               .anyRequest()
//               .denyAll();
               .authenticated();
         }
)

authenticated → 인증만 되면 접근 가능

hasAnyRole → 권한을 여려개 넣는게 가능

hasAuthority → ROLE_ 없이 진짜 문자열 그대로 검사

hasRole, hasAnyRole은 내가 넣어준 글자 앞에 ROLE_을 붙여서 검사


Config에서 UserDetailsService

@Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        String encode = passwordEncoder().encode(targetPwd);
        manager.createUser(
                User.withUsername("user")
                        .password(encode)
//                        .roles("MEMBER")
                        .authorities("ADMIN")
                        .build()
        );
        return manager;
    }

manager.createUser() →UserDetails를 만들고 등록해줌

그렇기 때문에 roles() 에서 검사를 할때는 ROLE_ 이 있다고 가정하고 검사

authorities() 를 사용하면 문자열 그대로를 저장하고 검사함


UserDetailsService 구현 클래스

Spring Security가 사용자 정보를 얻기 위해 꼭 구현해야 하는 인터페이스

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    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);
    }

    //id로 UserDetails 타입을 가져올 수 있어야함
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

loadUserByUsername → 사용자가 로그인할 때 Spring Security가 자동으로 호출, username으로 DB에서 사용자를 찾고 결과를 UserDetails로 감싸서 반환


DTO - 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() {

        SimpleGrantedAuthority roleMember = new SimpleGrantedAuthority(this.role);

        return List.of(roleMember);
    }

생성자 → DB에서 유저를 조회한 뒤 그걸 MemberDetails 객체로 감쌈

getAuthorities() → 유저가 어떤 권한을 가졌는지 반환


OAuth2 실습


oauth2Login(Customizer.withDefaults()) 을 통해 사용 가능

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        return http
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .oauth2Login(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers("/login", "signup")
                            .anonymous()
                            .requestMatchers("/user/**")
                            .hasAnyAuthority("ADMIN", "USER")
                            .requestMatchers("/admin")
                            .hasAnyAuthority("ADMIN")
                            .anyRequest()
                            .authenticated();
                })
                .build();
    }
  • 클라이언트 Id와 secret 설정파일에 작성

  • 권한부여 승인코드를 받아올 URL이 필요 → GET 방식으로 파라미터에 담아서 보내줌

http://localhost:8080/login/oauth2/code/google → 링크를 ‘승인된 리디렉션 URI’에 추가


인가 부여 방법

UserDetails 사용

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

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

        OAuth2User oAuth2User = super.loadUser(userRequest);
				log.info("loadUser: {}", oAuth2User);
        return super.loadUser(userRequest);
    }
}
  • MemberService에서 DefaultOAuth2UserService 를 상속받아 이용

로그를 출력해보면

User Attributes: [{sub=116923500273585730404, name=봉봉, 
given_name=봉봉, picture=https://lh3.googleusercontent.com/a/ACg8ocLN25toYSbEq1JQb3DgongyWtrIssN-Mw74GReXVkWrcML8exw2=s96-c, 
email=aladinsue@gmail.com, email_verified=true}]

❗️User Attributes 값을 가져오는 걸 확인 → 회원을 만들어서 등록하는게 가능해진다

로그인 공급자 가져오는 방법

OAuth2UserRequest → 에 ClientRegistration과 AccessToken이 들어있음

userRequest.getClientRegistration().getRegistrationId() 을 통해 가져온다.


회원가입

        Map<String, Object> attributes = oAuth2User.getAttributes();
        String findName = attributes.get("name").toString();        
        Member member = Member.builder()
                .name(findName)
                .nickname(findName)
                .email(attributes.get("email").toString())
                .provider(userRequest.getClientRegistration().getRegistrationId())
                .build();

        memberRepository.save(member);

oAuth2User 에서 Attributes를 가져와서 해당 정보로 회원가입

DB에 저장 완료!

@Getter
public class MemberDetails implements OAuth2User {

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

    @Setter
    private String role;

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

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

OAuth2User를 구현해서 OAuth2 로그인을 처리할 때 사용자 정보를 다룬다.

attributes→ OAuth2에서 받은 모든 사용자 정보

getAuthorities → 사용자의 권한을 가져옴


로그인 후 사용자 정보 처리

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService extends DefaultOAuth2UserService {

    private final MemberRepository memberRepository;

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

        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info("loadUser: {}", oAuth2User);

        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()
                            .email(email)
                            .name(findName)
                            .provider(userRequest.getClientRegistration().getRegistrationId())
                            .build();
                    return memberRepository.save(saved);
                }
        );

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

    }
}
  • DefaultOAuth2UserService를 상속
  • findByEmail로 사용자가 이미 가입되었는지 확인
  • 가입이 안되어있다면 (memberOptional.orElseGet()) Member 객체를 만들어서 DB에 저장
  • 이렇게 OAuth2User를 구현해놓은 MemberDetails를 반환해주면 로그인 처리 완료
profile
Abong_log

0개의 댓글