Spring Security 이어서 공부
@RequiredArgsConstructor를 클래스 위에 달고 OncePerRequestFilter를 상속.
userDetailsService와 passwordEncoder를 private final로 가져오고
doFilterInternal 메서드를 @Override
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username = " + username);
System.out.println("password = " + password);
System.out.println("request.getRequestURI() = " + request.getRequestURI());
// 뒤의 두 URI 중 하나로 들어오면서 동시에 유저명과 비밀번호가 null이 아니면
if(username != null && password != null && (request.getRequestURI().equals("/api/user/login") || request.getRequestURI().equals("/api/test-secured"))){
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 비밀번호 확인
if(!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
}
// 인증 객체 생성 및 등록
// 빈 context만들어 인증객체(authentication)를 넣고, 그 context를 context holder에 넣는다.
SecurityContext context = SecurityContextHolder.createEmptyContext();
// 위에서 비밀번호 검증을 했기때문에 credential에 비밀번호 넣을 필요 x
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
filterChain.doFilter(request,response);
}
doFilterInternal()은 request, response, filterChain을 파라미터로 받는다.
api Request가 오면 HTTP객체가 Filter를 거쳐 Controller까지 가는데 여기가 그 필터.
Filter는 Chain으로 연결되어 있는데 FilterChain 객체를 통해 Filter끼리 이동.
filterchain 객체를 받아 처리한 뒤 filterChain.doFilter(request,response)로 다음 필터로 전달.
(참고로 반환 값은 void라서 없다)
필터를 거치다가 예외처리가 되면 다음 필터로 못가고 이전 필터로 예외가 넘어간다.
request.getParameter("키 이름") 으로 클라이언트쪽에서 넘어오는 파라미터 가져온다.
request.getRequestURI()는 들어온 URI를 확인하는 메서드.
// Custom Filter 등록하기
http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);
@PostMapping("/login") // Controller에서 파라미터로 UserDetails 받을 때 사용
public String login(@AuthenticationPrincipal UserDetails userDetails) {
Authentication(인증객체)의 principal 부분의 값(userDetails)을 가져온다.
Controller에서 파라미터로 UserDetails 받을 때 앞에 사용.
(userDetails의 게터로 User의 entity와 username, password 가져올 수 있다.)
유저 권한 Enum으로 등록. "ROLE_USER"와 "ROLE_ADMIN"으로 등록.
역할(Role)에 권한(Authority) 속성 부여 / .getAuthority()로 권한을 가져올 수 있다.
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
Admin 페이지가 있어서 일반 사용자는 관리자 페이지에 접근하지 않게 하려한다.
이럴 때 @Secured를 통해 API별로 권한 제어를 할 수 있다. (다른 방법도 더 있긴 하다.)
Spring Security에 권한(Authority) 설정 방법
회원 상세정보 (UserServiceImpl)를 통해 권한(Authority)을 1개 이상 설정 가능
"권한이름" 규칙을 정해야 한다. ex) "ROLE_"로 시작해서 "ROLE_USER", "ROLE_ADMIN"
API 별 권한 제어 방법
WebSecurityConfig
위에 @EnableGlobalMethodSecurity(securedEnabled = true)
를 달아주고
Controller안의 API에 @Secured("권한 이름")
을 선언하면 해당 권한만 사용 가능.
// (관리자용) 등록된 모든 상품 목록 조회
@Secured("ROLE_ADMIN")
@GetMapping("/api/admin/products")
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
접근 권한이 없을때 403(Forbidden) 에러가 발생.
1. forbidden.html 파일 만들어 templates 폴더에 넣고
2. Controller에 /forbidden 페이지 post로 구현한 다음
@PostMapping("/forbidden")
public ModelAndView forbidden() {
return new ModelAndView("forbidden");
}
// 접근 제한 페이지 이동 설정
http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
@Secured(value = UserRoleEnum.Authority.ADMIN)
401 Error는 Authorization 즉, 인증과정에서 실패할 때 발생하는 에러이다.
WebSecurityConfig의 securityFilterChain()에 아래의 코드를 넣고,
// 401 Error 처리, Authorization 즉, 인증과정에서 실패할 시 처리
http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
// 403 Error 처리, 인증과는 별개로 추가적인 권한이 충족되지 않는 경우
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
SecurityExceptionDto를 int statusCode와 String msg 변수로 만들고,
AccessDeniedHandler를 implements한 CustomAccessDeniedHandler를 만든다.(403처리)
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
// DTO에다가 Forbidden에러코드와 Forbidden에러 메세지를 넣어 만든다.
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();
}
}
}
AuthenticationEntryPoint를 implements한 CustomAuthenticationEntryPoint도 만든다(401처리)
@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();
}
}
}
둘 다 secutiry 패키지에 생성.