+ 00 00 0000

Have any Questions?

15_Simple Coding – JPA – 실전예제 #3 : Login

15_Simple Coding – JPA – 실전예제 #3 : Login

📃 요약

네이버 카페 등의 게시판을 기본 기능을 제작하는 예제입니다.
기본적인 DB CRUD 를 사용해 제작합니다.
DB 프레임워크는 JPA 를 사용해 자동화기능을 강화합니다.

요소 기술 및 테이블 설계는 아래와 같습니다.

요소 기술 :

– 프론트엔드 : JSP

– 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c) & 스프링 시큐리티

결과 화면 :

프로젝트 탐색기 :

Rest API :

메소드URL설명
POST/customLogin로그인 화면 열기 url
POST/login로그인 진행 : 스프링 시큐리티에서 자동으로 실행됨
POST/customLogout로그아웃 진행 : 스프링 시큐리티에서 자동으로 실행됨

📃 기술 구현

스펙 :

- jdk 17
- spring boot 3.x
- gradle

테이블 설계

-- TODO: 인증관련 테이블 정의

-- 공통코드 테이블은 시퀀스는 사용하지 않음
-- 공통코드 테이블의 등록된 코드는 향후에 않쓰이더라도 삭제/수정하지 않음 : 데이터가 많지않아 오버헤드가 없음
DROP TABLE TB_CODE_CATEGORY CASCADE CONSTRAINT;
DROP TABLE TB_CODE CASCADE CONSTRAINT;

-- 코드성 테이블 : 공통 코드 유형 테이블
CREATE TABLE TB_CODE_CATEGORY
(
    CATEGORY_ID   NUMBER NOT NULL PRIMARY KEY,
    CATEGORY_NAME VARCHAR2(255)
);

-- 코드성 테이블 : 공통 코드 테이블
CREATE TABLE TB_CODE
(
    CODE_ID     NUMBER NOT NULL PRIMARY KEY,
    CODE_NAME   VARCHAR2(255),
    CATEGORY_ID NUMBER NOT NULL
        CONSTRAINT FK_CODE_CATEGORY_CODE REFERENCES TB_CODE_CATEGORY (CATEGORY_ID),
    USE_YN      VARCHAR(1) DEFAULT 'Y'
);

-- TODO: 인증관련 테이블 정의
-- 유저 테이블
-- login table ddl
DROP TABLE TB_MEMBER CASCADE CONSTRAINTS;

CREATE TABLE TB_MEMBER
(
    EMAIL       VARCHAR2(1000) NOT NULL PRIMARY KEY, -- id (email)
    PASSWORD    VARCHAR2(1000),                                         -- 암호
    NAME        VARCHAR2(1000),                                         -- 유저명
    CODE_NAME   VARCHAR2(1000),                                         -- 권한코드명(ROLE_USER, ROLE_ADMIN)
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

로그인 구현을 위한 테이블 설계입니다.

데이터베이스를 오라클을 사용하여 구현해 보겠습니다.

로그인 로직

로그인은 인증(사용자 확인), 인가(권한관리)로 나눔
1) 인증(authority) : 로그인시 적절한 사용자인지 확인하는 절차
2) 인가(authentication) : 로그인은 했지만 일정 권한을 가진 사람만 특정 화면을 볼 수 있는 권리, 권한관리라고도 함

인증 방식 :

1) 쿠키/세션 인증 : 전통적인 인증방식으로 주로 JSP 등을 이용하는 프로그램에서 사용하며, 관련 해킹공격이 있으므로 방어에 유의해야함
    (csrf 해킹 공격)
    - 쿠키 : 웹브라우저에서 기본적으로 제공하는 저장공간 : 주로 민감 보안외의 정보를 넣어서 관리
    - 세션 : 서버에서 정상적인 유저로 확인되면 세션이라는 공간에 인증 정보를 저장하고 세션ID 를 발급해서 쿠키에 넣어 유저에게 전송함
           유저는 로그인 후 다른 화면을 검색할때 정상적인 유저임을 확인하기 위해 항상 쿠키안의 세션id 를 확인함 (쿠키는 서버로 자동전송됨)
    가. 절차 
            1) 로그인 버튼을 클릭하면 유저/암호 정보를 spring 서버로 전송
            2) spring 서버에서 정상적인 유저인지 DB 에서 확인 : 스프링 시큐리티 인증
            3) 확인되면 세션ID 를 발급해서 쿠키에 넣어 유저의 웹브라우저로 전송
            4) 유저는 이제 세션ID 가 있는 쿠키를 가지고 해당 웹사이트의 화면을 볼수 있음
            => 상기의 모든 절차는 spring security 에서 몇가지 설정에 의해 자동으로 모두 이루어지며 기본으로 작동되어 코딩은 필요없음
            => 단, 쿠키/세션에 대한 csrf 해킹공격이 있으므로 방어를 위해 csrf 토큰인증을 추가로 진행해야함
2) 웹토큰 인증 : 현재 MSA 환경에서 사용되는 인증방식으로 정상적인 웹토큰(디지털서명인증)을 지닌 사람만이 화면을 볼 수 있게하는 방식,
     주로 vue/react/angular 와 spring 연결 프로그램 제작시 사용하는 인증 방법
     (쿠키/세션 방식이 아니므로 csrf 해킹은 할수 없음 : csrf 해킹방어 불필요)
    가. 절차
            1) 로그인 버튼을 클릭하면 유저/암호 정보를 spring 서버로 전송
            2) spring 서버에서 정상적인 유저인지 DB 에서 확인 : 스프링 시큐리티 인증
            3) 확인되면 웹토큰 발급해서 유저의 웹브라우저로 전송
            4) 웹브라우저의 저장공간 로컬스토리지 또는 쿠키에 웹토큰 저장
            5) 유저는 이제 웹토큰을 가지고 해당 웹사이트의 화면을 볼수 있음
            => 스프링 시큐리티에서 웹토큰 인증방식을 아직 지원하지 않으므로 웹토큰 클래스 로직을 직접 코딩해야하며 
              , 스프링 시큐리티에 수동으로 적용해야함
            => 로컬스토리지에 저장시 탈취될 위험도 존재하므로 리프레쉬 웹토큰 인증을 적용할 수도 있음(인증 강화)
            => 리프레쉬 토큰전략은 웹토큰 만료시간을 아주 짧게 유지하여 탈취되더라도 만료시간으로 인해 위험도를 낮추는 방식임
              , 정상적인 유저는 웹토큰이 만료되면 리프레쉬 토큰을 spring 에 보내 웹토큰을 재발급 받을 수 있음
                (웹토큰 , 리프레쉬 토큰 모두 탈취되면 방법이 없음 : 
                      최고의 기본보안은 pc, 노트북 등에 잠금장치를 해서 탈취를 방지하고, 암호등이 노출안되게 주의해야함 )

프론트 : JSP

  • 로그인 form 이용,
  • 보안토큰 : csrf 토큰 전송 (쿠키/세션 해킹 공격 방어)

1) 로그인 form 이용 : 방식 – post, /login url 로 로그인 요청, 이때 컨트롤러 함수 작성 불필요하고 스프링 시큐리티에서 자동으로 진행됨
단, 시프링 시큐리티에서 제공하는 User 또는 UserDetails 클래스를 상속받아 사용해야함
2) csrf 토큰 전송 : 쿠키 탈취 해킹 공격 방어 , 로그인 할때 마다 csrf 토큰이 있는 사용자인지 추가 확인함(스프링에서 자동확인됨)
사용법 :

  • 1) 스프링 시큐리티 설정 : 기본설정
  • 2) jsp 설정 : csrf 히든 태그 넣기
    <form>
    ...
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    </form>

벡엔드

– 스프링 시큐리티로 환경 설정 : WebSecurityConfig 클래스 – 권한 관리 가능
 스프링 시큐리티 : 필터 기반 인증을 사용 각각의 인증 필터를 통과해야 전체 인증이 됨 ( 디자인패턴 : 책임연쇄 패턴(chain of responsibility)
예) 인증필터 종류 : 웹토큰 필터, id/pwd 필터 등
– JSP 는 쿠키/세션 인증 사용, 웹토큰 인증은 vue/spring 방식때 사용함

로그인 절차 :

1) 프론트에서 로그인 url 로 form 로그인 버튼 클릭 :

        (1) 최초 로그인 열기 화면 접근 : /customLogin
            - 로그인 버튼 클릭
        (2) /login 로 spring 서버에 로그인 요청 : post 방식으로 웹브라우저 주소창 노출 금지
            - csrf 토큰 전송됨(해킹 방어)

2) 벡엔드 시프링 시큐리티 작동 :

스프링 시큐리티 설정 중 로그인 부분 :

WebSecurityConfig.java

            ...
            http.formLogin()                    // 스프링시큐리티 기본방식 로그인
                .loginPage("/customLogin")      // 사용자가 직접 제작한 로그인 페이지(없으면 스프링에서 화면기본 제공)
                .loginProcessingUrl("/login")   // 로그인 진행
                .defaultSuccessUrl("/");        // 성공후 이동할 페이지
           ...

설명 :

        (1) /login 요청받으면 스프링 시큐리티 설정에 따라 자동 진행됨 :
           가. .loginProcessingUrl("/login") : 스프링 시큐리티에서 자동 로그인 진행, 아래 절차를 따름
           나.  MemberDto : 스프링 시큐리티에서 제공된 User 클래스를 상속받은 클래스 정의, 이때 ID 는 email 로 구현함
           다.  UserDetailsServiceImpl : DB 에 유저 확인 , 권한 생성
              a) 제공된 email로 우리 DB 에 있는 지 확인
              b) 있으면 MemberDto 객체 생성하고 새로운 권한 부여 : 기본권한 - ROLE_USER 권한 부여    
          라. .defaultSuccessUrl("/") : 로그인 성공하면 강제 이동될 페이지
             a) 실패하면 아무것도 하지 않음

로그아웃 절차

1) 프론트에서 form 로그아웃 버튼 클릭 :

        (1) 최초 로그아웃 페이지==header.jsp 메뉴에 있음 : /customLogout
            - 로그아웃 버튼 클릭
        (2) /customLogout 로 spring 서버에 로그아웃 요청 : post 방식으로 웹브라우저 주소창 노출 금지
            - csrf 토큰 전송됨(해킹 방어)

2) 벡엔드 시프링 시큐리티 작동 :

스프링 시큐리티 설정 중 로그아웃 부분 :

WebSecurityConfig.java

        ...
        http.logout()                         //
                .logoutUrl("/customLogout")  // 스프링에서 로그아웃 진행 : 로그아웃 페이지는 따로 필요없음
                .invalidateHttpSession(true) // 로그아웃 시 session 삭제
                .logoutSuccessUrl("/");     // logout에 성공하면 /로 redirect        

        ...

설명 :

        (1) /customLogout 요청받으면 스프링 시큐리티 설정에 따라 자동 진행됨 :
           가. .logoutUrl("/customLogout")   : 스프링에서 로그아웃 진행 : 로그아웃 페이지는 따로 필요없음
           나. .invalidateHttpSession(true)  : 로그아웃 시 session 삭제
           다. .logoutSuccessUrl("/")        : logout에 성공하면 /로 redirect

모델 : 엔티티

공통 모델

package com.example.jpaexam.model.common;

import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : GGG
 * date : 2023-10-16
 * description : JPA 에서 자동으로 생성일자/수정일자를 만들어 주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Getter
// todo: 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
//    todo: 공통속성 : yyyy-MM-dd HH:mm:ss 아니고 기본 패턴으로 보임
    private String insertTime;

    private String updateTime;

////    todo: 해당 테이블에 데이터가 만들어 질때(insert 문) 실행되는 이벤트 함수
    @PrePersist
    void OnPrePersist() {
        this.insertTime
                = LocalDateTime.now()
                    .format(DateTimeFormatter
                            .ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
//
////    todo: 해당 테이블에 데이터가 수정 질때(update 문) 실행되는 이벤트 함수
    @PreUpdate
    void OnPreUpdate() {
        this.updateTime
                = LocalDateTime.now()
                .format(DateTimeFormatter
                        .ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
    }
}

Member.java

package com.example.jpaexam.model.entity.auth;

import com.example.jpaexam.model.common.BaseTimeEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * packageName : com.example.simplelogin.model.entity.auth
 * fileName : User
 * author : kangtaegyung
 * date : 2023/08/06
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/08/06         kangtaegyung          최초 생성
 */
@Entity
@Table(name="TB_MEMBER")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member extends BaseTimeEntity {

  @Id
  private String email;

  private String password;
//  로그인 ID 임
  private String name;

  private String codeName; // 권한명 ( ROLE_USER, ROLE_ADMIN )

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

로그인 레포지토리

package com.example.jpaexam.repository.auth;

import com.example.jpaexam.model.entity.auth.Member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<Member, String> {

  Page<Member> findAllByNameContaining(String name, Pageable pageable);
}

select : @Query 이용한 오라클 기반 쿼리(nativeQuery = true) 임

스프링 시큐리티 관련 클래스

- WebSecurityConfig.java : 스프링 시큐리티 설정 클래스 
- /login url 로 spring 에 요청하면 시프링시큐리티에서 자동 진행됨 
- 자동 실행되는 클래스는 아래의 클래스임
- 아래 클래스를 이용해서 DB 에 사용자가 있는지 확인하고 새로운 권한을 부여함 : 추가 코딩이 필요

WebSecurityConfig : 스프링 시큐리티 설정 클래스

- 다양한 인증/인가 설정 가능 : 예) 특정 웹페이지만 접속, 특정 권한만 접속 등
package com.example.jpaexam.config;

import com.example.jpaexam.security.services.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

/**
 * packageName : com.example.modelexam.controller
 * fileName : FileDbController
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 컨트롤러
 * 요약 :
 * // spring security 라이브러리를 설치하면
 * // 기본적으로 모든 url 에 대해 인증을 진행함
 * // 내부적으로 사용하고 있는 로그인페이지로 자동 리다이렉트함
 * // 기본 user id : user , pwd: 콘솔에 보임
 * // application.properties 파일에 user/pwd 설정 가능
 * // 아래 클래스에서 인증/접근권한을 설정할 수 있음
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */
@Configuration
public class WebSecurityConfig {
    @Autowired
    UserDetailsServiceImpl userDetailsService;  // DB 조회 함수 객체

    //  패스워드 암호화 함수
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*
     * 스프링 시큐리티 룰을 무시하게 하는 Url 규칙(여기 등록하면 규칙 적용하지 않음)
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/js/**", "/images/**", "/css/**");
    }

    //  스프링 시큐리티 룰 정의 : 여기서 여러가지 설정 가능
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//        spring 시큐리티 : 기본보안 - 쿠키/세션 방식 보안 적용됨, 만약 다른 인증(웹토큰) 사용하면 비활성화 해야함
        http.authorizeRequests()
                .antMatchers("/auth/**").permitAll()       // 이 url 은 모든 사용자 접근 허용
                .antMatchers("/admin/**").hasRole("ADMIN") // admin 메뉴는 ROLE_ADMIN 만 가능
                .antMatchers("/**").permitAll();           // 이 url 은 모든 사용자 접근 허용

        http.formLogin()
                .loginPage("/customLogin")                   // 사용자가 만든 화면 로그인 사용
                .loginProcessingUrl("/login")                // Url로 요청이 될 시 SpringSecurity가 직접 알아서 로그인 과정을 진행 : 컨트롤러함수 필요없음, 대신 user(userdetails) 정의 필요
                .defaultSuccessUrl("/");                     // 로그인 성공하면 이동할 페이지 url

//        http.csrf().disable();                               // csrf 보안 비활성화

        http.logout()                                        //
                .logoutUrl("/customLogout")                  // 스프링에서 logout url 제공함 (로그아웃 페이지는 따로 필요없음)
                .invalidateHttpSession(true)                 // session 삭제 후
                .logoutSuccessUrl("/");                      // logout에 성공하면 /로 redirect

        return http.build();
    }
}

MemberDto : User 상속(스프링 시큐리티 제공)

package com.example.jpaexam.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
 * packageName : com.example.jpaexam.security.dto
 * fileName : UserDto
 * author : kangtaegyung
 * date : 1/14/24
 * description : UserDetailsService 에서 사용할 스프링 시큐리티 유저 DTO
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 1/14/24         kangtaegyung          최초 생성
 */
@Getter
@Setter
@ToString
public class MemberDto extends User {

    private String email; // 개발자 추가 속성, 스프링시큐리티의 username 과 같음(로그인 ID 로 사용할 변수)

    public MemberDto(String email, String password,
                     Collection<? extends GrantedAuthority> authority) {
        super(email, password, authority);
        this.email = email;
    }

}

UserDetailsServiceImpl : UserDetailsService 상속(스프링 시큐리티 제공)

package com.example.jpaexam.security.services;

import com.example.jpaexam.model.entity.auth.Member;
import com.example.jpaexam.repository.auth.UserRepository;
import com.example.jpaexam.security.dto.MemberDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

import java.util.HashSet;
import java.util.Set;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : DB 에 ID(email) 로 조회해서 있는지 확인하는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
// 유저 인증을 위한 클래스
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  UserRepository userRepository;

//  유저 인증을 위한 함수
//  DB에 있는 지 확인해서 있으면 UserDto 객체 생성
  @Override
  public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    //  유저 인증을 위한 함수 ( DB 확인 ) : 기본키 (이메일)
    Member member = userRepository.findById(email)
        .orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email));

//    권한 정보 생성 :
    GrantedAuthority authority = new SimpleGrantedAuthority(member.getCodeName());

//    권한이 여러개 저장될 수 있으므로 set로 생성해서 전달되어야 함 : (스프링 시큐리티에서 list 대신 set 을 사용함)
    Set<GrantedAuthority> authoritis = new HashSet<>();
    authoritis.add(authority);

    //  DB에 있는 지 확인해서 있으면 UserDto 로 User 객체 생성
    return new MemberDto(member.getEmail(), member.getPassword(), authoritis);
  }

}

로그인 서비스

package com.example.jpaexam.service.auth;

import com.example.jpaexam.model.entity.auth.Member;
import com.example.jpaexam.repository.auth.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Optional;

/**
 * packageName : com.example.simpledms.service
 * fileName : UserService
 * author : kangtaegyung
 * date : 2023/07/13
 * description : User 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    //    username like 검색
    public Page<Member> findAllByUsernameContaining(String username, Pageable pageable) {

        //        제목 조회 결과
        Page<Member> page = userRepository.findAllByNameContaining(username, pageable);

        return page;
    }

    public Optional<Member> findById(String email) {
        Optional<Member> optionalUser = userRepository.findById(email);

        return optionalUser;
    }

    public boolean existsById(String email) {
        boolean bResult = userRepository.existsById(email);

        return bResult;
    }

    public Member save(Member user) {
        Member user2 = userRepository.save(user);

        return user2;
    }

    public boolean removeById(String email) {

        if (userRepository.existsById(email)) {
            userRepository.deleteById(email);
            return true;
        }

        return false;
    }

}

로그인 컨트롤러

package com.example.jpaexam.controller.auth;

import com.example.jpaexam.service.admin.CodeService;
import com.example.jpaexam.service.auth.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : 부서 컨트롤러
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
@Slf4j
@Controller
public class AuthController {

  @Autowired
  UserService userService;

  @Autowired
  CodeService codeService;

  //  로그인 함수 :
      @GetMapping("/customLogin")
  public String login() {

   return "auth/customLogin.jsp";
  }

//  로그아웃 함수 :
//    @GetMapping("/customLogout")
//    public String logout() {
//          log.debug("custem logout");
//
//          return "auth/customLogout.jsp";
//    }

}

jsp 페이지

jsp 로그인 페이지

결과 화면 :

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <title>Title</title>
</head>
<body>
<%--header--%>
<jsp:include page="../common/header.jsp"/>

<div content="container">
    <div class="row justify-content-center">
        <div class="col-xl-10 col-lg-12 col-md-9">
            <div class="card mt-5">
                <div class="card-body p-0">
                    <!-- {/* Nested Row within Card Body */} -->
                    <div class="row">
                        <div class="col-lg-6 bg-login-image"></div>
                        <div class="col-lg-6">
                            <div class="p-5">
                                <div class="text-center">
                                    <h1 class="h4 mb-4">Welcome Back!</h1>
                                </div>
                                <form class="user" method="post" action="/login">
                                    <div class="form-group">
                                        <input
                                                type="email"
                                                class="form-control form-control-user mb-3"
                                                id="username"
                                                aria-describedby="emailHelp"
                                                placeholder="Enter Email Address..."
                                                name="username"
                                        />
                                    </div>

                                    <div class="form-group">
                                        <input
                                                type="password"
                                                class="form-control form-control-user mb-3"
                                                id="password"
                                                placeholder="password"
                                                name="password"
                                        />
                                    </div>

                                    <button class="btn btn-primary btn-user w-100 mb-3">Login</button>

                                    <hr/>
                                    <a href="/" class="btn btn-google btn-user w-100 mb-2">
                                        <i class="fab fa-google fa-fw"></i> Login with
                                        Google
                                    </a>
                                    <a href="/" class="btn btn-naver btn-user w-100 mb-2">
                                        <i class="fa-solid fa-n"></i> Login with Naver
                                    </a>
                                    <a href="/" class="btn btn-kakao btn-user w-100 mb-3">
                                        <i class="fa-solid fa-k"></i> Login with Kakao
                                    </a>
                                    <%--                                    csrf 보안 토큰 : 해커 쿠키 탈취해 위조 방지 --%>
                                    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

                                </form>
                                <hr/>
                                <div class="text-center">
                                    <a class="small" href="/forgot-password">
                                        Forgot Password?
                                    </a>
                                </div>
                                <div class="text-center">
                                    <a class="small" href="/register">
                                        Create an Account!
                                    </a>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<%--footer--%>
<jsp:include page="../common/footer.jsp"/>
</body>
</html>

jsp 로그인 후 성공 페이지

결과 화면 :

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<%--header--%>
<jsp:include page="common/header.jsp"/>

<div class="container">
    안녕하세요 Simple Coding Jsp 사이트에 오신 것을 환영합니다.

<%--    세션이 있으면 정보가 보임 없으면 안보임 --%>
    <sec:authorize access="isAuthenticated()">
        <p>principal : <sec:authentication property="principal"/></p>
        <p>사용자이름 : <sec:authentication property="principal.username"/> /></p>
    </sec:authorize>
</div>

<script>
    let principal = "<sec:authentication property="principal" />";

    console.log("principal", principal);
</script>

<%--footer--%>
<jsp:include page="common/footer.jsp"/>

</body>
</html>

jsp 로그아웃 페이지 : 로그인하면 메뉴가 변경되고, 메뉴 오른쪽에 로그아웃 버튼 있음

결과 화면 :

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <!--    bootstrap css cdn 추가 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <%--    css 연결--%>
    <link href="/resources/css/login.css" rel="stylesheet">
</head>
<body>
<!-- 세션정보 접근 -->

<nav class="navbar navbar-expand-lg mb-4" style="background-color: #e3f2fd;">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">
            <%--         todo: img 기본 경로 : resources/static 임 : 절대경로로 넣을 것   --%>
            <img src="/resources/img/simple-coding.png" width="20" height="20"/> 
            Navbar
        </a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <%--                부서 시작--%>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
                       aria-expanded="false">
                        Dept
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/basic/dept">Dept</a></li>
                        <li><a class="dropdown-item" href="/basic/dept/addition">Add Dept</a></li>
                    </ul>
                </li>
                <%--                부서 끝--%>

                <%--                회원 시작--%>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
                       aria-expanded="false">
                        Emp
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/basic/emp">Emp</a></li>
                        <li><a class="dropdown-item" href="/basic/emp/addition">Add Emp</a></li>
                    </ul>
                </li>
                <%--                회원 끝--%>

                <%--                업로드 시작--%>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
                       aria-expanded="false">
                        FileDb
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/advanced/fileDb">FileDb</a></li>
                        <li><a class="dropdown-item" href="/advanced/fileDb/addition">Add FileDb</a></li>
                    </ul>
                </li>
                <%--                업로드 끝--%>

                <%--                업로드 시작--%>
                <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
                       aria-expanded="false">
                        Gallery
                    </a>
                    <ul class="dropdown-menu">
                        <li><a class="dropdown-item" href="/advanced/gallery">Gallery</a></li>
                        <li><a class="dropdown-item" href="/advanced/gallery/addition">Add Gallery</a></li>
                    </ul>
                </li>
                <%--                업로드 끝--%>
            </ul>
            <%--                로그인 시작 : isAnonymous : 세션이 없으면 --%>
            <ul class="navbar-nav ms-auto mb-2 mb-lg-0">
                <%--                어드민 시작 : isAnonymous : 세션이 없으면 --%>
                <sec:authorize access="hasRole('ADMIN')">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="/customLogin">Admin</a>
                    </li>
                </sec:authorize>
                <%--                어드민 끝--%>

                <sec:authorize access="isAnonymous()">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="/customLogin">Login</a>
                    </li>
                </sec:authorize>
                <%--                로그인 끝--%>
                <%--                로그아웃 시작 : isAuthenticated : 세션이 있으면 --%>
                <sec:authorize access="isAuthenticated()">
                    <li class="nav-item d-flex align-items-center">
                        <form method="POST" action="/customLogout">
                            <button class="btn" type="submit">Logout </button>
                            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
                        </form>
                    </li>
                </sec:authorize>
            </ul>
            <%--                로그아웃 끝--%>
        </div>
    </div>
</nav>
</body>
</html>

📃 결론

로그인/로그아웃 샘플 예제를 살펴보았습니다.
스프링 시큐리티 설정을 이용해서 구현했으며 스프링 시큐리티의 자동 실행되는 내부 클래스에 수정해서 사용해봤습니다.
인증 방식은 기본인증 방식인 쿠키/세션 방식을 적용했으며,
csrf 토큰으로 해킹공격을 방어했습니다.

DB 프레임워크는 JPA 를 이용해서 sql 문을 직접 제작하지 않고 자동화기능을 이용해 구현했습니다.
Mybatis 의 직접 sql 문 제작 기능에 반해서 자주 반복되고 쉬운 기능은 JPA 의 sql 자동생성 기능을 이용하고,
복잡한 sql 문은 @Query 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다