[Spring Boot] Spring Security를 이용한 인증/인가

2025. 11. 8. 11:38·프로젝트

 

 

 

Spring Security란?

Spring Security는 Spring 기반 애플리케이션에 인증(Authentication)과 인가(Authorization) 기능을 제공하는 강력한 보안 프레임워크입니다.

 

핵심 특징

  • 인증(Authentication)과 인가(Authorization) 처리
  • 세션/토큰 기반 인증 지원
  • CSRF, XSS, 세션 고정 공격 등 방어
  • 필터 기반의 보안 체인(Security Filter Chain)
  • 커스터마이징 가능한 설정 구조 (DSL 기반 설정)

 

Spring Security 작동원리

Spring Security는 Filter 기반의 체인 구조(Security Filter Chain) 로 동작한다.
사용자가 요청을 보낼 때, DispatcherServlet에 도달하기 전 보안 필터들이 요청을 검사하고 필요한 경우 인증 절차를 수행한다.

 

 

1. Http 요청 가로채서 로그인 요청을 가로채서 인증절차가 시작되는 스프링 시큐리티 필터체인 인증관문시작 

2. UsernamePasswordAuthenticationToken:인증 전 사용자의 정보를 담는 인증 시도용 토큰 객체를 생성한다.

3. AuthenticationManager: AuthenticationProvider중 하나를 선택해 인증을 위임한다.

4. AuthenticationProvider: 사용자 인증을 수행하는 핵심 처리자 인증 성공시 Authentication 객체 반환

5. UserDetailsService: DB에서 사용자 정보를 조회하는 인터페이스로 loadUserByUsername(username) 메서드를 구현해서 DB나 외부 API로부터 사용자를 조회하고 UserDetails 객체로 반환한다.

6. UserDetails: 인증 대상 사용자 정보를 담는 객체로 UserDetailsService에서 조회 후 이 객체로 감싸서 반환한다.

7. SecurityContextHolder: 인증이 끝난 다음에 생성된 Authentication객체를 저장하는 저장소

 

[전체요청흐름]

1️.클라이언트 요청 (POST /login)
사용자 아이디와 비밀번호 전송
⠀⠀⠀⠀⠀⠀⠀⠀⬇
2️.UsernamePasswordAuthenticationFilter
로그인 요청 가로채기
→ UsernamePasswordAuthenticationToken 생성
→ AuthenticationManager.authenticate() 호출
⠀⠀⠀⠀⠀⠀⠀⠀⬇
3️.AuthenticationManager (ProviderManager)
처리 가능한 AuthenticationProvider 선택
→ provider.authenticate() 실행
⠀⠀⠀⠀⠀⠀⠀⠀⬇
4️.AuthenticationProvider (DaoAuthenticationProvider)
UserDetailsService 호출해 사용자 조회
→ PasswordEncoder로 비밀번호 검증
→ 인증 성공 시 Authentication 생성
⠀⠀⠀⠀⠀⠀⠀⠀⬇
5️.UserDetailsService
DB에서 사용자(username) 조회
→ UserDetails 객체 반환
⠀⠀⠀⠀⠀⠀⠀⠀⬇
6️.UserDetails
사용자 정보(username, password, 권한 등) 보유
→ AuthenticationProvider가 이 정보를 사용해 인증 완료
⠀⠀⠀⠀⠀⠀⠀⠀⬇
7️.SecurityContextHolder
Authentication을 SecurityContext에 저장
→ SecurityContextPersistenceFilter가 세션에 보관
⠀⠀⠀⠀⠀⠀⠀⠀⬇
✅ 이후 요청
세션에서 SecurityContext 복원
→ 로그인 상태 유지 후 Controller 접근 가능

 

 

Spring Security 설정방법

Spring Security를 적용하기 위해서는 요청에 대한 처리방식을 표현하는 SecurityConfig를 작성하고 회원검증 로직을 커스텀하기위해 UserDetailsService와 UserDetails를 구체화하는 과정이 필요하다 간단하게 말하면 다음과 같다

  • 요청처리방식설정
  • 회원검증구체화

[공통 Gradle 설정]

implementation 'org.springframework.boot:spring-boot-starter-security'

 

[PasswordEncoder 등록]

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

비밀번호의 암호화를 위해서 SpringConfig에 빈으로 등록한다

 

[UserDetailsService 구체화]

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 유저입니다."));
        
        return new CustomUserDetails(member);
    }
}
  • UserDetailsService 인터페이스를 implements해서 loadUserByUsername메서드를 서비스에 맞게 구체화

 

[UserDetails 구체화]

@Getter
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final Member member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(member.getRole().name()));
    }

    @Override
    public String getPassword() {
        return member.getPassword();
    }

    @Override
    public String getUsername() {
        return member.getUsername();
    }
}
  • UserDetails인터페이스를 implements해서 loadUserByUsername메서드를 서비스에 맞게 구체화

 

 

Session

 

세션기반으로 구현할 경우 폼로그인을 사용해 로그인 컨트롤러를 따로 구현할 필요없이 UserDetailsService와 UserDetails를 각자의 서비스에 맞게 구현하고 SecurityConfig설정을 작성하면된다.

 

[Config 설정]

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // ✅ CSRF는 브라우저 기반 앱에서는 켜두는 게 안전
            .csrf(csrf -> csrf.disable()) // REST API 형태면 disable 가능
            // ✅ 요청별 접근 제어
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // ✅ 폼 로그인 설정
            .formLogin(form -> form
                .loginPage("/login")               // 커스텀 로그인 페이지
                .loginProcessingUrl("/login")      // 로그인 요청 처리 URL (POST)
                .defaultSuccessUrl("/home", true)  // 로그인 성공 시 리다이렉트
                .failureUrl("/login?error=true")   // 실패 시 이동
                .permitAll()
            )
            // ✅ 로그아웃 설정
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true) // 세션 무효화
                .deleteCookies("JSESSIONID") // 쿠키 삭제
                .permitAll()
            )
            // ✅ 세션 관리 설정
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // 세션 항상 생성
                .invalidSessionUrl("/login?expired=true")            // 세션 만료 시 이동
                .maximumSessions(1)                                 // 중복 로그인 제한
                .maxSessionsPreventsLogin(false)
            );
        return http.build();
    }
}

 

 

JWT

 

JWT기반으로 Spring Security를 사용할 때는 JWT 토큰의 로직 클래스, 토큰을 검증하는 필터, UserDetailsService, UserDetailsf를 구현하여 적용하면 된다. jwt 토큰은 stateless하므로 설정파일에서 여러부분을 비활성화해야하므로 주의하자!!

 

[Gradle 설정]

    implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

jwt의 간편한 로직생성을 위해 jjwt 라이브러리 의존성을 추가한다

 

[Config 설정]

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable) //세션이나 쿠키를 사용하지 않으므로 비활성
            .formLogin(AbstractHttpConfigurer::disable) // 컨트롤러에서 처리하므로 비활성
            .httpBasic(AbstractHttpConfigurer::disable) // Authorization헤더 별도로 사용하므로 비활성
            // 세션은 사용하지 않으므로 비활성
            .sessionManagement(session
                -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            //경로별 접근제어
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()// 인증&인가 요청은 허용
                .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 허용
                .anyRequest().authenticated() //그외 인증필요
            )
            //토큰검증필터추가
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
                UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

 

[JwtTokenProvider]

@Component
public class JwtTokenProvider {
    
    private static final String SECRET_KEY = "2256913ae74493677cb77fca6bbd7294e5115331341a7374037135ecd441afe3743f0210a50e44b9095fade92a16f5c433549ae4ed5aba3aaee3916d09f953f0";
    private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
    private static final long VALIDITY = 1000 * 60 * 60 * 24;

    public String createAccessToken(String username) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + VALIDITY);

        return Jwts.builder()
            .subject(username)
            .issuedAt(now)
            .expiration(validity)
            .signWith(KEY, Jwts.SIG.HS512)
            .compact();
    }


    public boolean validateAccessToken(String token) {
        try {
            Jwts.parser()
                .verifyWith(KEY)
                .build()
                .parseSignedClaims(token);
            return true;
        } catch (RuntimeException e) {
            throw new CUnAuthorizedException("유효하지 않은 토큰입니다.");
        }
    }
    
    public String getUsername(String token){
        return Jwts.parser()
            .verifyWith(KEY)
            .build()
            .parseSignedClaims(token)
            .getPayload()
            .getSubject();
    }
    
}

 

 

[JwtAuthenticationFilter]

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider tokenProvider;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {

        String jwt = resolveToken(request);

        if (StringUtils.hasText(jwt) && tokenProvider.validateAccessToken(jwt)) {
            String username = tokenProvider.getUsername(jwt);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            //검증
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String prefix = "Bearer ";
        String authHeader = "Authorization";
        String bearerToken = request.getHeader(authHeader);
        int start = prefix.length();

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(prefix)) {
            return bearerToken.substring(start);
        }
        return null;
    }
}

 

 

로그인 관련 컨트롤러나 서비스의 경우 각자의 서비스에 맞게 작성해주시면 됩니다.

 

 

 

 

'프로젝트' 카테고리의 다른 글

[Spring Boot] 카카오 소셜로그인 구축기2  (0) 2025.11.09
[Spring Boot] 카카오 소셜 로그인 구축기1  (0) 2025.11.08
[Spring Boot] QueryDSL 적용기  (0) 2025.11.07
'프로젝트' 카테고리의 다른 글
  • [Spring Boot] 카카오 소셜로그인 구축기2
  • [Spring Boot] 카카오 소셜 로그인 구축기1
  • [Spring Boot] QueryDSL 적용기
Noaahhh
Noaahhh
  • Noaahhh
    노아
    Noaahhh
  • 전체
    오늘
    어제
    • 분류 전체보기 (118)
      • 프로젝트 (4)
      • 알고리즘 (113)
        • SQL (108)
        • CP (5)
      • 자격증 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    cp초보
    springboot
    PCSQL
    pasql
    인증/인가
    atcoder
    contest395
    Spring
    PS
    cp
    querydsl
    카카오로그인
    spingboot
    abc421
    SQL문제
    JWT
    SQL
    경쟁적프로그래밍
    코딩테스트
    소셜로그인
    contest397
    집계함수
    OAuth2.0
    ABC
    JPQL
    프로그래밍대회
    어린 동물 찾기
    아픈 동물 찾기
    프로그래머스
    atcoder beginner contest
  • hELLO· Designed By정상우.v4.10.5
Noaahhh
[Spring Boot] Spring Security를 이용한 인증/인가
상단으로

티스토리툴바