2026. 4. 28. 21:01ㆍ대우개발원 수업 내용/spring boot, framework
Spring Security 기반 자동 로그인 구현 및 사용자별 보안 로직 강화
1. Remember-Me(로그인 유지) 기능 및 DB 연동 설정
- 자동 로그인 구현: 세션이 만료되어도 쿠키를 통해 인증을 유지하는 기능을 추가하고, 보안을 위해 토큰을 DB(persistent_logins)에 저장하도록 설정함
- 로그인 상태 유지: 서버가 꺼졌다 켜져도 사용자가 다시 로그인할 필요 없도록 JDBC 저장소를 연결해 안정적인 환경을 구축함
- 화면 적용: 로그인 폼에 체크박스를 배치하여 사용자가 직접 자동 로그인 여부를 선택할 수 있게 함
2. 타임리프를 활용한 사용자 정보 연동 및 작성자 제한
- 인증 정보 활용: 시큐리티 확장 라이브러리를 통해 로그인한 사람의 정보를 자바스크립트에서 바로 쓸 수 있게 연결함
- 작성자 자동 입력: 글이나 댓글을 쓸 때 본인의 아이디가 자동으로 입력되게 하고, 다른 사람의 아이디로 고칠 수 없도록(readonly) 잠금 처리함
3. 본인 확인 및 권한 검증 로직 적용 (@PreAuthorize)
- 접근 제어: 로그인한 사람만 게시글을 볼 수 있게 제한하고, 수정이나 삭제는 반드시 '글을 쓴 당사자'만 가능하도록 서버에서 한 번 더 검증함
- 버튼 제어: 상세 페이지에서 현재 로그인한 유저와 작성자가 다를 경우, '수정' 버튼 자체가 아예 보이지 않도록 화면을 구성함
4. 댓글 시스템의 사용자 본인 확인 처리
- 권한 체크: 자바스크립트를 이용해 현재 접속자와 댓글 작성자가 일치하는지 확인하는 로직을 추가함
- 부정 접근 차단: 남의 댓글을 수정하거나 삭제하려고 시도할 경우 경고 메시지를 띄우고 창을 닫아 데이터 조작을 원천 봉쇄함
5. 권한 부족 시 예외 처리(Custom403Handler) 설정
- 에러 가로채기: 주소를 강제로 입력해 남의 글에 접근하려다 발생하는 '403 권한 없음' 에러를 전용 핸들러가 처리하게 함
- 맞춤 응답: 일반 사용자는 에러 메시지와 함께 로그인 페이지로 보내고, 데이터 통신(JSON) 시에는 상태 코드만 깔끔하게 전달해 프론트에서 대응하게 함
[요약]
Spring Security 기반 자동 로그인 구현 및 사용자별 보안 로직 강화
1. 자동 로그인(Remember-Me) 설정으로 사용자 편의성 및 데이터 유지력 향상
2. 로그인 유저 정보를 활용한 작성자 자동 할당 및 사칭 방지 기능 구현
3. 서버 측 권한 검증(@PreAuthorize)을 통한 게시물 보안 및 버튼 노출 제어
4. 댓글 작성자 일치 여부 확인으로 본인만 수정/삭제 가능한 환경 구축 5. 전용 핸들러(403Handler)를 이용한
잘못된 접근 차단 및 리다이렉트 처리
CSRF 토큰
• CSRF 토큰의 생성과 사용
• CSRF 토큰은 보통 서버 측에서 생성됩니다. 토큰은 사용자의 세션 ID와 함께 생성되며,
이 두 가지 정보는 서로 연관되어 있습니다. 사용자가 로그인하면 서버는 세션 ID와 CSRF 토큰을 생성하고, 이를 사용자의
브라우저에 저장합니다.
• 사용자가 웹 사이트에 요청을 보낼 때, 이 토큰은 요청과 함께 서버에 전송됩니다. 서버는 토큰을 검증하여 요청이 유효한지
확인합니다. 토큰이 유효하다면 요청은 처리되고, 그렇지 않다면 요청은 거부됩니다.
• 이러한 방식으로 CSRF 토큰은 웹 애플리케이션을 보호하며, 사용자의 세션을 안전하게 유지합니다.
하지만 이 방식은 완벽하지 않으며, 여전히 CSRF 공격 취약할 수 있습니다.
따라서 웹 개발자들은 CSRF 토큰 외에도 다른 보안 방법을 함께 사용해야 합니다.
post 방식으로 처리하는방법이 없는데 member.login으로 처리되는 이유

MemberController에서
@GetMapping("/login")
밖에 없지만 CustomSecurityConfig 코드의
http.formLogin(httpSecurityFormLoginConfigurer -> {
httpSecurityFormLoginConfigurer.loginPage("/member/login");
});
이부분 때문에 post 방식과 비슷하게 작동
==> "POST 방식과 동일하게 작동한다"기보다는,
"Spring Security가 POST 요청 처리를 전담하게 설정했으므로 개발자가 따로 컨트롤러를 만들 필요가 없다.
MemberController에 아래 코드를 추가
로그인 페이지로 리다이렉트될 때 쿼리 스트링에 logout 파라미터가 포함되어 있다면,
사용자가 로그아웃했음을 감지하여 로그를 남기는 조건문
if(logout != null) {
log.info("user logout..............");
}
login.html 에 아래코드를 추가
타임리프(Thymeleaf)의 파라미터 객체를 이용해 URL에 logout이라는 이름의
쿼리 스트링이 존재할 경우에만 화면에 로그아웃 완료 메시지를 출력하는 조건부 렌더링 코드
<th:block th:if="${param.logout != null}">
<h1>Logout...........</h1>
</th:block>
실행하고
http://localhost:8080/member/login?logout 으로 접속하면
Logout.....이라는 글이 보인다
모바일 환경처럼 매번 아이디와 비밀번호를 입력하기 번거로운 상황을 개선하기 위해,
Remember-Me(로그인 유지) 기능을 도입하여 쿠키 유효기간 동안 자동 인증이 유지되도록 구현
(Remember-Me : 세션이 만료되더라도 사용자가 다시 로그인할 필요가 없도록 브라우저에 암호화된 토큰(쿠키)을 보관)
webdbplus 콘솔창에 아래 쿼리 구문을 입력
스프링 시큐리티의 Remember-Me(로그인 유지) 기능을 사용할 때,
인증 토큰 정보를 데이터베이스에 반영구적으로 저장하기 위해 필요한 전용 테이블
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null );
CustomSecurityConfig 에 아래 구문들을 추가한다
1. 인증 및 자동 로그인 기능을 처리하기 위한 핵심 의존성
2. 자동 인증이 가능하도록 구성하는 로직
3. JDBC 기반의 토큰 저장소를 설정하는 로직
데이터베이스 연결 정보를 담고 있는 DataSource와 사용자 상세 정보를 조회하는
CustomUserDetailsService를 주입받아 인증 및 자동 로그인 기능을 처리하기 위한 핵심 의존성
private final DataSource dataSource; // javax로 import
private final CustomUserDetailsService userDetailsService;
스프링 시큐리티의 Remember-Me(로그인 유지) 기능을 설정하여,
세션이 만료된 후에도 브라우저에 저장된 토큰을 통해 자동 인증이 가능하도록 구성하는 로직
http.rememberMe(httpSecurityRememberMeConfigurer -> {
httpSecurityRememberMeConfigurer.key("12345678") // remember-me 쿠키를 암호화/검증할 때 사용하는 키
.tokenRepository(persistentTokenRepository()) // remember-me 토큰을 DB에 저장하는 방식
.userDetailsService(userDetailsService) // 자동 로그인 시 사용자 정보를 조회할 서비스
.tokenValiditySeconds(60*60*24*30); // 자동 로그인 유지 시간 설정. 30일
});
사용자가 로그인 유지(Remember-Me)를 선택했을 때 발행되는 토큰을 데이터베이스에 저장하고 관리할 수 있도록
JDBC 기반의 토큰 저장소를 설정하는 로직
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
return repo;
login.html 패스워드 아라 코드에 아래 내용을 추가
로그인 폼에서 사용자가 자동 로그인 여부를 선택할 수 있도록 체크박스를 생성하며,
name="remember-me" 속성을 통해 스프링 시큐리티의 Remember-Me 필터와 연동되는 입력창
<div class="input-group mb-3">
<input class="form-check-input" type="checkbox" name="remember-me">
<label class="form-check-label">
자동 로그인
</label>
</div>
실행하면
자동로그인 체크박스가 생기고
체크박스를 체크하고 개발자 도구에 로그를 보면 remeber-me라는 쿠기가 생기고 30일 유효기간이 있는것을 알 수 있다.
login이 되어 있는 상태에서는 db에 series, token값이 생긴다
logout에 직접 접근하면 쿠기가 사라진 것을 볼 수 있다.(http://localhost:8080/logout)
또한, login이 되어 있는 상태에서는 db에 series, token값이 사라진다.
build.gradle에 아래 코드를 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
인증이 필요한 페이지는 register이기 때문에
register.html 안에 있는 script 구간에 아래 코드를 추가
타임리프(Thymeleaf)의 인라인 자바스크립트 문법을 사용하여,
서버에서 관리하는 현재 사용자의 인증 정보(Authentication 객체)를 자바스크립트 변수 auth에 JSON 형식으로 할당하는 코드
const auth = [[${#authentication}]]
CustomUserDetailsService 클래스의 아래 표시된 부분과

authorities("ROLE_USER")는 로그인한 사용자에게 USER라는 직급(권한)을 부여하여,
@PreAuthorize로 보호된 기능에 접근할 수 있는 열쇠를 쥐여주는 설정
=> 서버에서 부여한 authorities("ROLE_USER") 권한 정보가 타임리프를 통해 자바스크립트 변수 auth에 담김으로써,
프론트엔드에서도 현재 사용자의 권한을 확인하고 화면 제어에 활용할 수 있게 됨
실행 하면 개발자도구에서 인증객체로 값이 들어오는것을 확인할 수 있다
register.html 에 writer부분의 코드를 수정
<input type="text" name="writer" class="form-control" placeholder="Writer"
th:value="${#authentication.principal.username}" readonly>
수정하고 다시 실행해서 로그인을 해보면
Writer에 user1이 들어와 있는것을 확인.
(readonly를 썼기 때문에 수정이 불가능하다.)
조회
BoardController
read, modify부분에 아래 어노테이션을 추가
@PreAuthorize("isAuthenticated()")
★ isAuthenticated() : 현재 사용자가 익명이 아니라면 (로그인 상태라면) true
실행하면
list페이지에서 read를 하려고 게시글을 눌렀을때 로그인이 되어 있지 않으면
로그인을 해야 보이도록 로그인창이 나온다
수정
위에서는 해당 글의 Writer가 1313인데 로그인한 유저는 user1이기 때문에
해당하는 유저만 modify, remove가 가능하도록 수정
read.html 에서
<div class="my-4">
<div class="float-end" th:with="link = ${pageRequestDTO.getLink()}">
<a th:href="|@{/board/list}?${link}|" class="text-decoration-none">
<button type="button" class="btn btn-primary">List</button>
</a>
<a th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
<!--/board/modify?bno=123-->
<button type="button" class="btn btn-secondary">Modify</button>
</a>
</div>
</div>
이 구간 안에 인증을 받고 동작을 해보자고 하는 (user정보를 같이 가지고 가자) 코드
<div class="my-4" th:with="user=${#authentication.principal}">
제일 윗 부분 코드를 변경
조건이(writer의 이름과 username의 이름이 같을 때만) 맞을때만 modify 버튼이 표시되도록 하는 코드
<a th:if="${user != null && user.username == dto.writer}"
th:href="|@{/board/modify(bno=${dto.bno})}&${link}|" class="text-decoration-none">
다시 실행하고
현재 user1으로 로그인되어 있는 상태에서 게시물을 만듬
read페이지로 넘어가면 Writer가 user1인 경우에는 modify버튼이 보이지만 1313인 경우에는 modify버튼이 보이지 않음
댓글 추가
게시물에 있는 댓글에

Replyer에도 user1이 써지도록 수정한다
read.html에서
아래 Replyer 구문을
<div class="input-group mb-3" >
<span class="input-group-text">Replyer</span>
<input type="text" class="form-control replyer" >
</div>
아래 구문으로 변경
로그인된 사용자 정보를 변수에 담아 입력창에 자동 할당하고 수정을 막음으로써,
작성자 본인 인증을 자동화하고 사칭을 방지하는 설정
<div class="input-group mb-3" th:with="user=${#authentication.principal}">
<span class="input-group-text">Replyer</span>
<input type="text" class="form-control replyer" th:value="${user.username}" readonly>
</div>
그리고 아래 구문을 주석처리
Replyer의 내용을 사라지게 하면 안되기 때문
//replyerText.value = '' // 댓글 입력란 초기화
다시 실행해 보면
Replyer에 user1이 자동으로 들어가 있는것을 확인
댓글 수정 삭제
본인이 작성한 댓글만 수정/삭제가 가능하도록
read.html 코드의 script안에 아래 구문을 추가
서버에서 인증된 사용자의 아이디(username)를 타임리프 문법으로 자바스크립트 변수 currentUser에 직접 할당하여,
프론트엔드에서 본인 확인이나 권한 제어 등에 사용할 수 있게 하는 코드
const currentUser = [[${#authentication.principal.username}]]
댓글의 작성자와 currentUser가 일치하는지 확인하는 변수를 추가
let hasAuth = false // 댓글의 작성자와 currentUser의 일치여부
hasAuth가 true인지 false인지에 따라 동작이 달라지도록
현재 사용자가 해당 댓글의 작성자가 아닐 경우(!hasAuth),
경고 메시지를 띄우고 수정창을 닫아 인가되지 않은 사용자의 접근을 차단하는 방어 코드
modifyBtn.addEventListener("click", function(e) {
아래에 넣음
if(!hasAuth) {
alert("댓글 작성자만 수정이 가능합니다.")
modifyModal.hide()
return
}
removeBtn.addEventListener("click", function(e) {
아래에 넣음
if(!hasAuth) {
alert("댓글 작성자만 삭제 가능합니다.")
modifyModal.hide()
return
}
실행하면
user1이 작성한 댓글의 modify, Remove 두가지가 뜨고
가상의 reply를 넣어서 확인해보면
user_test가 작성한 댓글은 수정 및 삭제가 안된다.
PostMapping modify
BoardController에 있는
@PostMapping("/modify")
위에 아래 코드를 추가
메서드 실행 전, 현재 로그인한 사용자(principal.username)와 파라미터로 전달된
게시물 작성자(boardDTO.writer)가 일치하는지 검사하여 본인만 해당 기능을 사용할 수 있도록 제한하는 보안 설정
@PreAuthorize("principal.username == #boardDTO.writer")
수정 후 실행해 보면
본인이 작성한 게시글 modify가 잘 작동하는것을 확인 할 수 있다
내가 작성하지 않은 게시글을 list에서 선택
내가 작성하지 않은 게시물에는 원래 read로 연결된 후 modify버튼이 보이지 않지만
강제로 url을 read?bno=110이 아니라 modify?bno=111로 입력해서 보면
modify버튼이 보이고 이 버튼을 누르면 에러페이지가 나온다
에러 페이지가 아닌 다른것을 보이게 변경
securoty 패키지 안에 handler 패키지를 추가
그리고 Custom403Handler 클래스를 추가

권한이 없는 사용자가 보호된 자원에 접근했을 때(403 Forbidden)를 처리하는 사용자 정의 핸들러로,
요청 방식에 따라 서로 다른 응답을 보내주는 역할을 합니다.
- 상태 코드 설정: 접근 거부 발생 시 HTTP 상태 코드를 403(Forbidden)으로 지정합니다.
- 요청 타입 판별: 요청 헤더(Content-Type)를 확인하여 해당 요청이 JSON(비동기) 방식인지 일반 페이지 요청인지 구분합니다.
- 일반 페이지 대응: JSON 요청이 아닐 경우(일반 브라우저 접근 등), 로그인 페이지로 리다이렉트시키면서 error=Access_DENIED 파라미터를 전달해 사용자에게 알립니다.
- REST 방식 고려: JSON 요청일 경우 별도의 리다이렉트 없이 403 상태 코드만 반환하여, 프론트엔드(Axios 등)에서
에러를 직접 처리할 수 있게 설계되었습니다.
package com.example.b01.security.handler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
@Log4j2
public class Custom403Handler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException
{
log.info("----------ACCESS DENIED-------------");
response.setStatus((HttpStatus.FORBIDDEN.value()));
//JSON 요청이었는지 확인
String contentType = request.getHeader("Content-Type");
boolean jsonRequest = contentType.startsWith("application/json");
log.info("isJSON: " + jsonRequest);
// 일반 request
if (!jsonRequest) {
response.sendRedirect("/member/login?error=Access_DENIED");
}
}
}
CustomSecurityConfig 에 코드를 추가
권한 없는 사용자가 접근해서 403 에러가 터지면,
내가 만든 Custom403Handler 로직(로그인 페이지 리다이렉트 등)을 실행하라고 명령을 내리는 코드
http.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler());
});
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new Custom403Handler();
}
실행을 하고
위에서 했던것과 같은 방식으로 본인이 작성하지 않은 글에 대한 modify를 강제로 버튼 클릭을 하게 되면
사진과 같이 Custom403Handler에서 작성한
http://localhost:8080/member/login?error=Access_DENIED 주소를 가진 로그인페이지로 넘어가게 된다
remove도 같은 방식으로 처리 한다
BoardController에 @PostMapping("/remove")이 부분에
아래 코드를 추가
@PreAuthorize("principal.username == #boardDTO.writer")
코드를 수정하고 같은 방식으로 remove버튼을 누르면 (http://localhost:8080/board/modify?bno=110)
사진과 같이 Custom403Handler에서 작성한
http://localhost:8080/member/login?error=Access_DENIED 주소를 가진 로그인페이지로 넘어가게 된다
'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바 스프링 부트 17일차 /b01Security 카카오 OAuth2 소셜 로그인 연동 (0) | 2026.04.30 |
|---|---|
| 자바 스프링 부트 16일차 / b01Security JPA 연동 실무 회원 엔티티 설계 및 중복 방지 회원가입 시스템 구축 (0) | 2026.04.29 |
| 자바 스프링 부트 14일차 /b01Security 회원가입(Security) 핵심 설정 및 커스텀 인증 시스템 구축 (0) | 2026.04.27 |
| 자바 스프링 부트 13일차 b01Upload (1) | 2026.04.24 |
| 자바 스프링 부트 13일차 b01Upload (1) | 2026.04.23 |
































