gi_dor

Spring Security 로그인 / 로그아웃 본문

Back_End/SpringSecurity

Spring Security 로그인 / 로그아웃

기돌 2026. 2. 22. 19:58

과거 : 직접 세션을 다루는 로그인

   @PostMapping("/login")
    public String loginComplete(@Valid @ModelAttribute MemberDTO memberDTO,BindingResult bindingResult,HttpServletRequest request) {
        // DTO에서 정의한 입력값 검증하기
        if (bindingResult.hasErrors()) {
            return "login"; // login.html
        }

        // memberService에서 login 메서드 호출
        Member loginMember = memberService.login(memberDTO.getEmail(), memberDTO.getPassword());


        // 로그인 실패 처리 - 아이디 공백 , 비밀번호 틀릴경우
        if (loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            log.warn("로그인 실패 : {} ,{}",memberDTO.getEmail(),memberDTO.getPassword());
            return "login";
        }

        // 로그인 성공 (세션 생성)
        HttpSession session = request.getSession();
        session.setAttribute("loginMember", loginMember);

        return "redirect:/";
    }
    
        // 로그아웃
    @PostMapping("/logout")
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return "redirect:/";

    }
<!-- 로그인하지 않은 경우 -->
<th:block th:if="${session.loginMember == null}">
    <a th:href="@{/members/login}">로그인</a>
</th:block>

<!-- 로그인한 경우 -->
<th:block th:if="${session.loginMember != null}">
    <form th:action="@{/members/logout}" method="post">
        <button type="submit">로그아웃</button>
    </form>
</th:block>

 

  • 로그인 : POST /members/login 요청은 Spring Security 가 가로채기에 MemberService 에서 loadUserByUserName을 통해 인증을 처리한다
  • 로그아웃 : POST /members/logout 요청은 Spring Security가 세션을 무효화 하고 쿠키 삭제를 해서 처리해준다
  • 상태확인 : HTML 에서 sec:authorize을 통해 컨트롤러 도움없이 로그인 상태를 확인할 수있다

 

Spring Security에게 모든 것을 위임해버리기

 

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

UserDetails와 UserDetailsService

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class) // 생성일/수정일 자동 관리
@Entity // JPA가 관리하는 엔티티가됨
@Getter // getter 필드 값 가져오기
@Table(name = "member") // 테이블 이름을 명시적으로 지정
public class Member implements UserDetails { // Java에는 Member 객체있음 , DB에는 member 라는 테이블 있음 이걸 1:1 매핑해주는 Entity
    // UserDetails 상속받아서 인증객체로 사용


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 내가 건들이지 않고 DB가 알아서 증가시킴
    private Long id;        // 회원 고유 ID

    @Column(nullable = false, unique = true)        // 중복 불가
    private String email;   // 이메일 (로그인 ID)

    @Column(nullable = false)
    private String password;

    private String name;

    @Column(nullable = false, unique = true)
    private String nickname;    // 사용자 커뮤니티나 마이정보에서 사용할 닉네임

    // 나중에 카카오 로그인을 위해 미리 만들어둠 "google", "kakao", "naver" 등
    private String provider;

    // 사용자인지 , 관리자인지 구분 - 기본 회원가입시 사용자 default값 설정
    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 10)
    private MemberRole memberRole;

    // 등록일
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    // 수정일
    @LastModifiedDate
    private LocalDateTime updatedAt;


    @Builder
    public Member(String email, String password, String name, String nickname, String provider, MemberRole memberRole) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.nickname = nickname;
        this.provider = provider;
        this.memberRole = memberRole;
    }

    @Builder
    public Member(String email, String password) {
        this.email = email;
        this.password = password;
    }

    // 권한반환
    @Override
    @NonNull
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_"+ memberRole.name()));
    }

    // 사용자의 id를 반환 - 고유한값
    @Override
    @NonNull
    public String getUsername() {
        return this.email;
    }

    // 계정이 만료되었는지 확인
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠금되어있는 지 확인
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 패스워드 만료 확인
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 사용 여부 확인
    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

@Service
@RequiredArgsConstructor 
@Transactional(readOnly = true) /
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    // 비밀번호를 암호화하기 위한 도구 (SecurityConfig에 만들어둠)
    private final PasswordEncoder passwordEncoder;


    @NonNull
    @Override
    public UserDetails loadUserByUsername(@NonNull String email) {
        return memberRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("해당하는 이메일을 찾을 수 없습니다 : " + email));
    }

 

SecurityConfig를 설정하여, 로그인/로그아웃 절차를 구체적으로 지시

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // http 객체를 사용하여 메서드 체이닝 방식으로 보안 설정을 구성합니다.
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/", "/css/**", "/js/**", "/images/**", "/members/login", "/members/join").permitAll()
                        .requestMatchers("/members/mypage").authenticated()
                        // 그외 페이지는 인증 요구할꺼임
                        .anyRequest().authenticated()
                )
                // 폼 기반 로그인 설정
                .formLogin(form -> form
                        .loginPage("/members/login") // 로그인 페이지 URL GET
                        .loginProcessingUrl("/members/login") // 로그인 처리 URL POST
                        .usernameParameter("email")
                        .defaultSuccessUrl("/", true) // 로그인 성공 시 항상 메인 페이지로 이동
                        .permitAll() // 로그인 페이지 자체는 누구나 접근 가능
                )
                // 로그아웃 설정
                .logout(logout -> logout
                        .logoutUrl("/members/logout") // 로그아웃을 처리할 URL
                        .logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트될 URL
                        .invalidateHttpSession(true) // 세션 무효화
                        .deleteCookies("JSESSIONID") // 쿠키 삭제
                );

        return http.build();
    }



}

 

 

.loginPage("/members/login")
  • 컨트롤러의 GET 요청과 매핑이라고 생각하면된다
.loginProcessingUrl("/members/login")
  • Security 가 POST 요청을 보고 , 중간에 가로채서 MemberSerivce 안에 있는 loadUserByUserName 을 호출
  • 컨트롤러 XXX
  •  .loginProcessingUrl()에 설정된 URL은 Spring Security의 인증 필터를 깨우는 '가상의' 주소
설정 목적 HTTP 메서드 매핑 대상 컨트롤러에 메서드 필요한가
.loginPage() 로그인 화면 보기 GET MemberController O
.loginProcessingUrl() 로그인 정보 제출/처리 POST Spring Security X

 

 

<!-- 로그인하지 않은 경우 -->
<!-- sec:isAnonymous() : 현재 사용자가 익명(비로그인) 상태인지 확인 -->
<th:block sec:authorize="isAnonymous()">
    <li class="nav-item ms-2">
        <a class="btn btn-outline-light btn-sm" th:href="@{/members/login}">로그인</a>
    </li>
    <li class="nav-item ms-2">
        <a class="btn btn-primary btn-sm" th:href="@{/members/join}">회원가입</a>
    </li>
</th:block>

<!-- 로그인한 경우 -->
<!-- sec:isAuthenticated() : 현재 사용자가 인증(로그인)된 상태인지 확인 -->
<th:block sec:authorize="isAuthenticated()">
    <li class="nav-item">
        <!-- sec:authentication="name" : 현재 인증된 사용자의 이름을 표시 -->
        <span class="nav-link text-white">환영합니다 <span sec:authentication="principal.nickname"></span>님</span>
    </li>
    <li class="nav-item ms-2">
        <form th:action="@{/members/logout}" method="post" class="d-inline">
            <button type="submit" class="btn btn-outline-light btn-sm">로그아웃</button>
        </form>
    </li>
</th:block>

728x90