🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [57]일차
🚀57일차에는 Spring Security에서 사용자 별 권한을 부여하는 것과 쿠키에 대한 정보를 로그아웃을 통해서 실습해보았다.
학습 목표 : Spring Security 사용자 권한 부여 및 그에 따른 인가된 URL 페이지 실습 가능
학습 과정 : 회고를 통해 작성
Spring Security 로그인
▶️실습 - 로그인 페이지 리다이렉트 (loginPage())
// 2. 인증없이 접근 가능한 URL 지정 및 (.requestMatchers())
// 로그인 페이지와 로그인 성공 시 이동할 페이지 설정 (.formLogin())
http
.authorizeHttpRequests( auth -> auth
.requestMatchers("/hello","/loginForm", "fail", "/test/*").permitAll()
.anyRequest().authenticated()
// 모든 요청에 대해서 인증을 요구
)
.formLogin(formLogin -> formLogin
.loginPage("/loginForm") // 원하는 로그인 페이지 설정
.defaultSuccessUrl("/success") // 인증에 성공하면 가고싶은 페이지 설정
.failureUrl("/fail") // 인증에 실패하면 가고싶은 페이지 설정
.usernameParameter("userId") // 로그인 폼에서의 Input 상자의 ID부분과 일치해야함
.passwordParameter("password") // 로그인 폼에서의 Input 상자의 PASSWORD부분과 일치해야함
);
return http.build();
- permitAll() : 인증하지 않아도 접속하게 해달라는 것
- loginPage("loginForm")
➡️기본으로 제공해주는 로그인 페이지가 아닌 사용자 정의의 로그인 페이지로 이동하게끔 할 것
로그인 성공 페이지, 로그인 실패 페이지 리다이렉트
- defaultSuccessUrl("/success"), failureUrl("/fail")
➡️기본으로 제공하는 로그인 폼에서 로그인 성공시, 실패시 각각 success, fail페이지로 이동하도록 구현 - fail페이지는 로그인 실패된 사용자도 보여지도록 해야하므로 requestMatchers()에 fail페이지를 추가해야함
▶️실습 - 로그인 성공/실패 시 할 동작 추가
.formLogin(formLogin -> formLogin
.loginProcessingUrl("/login_proc") // 기본 login
.loginPage("/loginForm") // 원하는 로그인 페이지 설정
.defaultSuccessUrl("/success") // 인증에 성공하면 가고싶은 페이지 설정
.failureUrl("/fail") // 인증에 실패하면 가고싶은 페이지 설정
.usernameParameter("userId") // 로그인 폼에서의 Input 상자의 ID부분과 일치해야함
.passwordParameter("password") // 로그인 폼에서의 Input 상자의 PASSWORD부분과 일치해야함
.successHandler((request, response, authentication) -> {
log.info("로그인 성공!" + authentication.getName());
response.sendRedirect("/info"); // 로그인 성공 시 이동할 페이지 설정
})
);
- loginProcessingUrl("/login_proc")
➡️로그인 요청을 처리할 URL을 "/login_proc"으로 지정
➡️사용자가 로그인하면 이 URL로 POST 요청을 보내야 함
- successHandler ( (...) ) : 로그인 성공 시 할 동작 추가
- .authorizeHttpRequests( auth -> auth ... )
➡️HttpSecurity 객체 http는 보안 관련 설정을 정의함
➡️람다 표현식으로 auth 객체를 활용하여 권한 설정을 정의 - .anyRequest().authenticated()
➡️.anyRequest() : 명시적으로 설정하지 않은 모든 요청
➡️.authenticated() : 인증된 사용자만 접근 가능 - .usernameParameter("userId")
➡️로그인 폼에서의 Input 상자의 ID부분과 일치해야함
➡️로그인 폼에서 사용자 ID 필드명을 "userId"로 설정 (기본값은 "username") - response.sendRedirect("/info") : 로그인 성공 후 /info 페이지로 리다이렉트
- 이 후 반환 값은 return http.build();
➡️설정을 마친 HttpSecurity 객체를 SecurityFilterChain으로 반환
❓.formLogin() 메소드
- 특정 URL에 대해서는 인증 없이 접근 가능하도록 설정
- 그 외의 모든 요청은 인증이 필요하도록 제한
Spring Security를 통한 인증 과정 흐름
-----------------------------------
사용자가 요청
-----------------------------------
▼
-----------------------------------
Spring Security 필터 체인
-----------------------------------
▼
-----------------------------------
1) 요청 URL 확인
-----------------------------------
▼
-----------------------------------
인증 없이 접근 가능한 URL 확인
("/hello", "/loginForm", "/fail",
"/test/*" 요청이면 그대로 통과
-----------------------------------
▼
-----------------------------------
2) 인증이 필요한 경우
- 로그인 페이지로 이동
- 인증 성공 시 지정된 페이지로 이동
-----------------------------------
❓defaultSuccessUrl() vs successHandler() 차이점
- .defaultSuccessUrl("/success") : 로그인 성공 후 특정 URL로 리디렉트
➡️단순 리디렉트만 수행
로그인 성공 시 무조건 /success 페이지로 이동
예외적으로, 사용자가 로그인 페이지 전에 특정 보호된 페이지에 접근하려 했다면, 로그인 후 해당 페이지로 자동 이동
ex. /보호된페이지 → /loginForm(로그인) → /보호된페이지(자동 이동)
- .successHandler((request, response, authentication) -> { ... }) : 로그인 성공 후 커스텀 로직 실행 가능
➡️리디렉트뿐만 아니라 추가적인 작업 수행 가능 (ex: 로깅, 세션 설정, 권한에 따라 다른 URL 이동 등)
로그인 성공 후 사용자가 정의한 동작(=커스텀 로직)을 실행할 수 있음
➡️커스텀 로직의 예시
- 로그인 성공 로그 남기기
- 특정 세션 값 설정
- 사용자 권한(Role)에 따라 다른 페이지로 이동
- 비밀번호 변경이 필요한 사용자 감지 후 강제 이동
만약 둘 다 설정하면 Handler()가 우선 적용된다.
로그아웃 로직 추가
// 3. 로그아웃 기능 추가
http
.logout(logout-> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/hello") // 1) 단순히 로그아웃 시 이동할 페이지 설정
// 2) 로그아웃 시 수행할 일 지정 가능
.addLogoutHandler((request, response, authentication) -> {
log.info("로그아웃 세션, 쿠키 삭제!");
HttpSession session = request.getSession();
if(session != null){
session.invalidate(); // 세션 삭제
}
})
.deleteCookies("JSESSIONID") // 로그아웃 시에 원하는 쿠키를 삭제할 수 있음
);
- 로그인 했을 경우 쿠키의 정보 확인 가능
- NAME으로 JSESSIONID이므로, deleteCookies()로 로그아웃 시 쿠키가 삭제되도록 구현 가능
logback.xml 설정
<logger name="org.springframework.security" level="debug" additivity="false">
<appender-ref ref="FILE" />
</logger>
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
- ref=”STDOUT” : 콘솔에 로그를 출력하겠다는 것
- ref=”FILE” : 파일에 로그를 저장한다는 것
- 로그 레벨이 debug이면 FILE에 저장하고 로그 레벨이 trace이면 콘솔에 출력하겠다는 설정
로그 파일 관리 설정 추가
<appender name="DAILY_ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>c:/logs/security_debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>c:/logs/security-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
- 하나의 파일에 로그가 계속쌓이는 것을 방지하기 위해 파일의 용량에 따라 다른 로그 파일을 생성하도록 함
- <fileNamePattern>c:/logs/security-%d{yyyy-MM-dd}.log</fileNamePattern>
➡️파일명생성에 날짜를 포함한 .log 파일을 만들도록 하는 설정 추가
자동로그인 기능 rememberMe()
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception{
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.rememberMe(rememberMe -> rememberMe
.userDetailsService(userDetailsService)
);
return http.build();
}
}
- rememberMe() 메소드가 체크박스를 수행할 수 있는 자동로그인 기능을 구현 가능
➡️기억하기(자동 로그인) 기능 활성화 (.rememberMe()) - UserDetailsService userDetailsService
➡️rememberMe() 기능을 위해 사용자 정보를 로드하는 서비스를 주입받음 - .formLogin(Customizer.withDefaults())
➡️기본적인 "폼 로그인" 기능 활성화
➡️Customizer.withDefaults() : Spring Security의 기본 로그인 페이지 사용
즉 rememberMe() 기능을 사용 후 사용자가 "로그인 유지"를 체크하면 쿠키를 통해 자동 로그인 가능
토큰 기한 설정
.rememberMe(rememberMe -> rememberMe
.tokenValiditySeconds(3600) // 하루 정도 토큰을 기억할 수 있게 설정
.userDetailsService(userDetailsService)
);
- .tokenValiditySeconds(3600)
➡️세션이 사라져도 기억할 수 있는 토큰을 어느 정도 가지고 있을지를 설정 가능
3600: 하루 정도 토큰을 기억할 수 있게 설정
rememberMe가 있으면 세션ID가 없어져도 자동로그인은 그대로 기능하는 것을 확인할 수 있다.
사용자 권한 부여
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception{
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/hello").permitAll()
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/admin/abc").hasAnyRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
- .requestMatchers("/hello").permitAll() .requestMatchers("/user/**").hasAnyRole("User", "ADMIN")
- hasRole() : 역할 하나만 부여
- hasAnyRole() : 역할 여러개 부여 (ex. hasAnyRole(”USER”, “ADMIN”))
- .requestMatchers("/user/**")
➡️/user/로 시작하는 모든 URL을 포함
ex. /user/profile | /user/settings | /user/dashboard | /user/orders/123
/user/ 하위 경로는 모두 해당
비밀번호 인코딩
// 비밀번호 인코딩
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- Security를 쓸때는 비밀번호로 쓸 값을 반드시 인코딩해서 보내야한다.
(=Security는 암호화되지 않은 비밀번호를 받아들이지 않음)
➡️암호화되어있는 것만 취급
다양한 사용자 추가
- 사용자 정보를 저장하고 인증하는 역할
- 메모리 방식으로 유저정보를 관리하는 설정을 담음
// 비밀번호 인코딩
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- PasswordEncoder passwordEncoder
➡️비밀번호를 암호화하기 위한 인코더를 주입받음
➡️BCryptPasswordEncoder와 같은 암호화 알고리즘을 사용하여 비밀번호를 저장
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder){
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("1234")) // 원하는 패스워드를 넣음
.roles("USER")
.build();
UserDetails user2 = User.withUsername("Gordon")
.password(passwordEncoder.encode("1234")) // 원하는 패스워드를 넣음
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, user2);
}
- UserDetails : Spring Security에서 사용자 정보를 담는 객체
- .password() : 사용자의 비밀번호를 설정
passwordEncoder.encode("1234")
➡️"1234"라는 문자열을 암호화(해싱)하여 저장
➡️암호화된 비밀번호만 저장하고, 원본 비밀번호를 직접 저장하지 않음 - .roles("USER") : 사용자의 역할(Role)을 설정
➡️내부적으로 ROLE_USER로 변환됨 (roles()를 사용하면 자동으로 ROLE_이 붙음)
➡️사용자는 ROLE_USER 권한을 가지게 됨 - InMemoryUserDetailsManager
➡️메모리에 사용자 정보를 저장하는 Spring Security의 기본 구현체
이 설정을 통해 Spring Security가 메모리에 사용자 정보를 저장하고 관리
메모리로 관리하기때문에 애플리케이션을 재시작 시 모든 유저 정보가 초기화
▶️실습 - 로그인 과정
1) http://localhost:8080/login 접속
2) 유저 로그인 : user(1234), admin(admin1234), superuser(super1234)
3) 특정 URL 각각 접속하여 접근 가능 여부 확인
▶️실습 - 로그인된 사용자 가져오기
@GetMapping("/info")
public String info() {
String message = null;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
message = "로그인된 사용자가 없습니다.";
}
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
message = "현재 로그인 한 사용자 : " + userDetails.getUsername();
} else {
message = "현재 로그인 한 사용자 : " + principal.toString();
}
return message;
}
- SecurityContextHolder
➡️현재 로그인한 사용자의 정보를 가져와서 로그인 여부 및 사용자명을 확인하는 역할
(=현재 실행 중인 스레드의 보안 컨텍스트를 관리하는 클래스) - 사용자가 로그인하지 않았다면 "로그인된 사용자가 없습니다." 메시지 반환
- 사용자가 로그인했다면 "현재 로그인한 사용자: [유저네임]" 반환
- SecurityContextHolder.getContext().getAuthentication()
➡️Spring Security에서 현재 로그인한 사용자의 인증(Authentication) 정보를 가져옴
getContext().getAuthentication() : 현재 로그인한 사용자의 인증 정보를 가져올 수 있음 - authentication == null : 로그인하지 않은 경우 authentication 객체가 null일 수 있음
- !authentication.isAuthenticated() : false이면 로그인되지 않은 상태
- authentication.getPrincipal() : 현재 로그인한 사용자의 주체(Principal) 정보를 가져옴
➡️principal은 로그인한 사용자의 UserDetails 객체 또는 문자열일 수 있음
➡️현재 로그인한 사용자의 정보를 가져와 principal 변수에 저장 - if (principal instanceof UserDetails) { ... }
➡️principal이 UserDetails 타입인지 확인
❓UserDetails : Spring Security에서 사용자의 정보를 담는 인터페이스 (username, password, roles 등의 정보 포함)
principal이 UserDetails 타입이면, 로그인한 사용자의 정보를 가져올 수 있게됨 - UserDetails userDetails = (UserDetails) principal;
➡️UserDetails 객체로 캐스팅 후, getUsername()을 호출하여 사용자 이름을 가져옴
Authentication (인증 정보 객체)
- 현재 로그인한 사용자의 인증 정보를 저장하는 객체입니다.
- 로그인하면 Authentication 객체가 생성되어 SecurityContext에 저장됨
- 주요 메소드
- getPrincipal() : 로그인한 사용자의 객체 (UserDetails 또는 문자열)
- getAuthorities() : 사용자의 역할(Role) 목록
- isAuthenticated() : 사용자가 인증되었는지 여부 (true or false)
SecurityContextHolder (보안 컨텍스트 저장소)
- 현재 실행 중인 스레드에서 SecurityContext를 관리하는 클래스
- SecurityContext는 로그인한 사용자의 Authentication 객체를 보관
- 주요 메소드
- getContext() : 현재 스레드의 보안 컨텍스트(SecurityContext) 가져오기
- setContext(SecurityContext context) : 보안 컨텍스트 설정
AuthenticationManager (인증 매니저)
- AuthenticationManager는 사용자의 인증을 담당하는 인터페이스
- 로그인 요청이 들어오면 authenticate()를 호출하여 인증 수행
- authenticate(Authentication authentication) : 인증 처리 후 Authentication 객체 반환
▶️실습 - 사용자 정보 페이지 생성
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
- 뷰에 출력하기 위한 타임리프+시큐리티 추가
사용자 정보페이지 HTML
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>사용자 정보 페이지</title>
</head>
<body>
<h2>로그인한 사용자 정보</h2>
<div sec:authorize="isAuthenticated()">
<!--로그인한 사용자는 isAuthenticated()의 결과가 true이므로 이 부분이 실행됨-->
<p>사용자 명 : <span sec:authentication="name"></span></p>
<p>권한 : <span sec:authentication="principal.authorities"></span></p>
</div>
<div sec:authorize="!isAuthenticated()">
<!-- 로그인하지 않은 사용자를 보여주는 부분-->
<p>로그인이 필요합니다.</p>
<a th:href="@{/login}">로그인 화면으로 이동</a>
</div>
</body>
</html>
- sec : security가 가지고 있는 객체 중에 가져오는 문법
이를 통해 로그인한 사용자, 사용자 명, 로그인 필요 시 하이퍼링크 출력까지 확인할 수 있다.
🚀회고 결과 :
이번 회고에서는 로그아웃, 사용자 추가 및 로그아웃 등에 대한 실습을 해볼 수 있었고
관련한 클래스들과 메소드들의 개념에 대해서 더 익힐 수 있었다.
- 로그인, 로그아웃의 과정 파악
- 자동 로그인 기능
느낀 점 :
학습하면서 부족했던 메소드와 클래스들에 대한 개념을 회고를 통해 익힐 수 있었다.
Thymeleaf+Security 의존성에서는 sec (=security)와 같은 문법을 통해 가져오는 것 또한 배울 수 있던 회고였다.
향후 계획 :
- 각 메소드들에 대한 활용법
- 각각의 사용자에 권한을 부여 후 다양한 페이지에서 실습 필요
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_60일차_"Jwt Authentication" (0) | 2025.03.06 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_58일차_"Spring Security와 DB연결" (0) | 2025.03.04 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_56일차_"ThreadLocal, Spring Security" (0) | 2025.02.27 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_55일차_"DTO, Security" (0) | 2025.02.26 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_54일차_"@RestController" (0) | 2025.02.25 |