개발 이론/Spring

Spring Security

jeonghoe21 2025. 2. 20. 07:30

1. Spring Security 개요

1) Spring Security란?

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

2) Spring Security의 핵심 개념

개념설명

인증 (Authentication) 사용자의 신원을 확인하는 과정 (로그인)
인가 (Authorization) 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인
Session & Cookie 사용자의 인증 상태를 유지하는 방식
Password Encoding 비밀번호를 안전하게 저장하는 방법
OAuth2 외부 인증 제공자(Google, Facebook 등)를 통한 인증

2. Spring Security 설정 및 로그인 구현

1) Spring Security 기본 설정

Spring Boot에서 Spring Security를 사용하려면 spring-boot-starter-security 의존성을 추가해야 합니다.

Maven 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Gradle 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

2) 기본 로그인 설정 (자동 설정)

Spring Boot에서 spring-boot-starter-security를 추가하면 기본적인 로그인 페이지가 제공됩니다.

  • 기본 사용자 이름: user
  • 기본 비밀번호: 애플리케이션 실행 시 로그에 출력됨

3) 사용자 정의 로그인 페이지 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
            );
        return http.build();
    }
}
  • /admin/** 경로는 ADMIN 역할을 가진 사용자만 접근 가능
  • /login 페이지에서 로그인 진행
  • 로그인 성공 시 /home으로 리디렉션
  • /logout 경로로 로그아웃 가능

4) 커스텀 로그인 페이지 (Thymeleaf 예제)

<!-- login.html -->
<form method="post" action="/login">
    <div>
        <label>사용자명:</label>
        <input type="text" name="username" />
    </div>
    <div>
        <label>비밀번호:</label>
        <input type="password" name="password" />
    </div>
    <button type="submit">로그인</button>
</form>

 


3. REST API 개발 및 보안 적용

1) @RestController vs @Controller

어노테이션 설명
@Controller 일반적인 MVC 컨트롤러, View를 반환함 (HTML, JSP 등)
@RestController RESTful 웹 서비스를 위한 컨트롤러, JSON/XML 응답
@RestController
@RequestMapping("/api")
public class ApiController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, REST API!";
    }
}

2) HTTP 응답 변환 (HttpMessageConverter)

Spring MVC는 HttpMessageConverter를 사용하여 Java 객체를 JSON 또는 XML로 변환합니다.

자동 변환 예제

@RestController
@RequestMapping("/users")
public class UserController {
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return new User(id, "John Doe", "john@example.com");
    }
}

응답 예제 (JSON)

{
    "id": 1,
    "name": "John Doe",
    "email": "john@example.com"
}

3) Spring Security를 이용한 REST API 인증

@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .httpBasic(withDefaults());
        return http.build();
    }
}
  • /api/** 경로에 대한 요청은 인증된 사용자만 접근 가능
  • HTTP Basic 인증 방식 사용 (httpBasic())

API 호출 예제 (Basic Auth)

curl -u user:password http://localhost:8080/api/hello

4. JWT 인증 (JSON Web Token)

1) JWT란?

JWT(JSON Web Token)는 토큰 기반 인증 방식으로, 사용자의 로그인 정보를 암호화하여 클라이언트가 서버와 인증할 수 있도록 합니다.

2) JWT 구조

JWT는 .으로 구분된 3개의 부분으로 구성됩니다.

헤더(Header).페이로드(Payload).서명(Signature)
  • Header: 알고리즘 정보 (예: HS256, RS256)
  • Payload: 사용자 정보 및 만료 시간
  • Signature: 헤더와 페이로드를 비밀 키로 서명한 값

3) JWT 구현하기

JWT 의존성 추가 (Maven)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.11.5</version>
</dependency>

JWT 토큰 생성 및 검증 클래스

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {
    private static final String SECRET_KEY = "mysecretkeymysecretkeymysecretkey";
    private static final long EXPIRATION_TIME = 86400000; // 1일

    private static final Key key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());

    public static String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }

    public static Claims validateToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}

JWT 필터 추가 (Spring Security 연동)

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            Claims claims = JwtUtil.validateToken(token);
            String username = claims.getSubject();
            UserDetails userDetails = User.withUsername(username).password("").authorities("ROLE_USER").build();
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }
}

Security 설정에 JWT 필터 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

5. OAuth2 로그인 (Google 로그인)

1) OAuth2란?

OAuth2는 외부 인증 제공자(Google, Facebook 등)를 이용하여 사용자를 인증하는 방식입니다.

2) OAuth2 의존성 추가 (Maven)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

3) OAuth2 설정 (Google)

Google OAuth2를 사용하려면 Google Cloud Console에서 애플리케이션을 등록해야 합니다.

application.yml 설정

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: YOUR_CLIENT_ID
            client-secret: YOUR_CLIENT_SECRET
            scope:
              - email
              - profile

4) OAuth2 로그인 컨트롤러

@RestController
public class OAuth2LoginController {
    @GetMapping("/login-success")
    public String loginSuccess(@AuthenticationPrincipal OAuth2User oauth2User) {
        return "Welcome, " + oauth2User.getAttribute("name");
    }
}
  • 사용자가 /login/oauth2/code/google로 리디렉션되면 자동으로 인증됨.
  • 인증 후 /login-success에서 사용자 정보를 가져올 수 있음

6. 세션(Session) 및 쿠키(Cookie) 관리

1) 세션(Session)과 쿠키(Cookie)의 차이

개념설명

세션 (Session) 서버 측에서 사용자 정보를 저장하고 유지하는 방법 (ex: HttpSession)
쿠키 (Cookie) 클라이언트 측에서 정보를 저장하고 서버와 통신할 때 활용

세션과 쿠키 비교

비교 항목 세션(Session) 쿠키(Cookie)
저장 위치 서버 클라이언트(브라우저)
보안성 비교적 안전 (서버에서 관리) 보안 취약 (클라이언트에서 조작 가능)
유지 기간 기본적으로 브라우저 종료 시 삭제 만료 기간 설정 가능

2) Spring Security에서 세션 관리

Spring Security에서는 사용자의 로그인 상태를 세션을 이용하여 유지할 수 있습니다.

세션을 이용한 인증 방식 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .permitAll()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .invalidSessionUrl("/login?expired=true")
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
            );
        return http.build();
    }
}
  • SessionCreationPolicy.IF_REQUIRED: 필요할 때 세션 생성
  • invalidSessionUrl("/login?expired=true"): 세션 만료 시 로그인 페이지로 이동

세션을 이용한 사용자 정보 저장

@Controller
public class SessionController {
    @GetMapping("/session-info")
    public String sessionInfo(HttpSession session, Model model) {
        model.addAttribute("sessionId", session.getId());
        model.addAttribute("creationTime", new Date(session.getCreationTime()));
        return "sessionView";
    }
}
  • session.getId(): 현재 세션 ID 조회
  • session.getCreationTime(): 세션 생성 시간 확인

3) Spring Security에서 쿠키 관리

쿠키 기반 로그인 구현

@Controller
public class CookieController {
    @PostMapping("/set-cookie")
    public ResponseEntity<String> setCookie(HttpServletResponse response) {
        Cookie cookie = new Cookie("user-token", "secure-token-value");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(60 * 60 * 24); // 1일 유지
        response.addCookie(cookie);
        return ResponseEntity.ok("Cookie 설정 완료");
    }
}
  • setHttpOnly(true): XSS 공격 방지 (JavaScript에서 접근 불가)
  • setMaxAge(60 * 60 * 24): 쿠키 만료 시간 설정 (1일)

7. 패스워드 저장 방법 (Password Encoding)

1) 패스워드 저장 시 고려해야 할 사항

  1. 평문 저장 금지 (데이터베이스에 암호화되지 않은 비밀번호를 저장하면 보안 위험!)
  2. 단방향 암호화 사용 (복호화 불가능한 방식으로 저장해야 안전)
  3. 솔트(Salt) 사용 (무작위 문자열을 추가하여 보안 강화)

2) Spring Security에서 제공하는 패스워드 인코딩 방식

Spring Security는 PasswordEncoder 인터페이스를 제공하며, 다양한 암호화 알고리즘을 지원합니다.

패스워드 암호화 설정 (BCrypt)

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

사용자 등록 시 패스워드 암호화 적용

@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public void registerUser(String username, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        User user = new User(username, encodedPassword);
        userRepository.save(user);
    }
}

로그인 시 암호화된 비밀번호 검증

public boolean authenticate(String rawPassword, String encodedPassword) {
    return passwordEncoder.matches(rawPassword, encodedPassword);
}
  • passwordEncoder.encode(rawPassword): 암호화된 비밀번호 생성
  • passwordEncoder.matches(rawPassword, encodedPassword): 입력한 비밀번호가 저장된 비밀번호와 일치하는지 검증

8. Redis를 이용한 세션 저장

1) Redis란?

Redis는 빠른 성능과 높은 가용성을 제공하는 인메모리 데이터베이스로, Spring Security에서 세션 저장소(Session Store)로 활용할 수 있습니다.

2) Redis를 이용한 세션 관리의 장점

장점설명

확장성 여러 개의 서버에서 세션을 공유할 수 있음 (분산 처리)
속도 메모리 기반이므로 읽기/쓰기 속도가 빠름
내구성 세션 데이터를 영구 저장할 수도 있음

3) Redis 세션 저장소 설정 (Spring Boot)

의존성 추가 (Maven)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

application.yml 설정

spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379

Redis 세션 저장 활성화

@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory("localhost", 6379);
    }
}
  • @EnableRedisHttpSession: Spring Session을 Redis 기반으로 설정
  • LettuceConnectionFactory: Redis 서버와의 연결을 관리

Redis에서 세션 확인 (Redis CLI 사용)

$ redis-cli
> KEYS *
> GET "spring:session:sessions:<session-id>"

9. Security를 이용한 인가 (Authorization)

1) 인가(Authorization)란?

인가(Authorization)는 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다.

2) Spring Security에서의 인가 설정

1. 역할(Role) 기반 인가 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults());
        return http.build();
    }
}
  • /admin/**: ADMIN 역할만 접근 가능
  • /user/**: USER 또는 ADMIN 역할만 접근 가능

2. 메서드 기반 인가 설정 (@PreAuthorize)

@Service
public class AdminService {
    @PreAuthorize("hasRole('ADMIN')")
    public void adminOnlyFunction() {
        System.out.println("Admin 작업 수행");
    }
}
  • @PreAuthorize("hasRole('ADMIN')"): ADMIN 역할이 있어야 실행 가능

3. URL 패턴을 동적으로 제한 (@PostAuthorize)

@Service
public class UserService {
    @PostAuthorize("returnObject.username == authentication.name")
    public User getUserDetails(String username) {
        return userRepository.findByUsername(username);
    }
}
  • @PostAuthorize("returnObject.username == authentication.name"): 현재 인증된 사용자만 자신의 정보에 접근 가능

10. Security Test

1) Security Test 의존성 추가

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

2) MockMvc를 이용한 로그인 테스트

@WebMvcTest
class LoginTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    void loginTest() throws Exception {
        mockMvc.perform(formLogin().user("admin").password("password"))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrl("/home"));
    }
}
  • formLogin().user("admin").password("password"): 로그인 요청 수행
  • andExpect(redirectedUrl("/home")): 로그인 후 리디렉션 확인

3) @WithMockUser를 이용한 인가 테스트

@SpringBootTest
@AutoConfigureMockMvc
class AdminControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void testAdminAccess() throws Exception {
        mockMvc.perform(get("/admin"))
            .andExpect(status().isOk());
    }
}
  • @WithMockUser(username = "admin", roles = {"ADMIN"}): 가상의 관리자 사용자 생성
  • mockMvc.perform(get("/admin")): /admin 페이지에 접근 가능한지 테스트

11. CustomUserDetailsService (사용자 정의 인증 서비스)

1) UserDetailsService란?

Spring Security에서 제공하는 UserDetailsService 인터페이스는 사용자 인증 정보를 로드하는 역할을 합니다.

2) 사용자 정의 UserDetailsService 구현

1. 사용자 엔티티 (User 클래스)

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role;

    // 기본 생성자, Getter & Setter
}

2. 사용자 레포지토리 (UserRepository 인터페이스)

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

3. UserDetailsService 구현 (CustomUserDetailsService)

@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
        
        return User.withUsername(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole())
            .build();
    }
}
  • UserDetailsService를 구현하여 DB에서 사용자 정보를 조회
  • User.withUsername()을 사용하여 UserDetails 객체 생성
  • 비밀번호는 반드시 암호화해서 저장해야 함 (BCrypt 적용 필요)

12. 로그인/로그아웃 커스터마이즈

1) Spring Security 로그인 커스터마이즈

Security 설정에서 사용자 정의 로그인 설정 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(login -> login
                .loginPage("/login")
                .defaultSuccessUrl("/home", true)
                .permitAll()
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );
        return http.build();
    }
}
  • loginPage("/login"): 사용자 정의 로그인 페이지 사용
  • logoutUrl("/logout"): /logout URL로 로그아웃 가능
  • deleteCookies("JSESSIONID"): 로그아웃 시 쿠키 삭제

로그인 페이지 (login.html) 구현 (Thymeleaf 예제)

<form method="post" action="/login">
    <div>
        <label>아이디:</label>
        <input type="text" name="username" />
    </div>
    <div>
        <label>비밀번호:</label>
        <input type="password" name="password" />
    </div>
    <button type="submit">로그인</button>
</form>

2) Spring Security 로그아웃 처리

로그아웃 컨트롤러 구현 (예제)

@Controller
public class LogoutController {
    @PostMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        SecurityContextHolder.clearContext();
        return "redirect:/login?logout";
    }
}
  • SecurityContextHolder.clearContext();: 현재 사용자 정보 삭제

13. 패스워드 저장 및 암호화 (Bcrypt 적용)

1) 패스워드 저장 시 고려해야 할 사항

  1. 평문 저장 금지 (데이터베이스에 암호화되지 않은 비밀번호를 저장하면 보안 위험!)
  2. 단방향 암호화 사용 (복호화 불가능한 방식으로 저장해야 안전)
  3. 솔트(Salt) 사용 (무작위 문자열을 추가하여 보안 강화)

2) Spring Security에서 제공하는 패스워드 인코딩 방식

Spring Security는 PasswordEncoder 인터페이스를 제공하며, 다양한 암호화 알고리즘을 지원합니다.

패스워드 암호화 설정 (BCrypt)

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

사용자 등록 시 패스워드 암호화 적용

@Service
public class UserService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public void registerUser(String username, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        User user = new User(username, encodedPassword);
        userRepository.save(user);
    }
}

로그인 시 암호화된 비밀번호 검증

public boolean authenticate(String rawPassword, String encodedPassword) {
    return passwordEncoder.matches(rawPassword, encodedPassword);
}
  • passwordEncoder.encode(rawPassword): 암호화된 비밀번호 생성
  • passwordEncoder.matches(rawPassword, encodedPassword): 입력한 비밀번호가 저장된 비밀번호와 일치하는지 검증

14. OAuth2 추가 설정 (사용자 정보 저장)

1) OAuth2 로그인 후 사용자 정보 저장

Spring Security의 OAuth2를 사용하면 로그인 이후 사용자 정보를 DB에 저장할 수 있습니다.

OAuth2 User Service 구현

@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;

    public CustomOAuth2UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = new DefaultOAuth2UserService().loadUser(userRequest);
        String email = oauth2User.getAttribute("email");
        String name = oauth2User.getAttribute("name");
        
        User user = userRepository.findByUsername(email)
            .orElseGet(() -> userRepository.save(new User(email, "OAUTH2_USER", "USER")));
        
        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                                     oauth2User.getAttributes(), "email");
    }
}

Security 설정에 CustomOAuth2UserService 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService oAuth2UserService) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
            );
        return http.build();
    }
}

15. JWT와 OAuth2 연동 (OAuth2 로그인 후 JWT 발급)

1) OAuth2 로그인 후 JWT 토큰 발급

OAuth2 로그인 후 JWT를 발급하여 클라이언트가 API 요청 시 사용할 수 있도록 설정합니다.

JWT 발급 및 반환

@RestController
@RequestMapping("/auth")
public class AuthController {
    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @GetMapping("/oauth2/token")
    public ResponseEntity<String> getJwtToken(@AuthenticationPrincipal OAuth2User oauth2User) {
        String token = jwtUtil.generateToken(oauth2User.getAttribute("email"));
        return ResponseEntity.ok(token);
    }
}

JWT 필터를 추가하여 OAuth2 로그인 사용자 인증

public class JwtOAuth2AuthenticationFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;

    public JwtOAuth2AuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            Claims claims = jwtUtil.validateToken(token);
            String username = claims.getSubject();
            UserDetails userDetails = User.withUsername(username).password("").roles("USER").build();
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }
}

Security 설정에 JWT OAuth2 필터 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService oAuth2UserService, JwtUtil jwtUtil) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
            )
            .addFilterBefore(new JwtOAuth2AuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

16. OAuth2 리프레시 토큰 관리

1) 리프레시 토큰(Refresh Token)이란?

리프레시 토큰은 액세스 토큰(Access Token)이 만료되었을 때, 새로운 액세스 토큰을 발급받기 위한 토큰입니다.

2) OAuth2 리프레시 토큰을 사용하는 이유

이유설명

보안성 강화 액세스 토큰을 짧은 시간만 유효하게 설정하여 보안 강화
사용자 경험 향상 로그인 없이 새로운 액세스 토큰을 발급 가능
서버 부하 감소 사용자가 다시 로그인할 필요 없이 자동으로 인증 유지

3) OAuth2 리프레시 토큰 저장 및 활용

1. 리프레시 토큰 저장을 위한 엔티티 생성

@Entity
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String token;
    private LocalDateTime expiryDate;
}

2. 리프레시 토큰 저장을 위한 Repository 구현

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByToken(String token);
    void deleteByUsername(String username);
}

3. 리프레시 토큰 발급 및 저장 로직

@Service
public class RefreshTokenService {
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtUtil jwtUtil;

    public RefreshToken createRefreshToken(String username) {
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUsername(username);
        refreshToken.setToken(jwtUtil.generateRefreshToken(username));
        refreshToken.setExpiryDate(LocalDateTime.now().plusDays(7)); // 7일 만료
        return refreshTokenRepository.save(refreshToken);
    }

    public Optional<RefreshToken> findByToken(String token) {
        return refreshTokenRepository.findByToken(token);
    }
}

4. 리프레시 토큰을 사용한 새로운 액세스 토큰 발급

@RestController
@RequestMapping("/auth")
public class RefreshTokenController {
    private final RefreshTokenService refreshTokenService;
    private final JwtUtil jwtUtil;

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshAccessToken(@RequestBody Map<String, String> request) {
        String refreshToken = request.get("refreshToken");
        return refreshTokenService.findByToken(refreshToken)
            .map(token -> ResponseEntity.ok(Map.of("accessToken", jwtUtil.generateToken(token.getUsername()))))
            .orElseThrow(() -> new RuntimeException("Invalid refresh token"));
    }
}

17. Spring Security Advanced 설정 (CORS, CSRF, Filter Customization)

1) CORS 설정 (Cross-Origin Resource Sharing)

CORS는 브라우저가 다른 도메인의 API 요청을 허용하거나 차단하는 정책입니다.

CORS 설정 추가 (Spring Security)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

2) CSRF 설정 (Cross-Site Request Forgery)

CSRF는 악의적인 사이트가 사용자의 세션을 도용하여 요청을 보낼 수 있는 공격 방식입니다.

CSRF 보호 비활성화 (API 서버의 경우)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }
}
  • CSRF 보호는 API 서버에서는 비활성화하는 것이 일반적 (브라우저 기반 폼 제출이 없기 때문)

3) Custom Security Filter 추가

Spring Security에서 필터를 추가하여 인증 및 권한 부여 로직을 커스텀할 수 있습니다.

Custom Filter 구현

@Component
public class CustomSecurityFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        String header = request.getHeader("Custom-Auth");
        if (header == null || !header.equals("valid-token")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        filterChain.doFilter(request, response);
    }
}

Security 설정에 Custom Filter 추가

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomSecurityFilter customFilter) throws Exception {
        http
            .addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        return http.build();
    }
}

18. OAuth2 로그아웃 관리

1) OAuth2 로그아웃 개요

OAuth2를 사용하는 경우 로그아웃 시 액세스 토큰과 리프레시 토큰을 무효화하고, 세션을 종료하는 과정이 필요합니다.

2) OAuth2 로그아웃 처리 방식

방식설명

서버 측 로그아웃 액세스 토큰, 리프레시 토큰을 삭제하고 세션을 종료
클라이언트 측 로그아웃 사용자의 브라우저에서 쿠키 삭제 및 리디렉션
OAuth2 Provider 로그아웃 Google, Facebook 등의 OAuth2 제공자 로그아웃 API 호출

3) Spring Security에서 OAuth2 로그아웃 설정

1. OAuth2 토큰 무효화 서비스 구현

@Service
public class LogoutService {
    private final RefreshTokenRepository refreshTokenRepository;

    public LogoutService(RefreshTokenRepository refreshTokenRepository) {
        this.refreshTokenRepository = refreshTokenRepository;
    }

    public void logout(String username) {
        refreshTokenRepository.deleteByUsername(username);
    }
}

2. 로그아웃 API 엔드포인트 추가

@RestController
@RequestMapping("/auth")
public class LogoutController {
    private final LogoutService logoutService;

    @PostMapping("/logout")
    public ResponseEntity<String> logout(@AuthenticationPrincipal OAuth2User oauth2User) {
        logoutService.logout(oauth2User.getAttribute("email"));
        SecurityContextHolder.clearContext();
        return ResponseEntity.ok("Logged out successfully");
    }
}
  • SecurityContextHolder.clearContext(); → 현재 사용자의 인증 정보 삭제
  • refreshTokenRepository.deleteByUsername(username); → 리프레시 토큰 제거

'개발 이론 > Spring' 카테고리의 다른 글

SSR  (0) 2025.03.18
Spring, Spring Boot, JPA의 정의  (0) 2025.03.01
Spring MVC  (0) 2025.02.20
Spring Boot란?  (0) 2025.02.20