1. SecurityFilterChain
HTTP 요청에 대한 보안 필터 설정
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
http.formLogin(auth -> auth
.loginPage("/login")
.loginProcessingUrl("/loginok")
);
// 3. 예외 처리
http.exceptionHandling(auth -> auth
.authenticationEntryPoint((req, res, e) -> res.sendRedirect("/login"))
.accessDeniedHandler((req, res, e) -> res.sendRedirect("/denied"))
);
// 4. 로그아웃
http.logout(auth -> auth
.logoutUrl("/logout")
.logoutSuccessUrl("/")
);
return http.build();
}
2. PasswordEncoder
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("1234");
boolean match = encoder.matches("1234", encoded);
| 특징 | 설명 |
|---|
| 일방향 | 복호화 불가 |
| 동적 | 같은 입력값도 매번 다른 결과 |
| 알고리즘 | BCrypt (업계 표준) |
| 용도 | 비밀번호만 암호화 |
3. UserDetails & UserDetailsService
UserDetails: Spring Security가 인식할 수 있는 사용자 정보
public interface UserDetails {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
필수 구현 (최소):
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
authorities.add(() -> member.getRole());
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
4. 권한 설정 상세
| 표기 | 의미 | 예시 |
|---|
ROLE_ADMIN | DB에 저장되는 권한 | "ROLE_ADMIN" |
hasRole("ADMIN") | SecurityConfig에서 사용 | ROLE_ADMIN 찾음 |
private String role;
.hasRole("ADMIN")
.hasRole("MEMBER")
5. 로그인 상세 흐름
1️⃣ GET /login
↓ AuthController.login()
↓ login.html (username, password 폼)
2️⃣ POST /loginok (username, password, _csrf)
↓ Spring Security 필터
3️⃣ CustomUserDetailsService.loadUserByUsername(username)
↓ MemberRepository.findById(username)
↓ DB 조회
4️⃣ CustomUserDetails 객체 생성
↓ getAuthorities() → member.getRole()
↓ getPassword() → member.getPassword()
5️⃣ BCryptPasswordEncoder.matches(입력password, DB_password)
↓ [일치] 세션 저장
↓ [불일치] 로그인 실패
6️⃣ 권한 확인
↓ /admin → ROLE_ADMIN만 접근
↓ /member → ROLE_MEMBER 또는 ROLE_ADMIN
↓ 권한 없음 → 403 (AccessDenied)
6. 실무 활용
현재 로그인 사용자 정보
@GetMapping("/mypage")
public String mypage(Principal principal) {
String username = principal.getName();
}
@GetMapping("/mypage")
public String mypage(@AuthenticationPrincipal CustomUserDetails user) {
Member member = user.getMember();
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
HTML에서 권한 확인
<div th:if="${#authentication.name != 'anonymousUser'}">
<p th:text="${#authentication.name}"></p>
<a href="/logout">로그아웃</a>
</div>
<div sec:authorize="hasRole('ADMIN')">
<a href="/admin">관리 페이지</a>
</div>
<div sec:authorize="hasRole('MEMBER')">
<a href="/member">회원 페이지</a>
</div>
회원가입 검증
@PostMapping("/joinok")
public String joinok(MemberDto dto, Model model) {
if(repo.existsById(dto.getUsername())) {
model.addAttribute("error", "이미 존재하는 아이디");
return "join";
}
if(!dto.getPassword().equals(dto.getPasswordConfirm())) {
model.addAttribute("error", "비밀번호 불일치");
return "join";
}
service.join(dto);
return "redirect:/login";
}
로그아웃
http.logout(auth -> auth
.logoutUrl("/logout")
.logoutSuccessUrl("/")
);
<form method="POST" action="/logout">
<button>로그아웃</button>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
7. 3가지 쿼리 방식과의 비교
| 기술 | 방식 | 복잡도 | 실무 |
|---|
| Query Method | 메서드명 기반 | 낮음 | 단순 조회 |
| JPQL | SQL 기반 | 중간 | 복잡한 쿼리 |
| Query DSL | 빌더 기반 | 높음 | 동적 조건 |
Spring Security도 마찬가지:
- 기본 설정 = Query Method (간단하지만 제한적)
- 커스텀 로그인 = JPQL (유연하지만 수동 작업)
- DB 기반 = Query DSL (복잡하지만 강력)
8. HTTP 상태 코드
| 코드 | 상황 | 원인 |
|---|
| 200 | OK | 요청 성공 |
| 401 | Unauthorized | 인증 필요 (로그인 X) |
| 403 | Forbidden | 권한 없음 (로그인 O) |
| 404 | Not Found | 페이지 없음 |
| 500 | Internal Error | 서버 에러 |
.authenticationEntryPoint((req, res, e) -> res.sendRedirect("/login"))
.accessDeniedHandler((req, res, e) -> res.sendRedirect("/denied"))
9. CSRF 이해
CSRF (Cross-Site Request Forgery): 다른 사이트에서 내 계정으로 요청
<form method="POST" action="/loginok">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
Spring Security는 CSRF 토큰으로 자동 방어
10. 주요 어노테이션
| 어노테이션 | 위치 | 역할 |
|---|
@Configuration | 클래스 | Spring 설정 |
@EnableWebSecurity | 클래스 | Security 활성화 |
@Bean | 메서드 | Spring Bean 등록 |
@Service | 클래스 | 비즈니스 로직 |
@Repository | 클래스 | DB 접근 |
@Entity | 클래스 | JPA 관리 |
@Id | 필드 | Primary Key |
@AuthenticationPrincipal | 파라미터 | 현재 사용자 주입 |
11. 용어 정리
| 용어 | 의미 |
|---|
| 인증 (Auth) | 사용자 확인 (로그인) |
| 권한 (Auth) | 사용자 권한 (역할) |
| 세션 | 서버의 메모리 저장소 |
| 쿠키 | 클라이언트의 파일 저장소 |
| 토큰 | 인증 증명 (JWT) |
| Role | 역할 (ROLE_ADMIN 등) |
| Principal | 인증된 사용자 |
| GrantedAuthority | 권한 정보 |
12. 비교표: 기본설정 vs 커스텀 vs DB
| 항목 | 기본설정 | 커스텀 | DB 기반 |
|---|
| 사용자 저장 | 메모리 | 메모리 | DB |
| 사용자 설정 | 자동 | 수동 | 동적 |
| 회원가입 | X | X | O |
| 비밀번호 암호화 | X | X | O |
| 권한 관리 | 단순 | 단순 | 복잡 |
| 테스트/학습 | O | O | X |
| 실무 | X | X | O |
기본 user / 임시 비밀번호
고정 username / password (수동)
Member 테이블 / BCrypt / CustomUserDetailsService