// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 어떤 요청이든 '인증'
.anyRequest().authenticated()
.and()
// 로그인 기능 허용
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
.failureUrl("/user/login?error")
.permitAll()
.and()
// 로그아웃 기능 허용
.logout()
.permitAll();
}
}
스프링 시큐리티는 요청이 들어오면 Servlet Filter Chain을 자동으로 구성한 후 거치게 된다.
Filter는 Client요청이 전달되기 전후의 URL패턴에 맞는 모든 요청에 필터링 역할을 한다.
예) CSRF, XSS 등 보안 검사를 통해 올바른 요청이 아닐 경우 이를 차단 한다.
이런 기능을 활용하여 스프링시큐리티에서 filter를 사용하여 인증/인가를 구현한다.
Spring 의 보안 Filter를 결정하는데 사용되는 Filter
session, jwt 등의 인증방식들을 사용하는데에 필요한 설정을 완전히 분리할 수 있는 환경을 제공한다.
그 중
- AbstractAuthenticationProcessingFilter
: 사용자의 credential을 인증하기 위한 베이스 Filter
- SecurityContextHolder에는 스프링 시큐리티로 인증한 사용자의 상세정보를 저장한 Authentication객체를 저장한 SecurityContext를 담고 있다.
SecurityContext ? SecurityContextHolder로 접근 가능 ,Authentication객체 가지고 있음
Authentication : 현재 인증된 사용자를 나타내며 , SecurityContext에서 가져올 수 있다.
<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
System.out.println("authority = " + authority);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
-UserDetailsService
: username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환한다. Custom하여 Bean으로 등록 후 사용 가능하다.
🔥 회원 등록 시 '비밀번호'는 사용자가 입력한 문자 그대로 DB 에 등록하면 안 됩니다.
'정보통신망법, 개인정보보호법' 에 의해 비밀번호 암호화(Encryption)가 의무입니다.
// 비밀번호 확인
if(!passwordEncoder.matches("사용자가 입력한 비밀번호","저장된 비밀번호")){
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
@Configuration
@EnableWebSecurity// 스프링 시큐리티 지원하능하게 함
public class WebSecurityConfig{
@Bean//비밀번호 암호화 기능 등록
public PasswordEncoder passwordEncoder(){
return new BcryptPasswordEncoer();
}
}
세션을 사용하는 방식과 jwt 같은 토큰을 사용하는 방식에 따라서 시큐리티 설정과 처리방식이 매우 다양한 로직을 띄는데..
간단히 살펴보면 세션을 사용한다면 브라우저의 JSESSIONID를 활용하여 사용자 세션정보를 구분할 수 있으므로 특별히 요청 전에 세션정보만 잘 가져온다면 추가적인 처리가 덜하겠지만 JWT 같은 토큰을 활용한다면 요청된 요청마다의 토큰 정보(곧 세션정보가 될)를 읽어 매번 인증을 진행할 것이다. 따라서 매 요청마다 Filter를 활용하여 SecurityContext에 요청마다 인증되는 Authentication 객체를 set 시킬 것이고 이후에 Controller 에서 @AuthenticationPrincipal을 활용하여 가져올 수 있는 부분이다.
@AuthenticationPricipal: authentication 즉 인증 객체의 principal부분의 값을 가지고 오는데,
Filter를 통해 인증객체를 만들 때 principal부분에 UserDetails를 넣어줬기 때문에 이것을 파라미터로 받아 올 수 있다.
그렇기 때문에 user,username,password 도 안에 들어 있어 유저의 정보를 가져올 수 있다.
@PostMapping("/login")
public String login(@AuthenticationPrincipal UserDetails userDetails) {
System.out.println("*********************************************************");
System.out.println("UserController.login");
System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
System.out.println("*********************************************************");
return "redirect:/api/user/login-page";
}
- Controller 에 "**@Secured"** 어노테이션으로 권한 설정 가능
- **@Secured("권한 이름")** 선언
- 권한 1개 이상 설정 가능
// (관리자용) 등록된 모든 상품 목록 조회
@Secured("ROLE_ADMIN")
@GetMapping("/api/admin/products")
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
<@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
**@EnableGlobalMethodSecurity(securedEnabled = true)** // @Secured 어노테이션 활성화
public class WebSecurityConfig {
401,403이 들어오면 commence랑 handle함수가 작동하여 결과를 objectMapperdmfh
스트링값으로 반환하여 Client에게 값을 보낸다.
WebSecurityConfig
public class WebSecurityConfig {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final UserDetailsServiceImpl userDetailsService;
// 접근 제한 페이지 이동 설정
// http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
// 401 Error 처리, Authorization 즉, 인증과정에서 실패할 시 처리
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
// 403 Error 처리, 인증과는 별개로 추가적인 권한이 충족되지 않는 경우
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
return http.build();
}
}
CustomAccessDeniedHandle.java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException{
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
CustomAuthenticationEntryPoint.java
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final SecurityExceptionDto exceptionDto =
new SecurityExceptionDto(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try (OutputStream os = response.getOutputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(os, exceptionDto);
os.flush();
}
}
}
ㅠ.ㅠ 정신차리자!!!!!!