![](https://i0.wp.com/www.simple-coding.com/wp-content/uploads/2024/05/course-details-tab-1.png?resize=300%2C300&ssl=1)
📃 요약
네이버 카페 등의 게시판을 기본 기능을 제작하는 예제입니다.
기본적인 DB CRUD 를 사용해 제작합니다.
DB 프레임워크는 JPA 를 사용해 자동화기능을 강화합니다.
요소 기술 및 테이블 설계는 아래와 같습니다.
요소 기술 :
– 프론트엔드 : JSP
– 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c) & 스프링 시큐리티
결과 화면 :
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/3cb914cc-45eb-41c3-ae56-25ab53fcf559/image.png?ssl=1)
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/8ef3f063-2e40-4228-b078-77029a774653/image.png?ssl=1)
프로젝트 탐색기 :
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/51aca3f6-57dc-4665-b4a6-0a977e88bc4f/image.png?ssl=1)
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 로그인 페이지
결과 화면 :
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/3cb914cc-45eb-41c3-ae56-25ab53fcf559/image.png?ssl=1)
<%@ 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 로그인 후 성공 페이지
결과 화면 :
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/8ef3f063-2e40-4228-b078-77029a774653/image.png?ssl=1)
<%@ 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 로그아웃 페이지 : 로그인하면 메뉴가 변경되고, 메뉴 오른쪽에 로그아웃 버튼 있음
결과 화면 :
![](https://i0.wp.com/velog.velcdn.com/images/forbob/post/c3a3747c-a3c1-4de6-8c9a-477204eea84d/image.png?ssl=1)
<%@ 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 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.
감사합니다.
“15_Simple Coding – JPA – 실전예제 #3 : Login”에 대한 3개의 응답
You actually make it seem so easy along with your presentation but
I to find this topic to be actually one thing
which I think I might by no means understand. It kind of
feels too complicated and extremely wide for me.
I’m having a look ahead for your next submit, I’ll try to get the hold of it!
Escape room lista
Hello there, just became aware of your blog through Google, and found
that it is really informative. I’m going to watch out for brussels.
I’ll be grateful if you continue this in future.
Numerous people will be benefited from your writing.
Cheers! Lista escape room
thanks