Spring Boot Security 학습 정리 (1/2) - 13단계 구현

박지명·2026년 5월 11일

스프링부트

목록 보기
8/10

1. 기본 개념

Spring Security: 자바 애플리케이션의 인증(Authentication)과 권한(Authorization)을 관리하는 프레임워크

개념설명
인증사용자가 누구인지 확인 (로그인)
권한인증된 사용자가 뭘 할 수 있는지 (역할)
세션서버가 사용자 정보를 메모리에 저장
CSRF다른 사이트에서 내 계정으로 요청하는 공격

2. 인증 방식 3가지

방식사용자 저장회원가입실무
기본 설정메모리불가X (테스트만)
커스텀 로그인메모리불가X (학습용)
DB 기반DB가능O (실무)

3. 구현 단계

1단계: 설정 준비

# application.yml
spring:
  datasource:
    driver-class-name: oracle.jdbc.OracleDriver
    url: jdbc:oracle:thin:@localhost:1521/XEPDB1
    username: springboot
    password: java1234
  jpa:
    database: oracle
    hibernate:
      ddl-auto: none

2~5단계: 기본 파일 생성

  • MainController → 페이지 라우팅
  • index.html, member.html, admin.html → 페이지
  • inc/header.html → 공통 헤더

6단계: SecurityConfig 생성

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        // URI 권한 설정
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/login", "/join", "/joinok").permitAll()
            .requestMatchers("/member").hasAnyRole("MEMBER", "ADMIN")
            .requestMatchers("/admin").hasRole("ADMIN")
            .anyRequest().authenticated()
        );
        
        // 커스텀 로그인
        http.formLogin(auth -> auth
            .loginPage("/login")
            .loginProcessingUrl("/loginok")
        );
        
        // 예외 처리
        http.exceptionHandling(auth -> auth
            .authenticationEntryPoint((req, res, e) -> res.sendRedirect("/login"))  // 401
            .accessDeniedHandler((req, res, e) -> res.sendRedirect("/denied"))      // 403
        );
        
        return http.build();
    }
    
    @Bean
    BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
}

4. 권한 설정

메서드설명
permitAll()모두 접근 가능
authenticated()인증된 사용자만
hasRole("ADMIN")ROLE_ADMIN만
hasAnyRole("MEMBER","ADMIN")둘 중 하나
denyAll()접근 불가
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()

5. 커스텀 로그인 (9단계)

AuthController.java:

@Controller
public class AuthController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }
    
    @GetMapping("/denied")
    public String denied() {
        return "denied";
    }
}

login.html:

<form method="POST" action="/loginok">
    <input type="text" name="username" required>
    <input type="password" name="password" required>
    <button>로그인</button>
    <!-- CSRF 토큰 필수! -->
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>

6. 회원 가입 (10단계)

script.sql:

create table member(
    username varchar2(50) PRIMARY KEY,
    password varchar2(100) not null,
    age number(3),
    email varchar2(50),
    role varchar2(50)  -- ROLE_MEMBER, ROLE_ADMIN
);

Member Entity:

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {
    @Id
    private String username;
    private String password;
    private Integer age;
    private String email;
    private String role;
}

MemberDto:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberDto {
    private String username;
    private String password;
    private Integer age;
    private String email;
    private String role;
}

MemberRepository:

public interface MemberRepository extends JpaRepository<Member, String> {
}

MemberService - ⭐ 비밀번호 암호화:

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository repo;
    private final BCryptPasswordEncoder encoder;

    public void join(MemberDto dto) {
        Member member = Member.builder()
            .username(dto.getUsername())
            .password(encoder.encode(dto.getPassword()))  // ⭐ 암호화 필수!
            .role(dto.getRole())
            .age(dto.getAge())
            .email(dto.getEmail())
            .build();
        repo.save(member);
    }
}

MemberController:

@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberService service;
    
    @GetMapping("/join")
    public String join() {
        return "join";
    }
    
    @PostMapping("/joinok")
    public String joinok(MemberDto dto) {
        service.join(dto);
        return "redirect:/login";
    }
    
    @GetMapping("/member")
    public String member() {
        return "member";
    }
}

7. PasswordEncoder (11단계)

비밀번호 안전 암호화

// 암호화
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("1234");  // $2a$10$... (매번 다름)

// 검증 (로그인 시 자동)
boolean match = encoder.matches("1234", encoded);  // true

특징:

  • 일방향 암호화 (복호화 불가)
  • 같은 비밀번호도 매번 다른 값
  • BCrypt 알고리즘 (업계 표준)

8. DB 기반 로그인 (12단계)

CustomUserDetails - 인증 객체:

@Getter
public class CustomUserDetails implements UserDetails {
    private Member member;
    
    public CustomUserDetails(Member member) {
        this.member = member;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(() -> member.getRole());
        return authorities;
    }
    
    @Override
    public String getPassword() {
        return member.getPassword();
    }
    
    @Override
    public String getUsername() {
        return member.getUsername();
    }
}

CustomUserDetailsService - ⭐ 핵심:

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository repo;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> member = repo.findById(username);
        if(member.isPresent()) {
            return new CustomUserDetails(member.get());  // ⭐ 이게 Spring Security가 찾음
        } else {
            throw new UsernameNotFoundException(username);
        }
    }
}

9. 예외 처리 (13단계)

HTTP Code상황해결
401익명 사용자 인증 필요/login으로 리다이렉트
403권한 없음/denied로 리다이렉트
http.exceptionHandling(auth -> auth
    .authenticationEntryPoint((req, res, e) -> res.sendRedirect("/login"))
    .accessDeniedHandler((req, res, e) -> res.sendRedirect("/denied"))
);

10. 로그인 흐름

사용자 /login 접근
    ↓
login.html 표시 (username, password 입력)
    ↓
POST /loginok (username, password, CSRF 토큰)
    ↓
Spring Security 필터 가로채기
    ↓
CustomUserDetailsService.loadUserByUsername(username) 호출
    ↓
DB에서 Member 조회
    ↓
CustomUserDetails 객체 생성
    ↓
BCryptPasswordEncoder.matches() 비밀번호 검증
    ↓
[일치] → 세션 저장 → 권한에 따라 페이지 이동
[불일치] → /login으로 리다이렉트 (실패 메시지)

11. 자주 하는 실수

❌ 실수 1: PasswordEncoder 미등록

// 잘못된 예
.password(dto.getPassword())  // 평문 저장!

// 올바른 예
.password(encoder.encode(dto.getPassword()))  // ✅ 암호화

❌ 실수 2: UserDetailsService 미구현

// 잘못된 예
// CustomUserDetailsService 없으면 DB를 모르므로 로그인 실패

// 올바른 예
@Service
public class CustomUserDetailsService implements UserDetailsService {
    // ✅ 반드시 필요!
}

❌ 실수 3: HTML에 CSRF 토큰 누락

<!-- 잘못된 예 -->
<form method="POST" action="/loginok">
    <!-- CSRF 공격에 취약 -->
</form>

<!-- 올바른 예 -->
<form method="POST" action="/loginok">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>

❌ 실수 4: 로그인 처리 URL 누락

// 잘못된 예
http.formLogin(auth -> auth
    .loginPage("/login")
    // loginProcessingUrl 없으면 기본값 /login 사용 (혼동!)
);

// 올바른 예
http.formLogin(auth -> auth
    .loginPage("/login")              // GET (페이지 표시)
    .loginProcessingUrl("/loginok")   // POST (로그인 처리)
);

12. 프로젝트 구조

com.test.java/
├── config/
│   └── SecurityConfig.java .... 보안 설정
├── controller/
│   ├── MainController.java
│   ├── AuthController.java ..... /login, /denied
│   └── MemberController.java ... /join, /member
├── entity/
│   └── Member.java ............ DB 테이블
├── dto/
│   ├── MemberDto.java
│   └── CustomUserDetails.java .. 인증 객체
├── repository/
│   └── MemberRepository.java
└── service/
    ├── MemberService.java ...... 회원 가입 (비밀번호 암호화)
    └── CustomUserDetailsService.java  로그인 서비스

13. 체크리스트

  • SecurityConfig 생성 (@Configuration, @EnableWebSecurity)
  • BCryptPasswordEncoder @Bean 등록
  • authorizeHttpRequests 설정
  • formLogin 설정
  • exceptionHandling 설정
  • Member Entity 생성
  • MemberRepository 생성
  • MemberService (비밀번호 암호화)
  • MemberController (/join, /joinok)
  • CustomUserDetails 구현
  • CustomUserDetailsService 구현
  • AuthController (/login, /denied)
  • login.html (CSRF 토큰 포함)
  • DB 테이블 생성
  • 테스트 데이터 삽입

0개의 댓글