[Spring] 스프링 심화 - Spring Security 3

호호빵·2023년 1월 8일
0

Spring

목록 보기
22/24

Spring Security

  • 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어줌.
    -> 공식 문서
    • [Spring] 스프링 심화 - Spring Security 1
      - 적용하기
      - Spring Security의 요소 확인

    • [Spring] 스프링 심화 - Spring Security 2
      - Default Form Login 방식 사용
      - UserDetails, UserDetailsService custom
      - 비밀번호 암호화 이해 및 적용

    • [Spring] 스프링 심화 - Spring Security 3
      - CustomSecurityFilter 적용
      - @AuthenticationPrincipal
      - @Secured
      - ExceptionHandling - 401, 403

1. CustomSecurityFilter 적용

적용과정

  • WebSecurityConfig
  • CustomSecurityFilter
# WebSecurityConfig

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();
        
        http.authorizeRequests().antMatchers("/api/user/**").permitAll()
																.anyRequest().authenticated();

        // Custom 로그인 페이지 사용
        http.formLogin().loginPage("/api/user/login-page").permitAll();

		// Custom Filter 등록하기
		// + UsernamePWAuthFilter 전에 CustomSecurityFilter 등록 
		http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), 
        											  UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
+) addFilterBefore()
CustomSecurityFilter를 먼저 실행하고 인증객체를 만들어 context에 추가를 하면 
인증이 완료가 되기때문에 UsernamePWAuthFilter가 수행되더라도 인증이되어서 다음필터로 넘아가지고
controller까지 요청 가능
# CustomSecurityFilter

@RequiredArgsConstructor
public class CustomSecurityFilter extends OncePerRequestFilter {

    private final UserDetailsServiceImpl userDetailsService;
    private final PasswordEncoder passwordEncoder;


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

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        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("비밀번호가 일치하지 않습니다.");
            }

            // 인증 객체 생성 및 등록
            SecurityContext context = SecurityContextHolder.createEmptyContext();			// 위에서 검증이 끝나서 필요없음
            Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            context.setAuthentication(authentication);

            SecurityContextHolder.setContext(context);
        }

		// 다음 필터 UsernamePasswordAuthenticationFilter로
        filterChain.doFilter(request,response);
    }
}

+ CustomSecurityFilter와 JwtAuthFilter의 차이점

CustomSecurityFilter

  • Filter에서 사용자가 요청한 로그인을 검증 및 인증

JwtAuthFilter

  • 토큰을 사용한 인증/인가 구현에서는 Filter에서 Id/Pw가 아니라 토큰을 검증해야함
    -> 로그인 요청에는 토큰이 없음
    -> JwtAuthFilter로 적용해야함

    JwtAuthFilter
    : 로그인 성공 후 반환된 토큰이, 인증이 필요한 API 요청과 같이 들어왔을 때 토큰을 검증하여 사용자를 인증처리 해줌

=> 토큰방식을 적용하게 되면 사용자의 로그인, 회원가입 요청은 Filter 인증되지 않게 permitAll() 처리하여 실제 검증 및 처리는 service에서 수행하도록 함
=> 그 외 인증이 필요한 요청에서는 로그인을 통해 발급받은 토큰을 같이 보내 Filter에서 토큰을 검증하고 인증처리 함


2. @AuthenticationPrincipal

  • 인증 객체의 principal 부분을 가져온다는 뜻
  • 로그인 사용자의 정보가 필요할 때 매번 DB에 접근하는 것은 비효율적이기때문에 한번 인증된 사용자 정보를 세션에 담아놓고 세션이 유지되는 동안 사용자 객체를 바로 사용하게 하기 위함
  • Spring Security에서는 해당 정보를 SecurityContextHolder 내부의 SecurityContext에 Authentication 객체로 저장해두고 있음
# UserController

@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";
    }
    
// 여기선 principal 부분에 UserDetails를 넣어줬기 때문에 UserDetails를 파라미터로 받아올 수 있음

3. @Secured

  • 권한 설정이 필요한 위치에 추가하여 권한별로 접근 통제하게 해줌
  • @PreAuthorize, @PostAuthorize 등
  • configure(HttpSecurity http)에 설정하여 관리하는 경우도 있음

적용과정

  • WebSecurityConfig : @Secured 활성화, 접근 제한 페이지 이동 설정
  • UserRoleEnum
  • UserServiceImpl : 권한 추상화
  • UserController : forbidden 페이지 반환
  • TestController
# WebSecurityConfig - 애노테이션 추가, 접근 제한 페이지 이동 설정

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화!!
public class WebSecurityConfig {
	private final UserDetailsServiceImpl userDetailsService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();
        
        http.authorizeRequests().antMatchers("/api/user/**").permitAll()
																.anyRequest().authenticated();

        // Custom 로그인 페이지 사용
        http.formLogin().loginPage("/api/user/login-page").permitAll();

		// Custom Filter 등록하기
		http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);
				
		// 접근 제한 페이지 이동 설정
		http.exceptionHandling().accessDeniedPage("/api/user/forbidden");        

        return http.build();
    }
}
# UserRoleEnum

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";
    }
}
# UserServiceImpl

public class UserDetailsImpl implements UserDetails {
		// ...

		@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(adminAuthority);

        return authorities;
    }
}
# TestController - @Secured 추가

@Controller
@RequestMapping("/api")
public class TestController {

    @Secured(value = UserRoleEnum.Authority.ADMIN)
    @PostMapping("/test-secured")
    public String securedTest(@AuthenticationPrincipal UserDetails userDetails) {
        System.out.println("*********************************************************");
        System.out.println("UserController.securedTest");
        System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
        System.out.println("*********************************************************");

        return "redirect:/api/user/login-page";
    }
}

4. ExceptionHandling - 401, 403

적용과정

  • WebSecurityConfig
  • SecurityExceptionDto
  • CustomAccessDeniedHandler
  • CustomAuthenticationEntryPoint
# WebSecurityConfig

	// 401 Error 처리, Authorization 즉, 인증과정에서 실패할 시 처리
	http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint);
        
	// 403 Error 처리, 인증과는 별개로 추가적인 권한이 충족되지 않는 경우
	http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler); 
# SecurityExceptionDto

@Getter
@NoArgsConstructor
public class SecurityExceptionDto {

    private int statusCode;
    private String msg;

    public SecurityExceptionDto(int statusCode, String msg) {
        this.statusCode = statusCode;
        this.msg = msg;
    }
}
# CustomAccessDeniedHandler

@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

@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();
        }
    }
}







@AuthenticationPrincipal 사용 이유

profile
하루에 한 개념씩

0개의 댓글