Secured Resource를 어떻게 보호할까?
PasswordEncoder, UserDetailsServiceUserDetails 객체 생성 후 인증 수행Authentication 저장 방식:
1. Session
2. ThreadLocal
private 내부적인 exception 발생/login 페이지로 redirectPOST /login으로 username, password 전송Filter에서 DaoAuthenticationProvider가 InMemory에서 사용자 확인커스터마이징 포인트:
InMemory → JDBC 방식으로 변경Username → email로 식별자 변경GET /logout 접근 시:POST /logout에 제공POST /logout 접근 시:중요: CSRF 보호를 활성화하면 모든 POST 요청에 토큰이 전달되어야 합니다.
AuthenticationManager:
사용자 정의 인증 구현 단계:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
사용자가 브라우저를 닫았다가 다시 열어도 로그인 상태를 유지하는 기능
구현 방법:
1. 쿠키 기반 방식:
설정 방법:
http.rememberMe(t -> t
.tokenValiditySeconds(60 * 60 * 24 * 30) // 30일 유효
.rememberMeParameter("remember-me")
.key("uniqueAndSecret") // 토큰 암호화에 사용되는 키
);
적용 예시:
http.rememberMe(t -> t.tokenValiditySeconds(60)); // 60초 동안 rememberMe 활성화
@Secured 어노테이션:
@Service
public class UserService {
@Secured("ROLE_ADMIN")
public List<User> getAllUsers() {
// 관리자만 접근 가능한 메서드
return userRepository.findAll();
}
}
@PreAuthorize/@PostAuthorize:
@RestController
@RequestMapping("/api/users")
public class UserController {
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// 관리자이거나 자신의 정보만 조회 가능
return userService.findById(id);
}
@PostAuthorize("returnObject.owner == authentication.name")
@GetMapping("/resources/{id}")
public Resource getResource(@PathVariable Long id) {
// 반환되는 리소스의 소유자가 현재 로그인한 사용자와 일치해야 함
return resourceService.findById(id);
}
}
Controller에서 접근:
@GetMapping("/profile")
public String viewProfile(Authentication authentication, Model model) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
model.addAttribute("user", userDetails);
return "profile";
}
SecurityContextHolder 사용:
@Service
public class UserService {
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
}
}