데브코스 W11D4

코끼릭·2022년 6월 3일
0

TIL

목록 보기
36/36

Spring Security 필터 커스터마이징 기본

기본으로 제공되는 Authentication 구현체인 UsernamePasswordToken을 사용한 토큰 인증 객체에 대한 시큐리티 필터 커스터마이징 프로젝트의 구현은 다음과 같다.

  1. GenericFilterBean을 상속하는 커스터마이징 필터 클래스를 정의하여 doFilter라는 메소드를 오버라이딩하는데 구현되어야 하는 기능은 다음과 같다.

    • request를 전달받아 인증에 필요한 토큰을 가져온다.
    • 토큰의 페이로드에서 클레임을 인증하여 인증 객체를 생성하기 위해 필요한 principal, credentials, authorities를 가져온다.
    • 성공적으로 인증 객체를 생성한 경우 securityContextHolder에 인증된 객체를 저장한다.
    • 다음 시큐리티 필터를 이어서 호출하기 위해 chain.doFilter(request, response)를 호출하여 마무리한다.
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;
    
      if (SecurityContextHolder.getContext().getAuthentication() == null) {
        String token = getToken(request);
        if (token != null) {
          try {
            Jwt.Claims claims = verify(token);
            log.debug("Jwt parse result: {}", claims);
    
            String username = claims.username;
            List<GrantedAuthority> authorities = getAuthorities(claims);
    
            if (isNotEmpty(username) && authorities.size() > 0) {
              UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(username, null, authorities);
              authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
              SecurityContextHolder.getContext().setAuthentication(authentication);
            }
          } catch (Exception e) {
            log.warn("Jwt processing failed: {}", e.getMessage());
          }
        }
      } else {
        log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'",
          SecurityContextHolder.getContext().getAuthentication());
      }
    
      chain.doFilter(request, response);
    }
  2. 완성된 필터는 반드시 SecurityContextPersistenceFilter 필터 뒤에 동작할 수 있도록 설정해야 한다.

    
    @Configuration
    public class SecurityConfiguration {    
      
      @Bean
      public JwtAuthenticationFilter jwtAuthenticationFilter() {
    	Jwt jwt = getApplicationContext().getBean(Jwt.class);
    	return new JwtAuthenticationFilter(jwtConfigure.getHeader(), jwt);
      }
      
      @Bean
      public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
          http.authorizeRequests()
                      .antMatchers("/me").hasAnyRole("USER","ADMIN")
                      .anyRequest().permitAll()
                      .and()
                  .addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class);
                  
          return http.build();
      }
    }

Spring Security 필터 커스터마이징 심화

기본으로 제공되는 Authentication 구현체가 아닌 직접 커스터마이징한 JwtAuthenticationToken을 사용한 필터 커스터마이징은 이전에 하던 커스터마이징 작업에서 추가적인 인증 객체와 해당 객체를 인증할 수 있는 Provider 클래스 구현을 추가하면 사용이 가능하다.

  1. 기존 UsernamePasswordAuthenticationToken 대신 AbstractAuthenticationToken를 상속한 JwtAuthenticationToken 구현체로 교체하고 사용자의 principal 타입으로 사용되는 User 대신 JwtAuthentication를 만든다.

      public class JwtAuthentication {
    
        public final String token;
    
        public final String username;
    
        JwtAuthentication(String token, String username) {
          checkArgument(isNotEmpty(token), "token must be provided.");
          checkArgument(isNotEmpty(username), "username must be provided.");
    
          this.token = token;
          this.username = username;
        }
    
        @Override
        public String toString() {
          return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
            .append("token", token)
            .append("username", username)
            .toString();
        }
    
      }
    
      public class JwtAuthenticationToken extends AbstractAuthenticationToken {
    
        private final Object principal;
    
        private String credentials;
    
        public JwtAuthenticationToken(String principal, String credentials) {
          super(null);
          super.setAuthenticated(false);
    
          this.principal = principal;
          this.credentials = credentials;
        }
    
        JwtAuthenticationToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
          super(authorities);
          super.setAuthenticated(true);
    
          this.principal = principal;
          this.credentials = credentials;
        }
      }
  2. AbstractUserDetailsAuthenticationProvider를 상속하는 JwtAuthenticationProvider 클래스를 정의하고 supports 메소드와 authenticate 메소드를 오버라이딩하는데 구현되어야 하는 각 메소드의 기능은 다음과 같다.

  • supports 메소드는 커스터마이징한 인증 객체와 관련된 클래스에 대한 인증 가능 여부를 반환해야 한다.

  • authenticate 메소드는 미인증된 객체에 대해 인증 서비스를 거쳐 검증한 후 인증된 객체로 변환해서 반환해야 한다.

      
    @Override
    public boolean supports(Class<?> authentication) {
      return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      JwtAuthenticationToken jwtAuthentication = (JwtAuthenticationToken) authentication;
      return processUserAuthentication(
        String.valueOf(jwtAuthentication.getPrincipal()),
        jwtAuthentication.getCredentials()
      );
    }
    
    private Authentication processUserAuthentication(String principal, String credentials) {
      try {
        //JwtAuthenticationToken에 들어있는 JwtAuthentication이 토큰과 유저이름이 유효한지 확인
        User user = userService.login(principal, credentials);
        List<GrantedAuthority> authorities = user.getGroup().getAuthorities();
        String token = getToken(user.getLoginId(), authorities);
        JwtAuthenticationToken authenticated =
          new JwtAuthenticationToken(new JwtAuthentication(token, user.getLoginId()), null, authorities);
        authenticated.setDetails(user);
        return authenticated;
      } catch (IllegalArgumentException e) {
        throw new BadCredentialsException(e.getMessage());
      } catch (DataAccessException e) {
        throw new AuthenticationServiceException(e.getMessage(), e);
      }
    }
    
    @Service
    public class UserService {
      private final PasswordEncoder passwordEncoder;
      private final UserRepository userRepository;
    
      public UserService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
      }
    
      @Transactional(readOnly = true)
      public User login(String principal, String credentials) {
        checkArgument(isNotEmpty(principal), "principal must be provided.");
        checkArgument(isNotEmpty(credentials), "credentials must be provided.");
    
        User user = userRepository.findByLoginId(principal)
          .orElseThrow(() -> new UsernameNotFoundException("Could not found user for " + principal));
        user.checkPassword(passwordEncoder, credentials);
        return user;
      }
    
      @Transactional(readOnly = true)
      public Optional<User> findByLoginId(String loginId) {
        checkArgument(isNotEmpty(loginId), "loginId must be provided.");
        return  userRepository.findByLoginId(loginId);
      }
    } 
  1. 인증을 처리하는 기능을 추가하기 위해 AuthenticationManager에 앞서 만든 JwtAuthenticationProvider를 추가한다.

    
    @Configuration
    public class SecurityConfiguration {    
      
      @Bean
      public JwtAuthenticationFilter jwtAuthenticationFilter() {
    	Jwt jwt = getApplicationContext().getBean(Jwt.class);
    	return new JwtAuthenticationFilter(jwtConfigure.getHeader(), jwt);
      }
      
      @Bean
      public JwtAuthenticationProvider jwtAuthenticationProvider() {
      	Jwt jwt = getApplicationContext().getBean(Jwt.class);
    	UserService userService = getApplicationContext().getBean(UserService.class);
    	return new JwtAuthenticationProvider(jwt, UserService);
      }
      
      @Bean
      public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
          http.authenticationProvider(jwtAuthenticationProvider())
          		.authorizeRequests()
                      .antMatchers("/me").hasAnyRole("USER","ADMIN")
                      .anyRequest().permitAll()
                      .and()
                  .addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class);
                  
          return http.build();
      }
    }
  2. JwtAuthenticationFilter에서 principal 필드에 JwtAuthentication 객체를 저장, details 필드에는 WebAuthenticationDetails 객체를 저장하도록 수정한다.

    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;
    
      if (SecurityContextHolder.getContext().getAuthentication() == null) {
        String token = getToken(request);
        if (token != null) {
          try {
            //토큰 유효성 확인하기	 
            Jwt.Claims claims = verify(token);
            log.debug("Jwt parse result: {}", claims);
    
    		//토큰에 저장된 사용자 이름과 권한 가져오기	
            String username = claims.username;
            List<GrantedAuthority> authorities = getAuthorities(claims);
    
            if (isNotEmpty(username) && authorities.size() > 0) {
              //사용자 이름과 권한을 바탕으로 인증 객체 생성
              JwtAuthenticationToken authentication =
                new JwtAuthenticationToken(new JwtAuthentication(token, username), null, authorities);
              authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
              SecurityContextHolder.getContext().setAuthentication(authentication);
            }
          } catch (Exception e) {
            log.warn("Jwt processing failed: {}", e.getMessage());
          }
        }
      } else {
        log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'",
          SecurityContextHolder.getContext().getAuthentication());
      }
    
      chain.doFilter(request, response);
    }
    
  3. Jwt 인증 객체를 활용한 정보 조회 API와 로그인 API를 구현한다.

    
    @RestController
    @RequestMapping("/api")
    public class UserRestController {
    
    private final UserService userService;
    
    private final AuthenticationManager authenticationManager;
    
      public UserRestController(UserService userService, AuthenticationManager authenticationManager) {
        this.userService = userService;
        this.authenticationManager = authenticationManager;
      }
    
      @GetMapping(path = "/user/me")
      public UserDto me(@AuthenticationPrincipal JwtAuthentication authentication) {
        return userService.findByLoginId(authentication.username)
          .map(user ->
            new UserDto(authentication.token, authentication.username, user.getGroup().getName())
          )
          .orElseThrow(() -> new IllegalArgumentException("Could not found user for " + authentication.username));
      }
    
      @PostMapping(path = "/user/login")
      public UserDto login(@RequestBody LoginRequest request) {
        JwtAuthenticationToken authToken = new JwtAuthenticationToken(request.getPrincipal(), request.getCredentials());
        Authentication resultToken = authenticationManager.authenticate(authToken);
        JwtAuthentication authentication = (JwtAuthentication) resultToken.getPrincipal();
        User user = (User) resultToken.getDetails();
        return new UserDto(authentication.token, authentication.username, user.getGroup().getName());
      }
    }
    

    @AuthenticationPrincipal

    인증된 Authentication 구현체에 principal 필드 값을 가져오는 아노테이션으로 AuthenticationPrincipalArgumentResolver를 통해 Authentication 구현체 내부 principal 필드 타입으로 변환된 객체를 반환해준다.

    JWT 인증을 사용한 로그인 API의 동작 과정

    1. 사용자로부터 로그인에 필요한 아이디와 비밀번호 정보가 들어온다.
    2. 사용자가 입력한 정보를 바탕으로 JwtAuthenticationToken을 만든다.
    3. AuthenticationManager에 등록된 JwtAuthenticationProvider가 authenticate 메소드를 사용하여 생성된 JwtAuthenticationToken에 대한 인증을 한다.
    4. principal에 있는 아이디와 credential에 있는 비밀번호가 DB에 저장된 정보와 일치하는지 확인한다.
    5. 일치하는 경우 principal에 인증된 JwtAuthenticationToken을 생성해서 반환한다.

    JWT를 이용한 인가 API 동작 과정

    1. JwtSecurityFilter에서 사용자 요청 내 token을 가져와 인증 객체를 SecurityContextHolder에 저장한다.
    2. SecurityContextHolder에 저장된 인증 객체가 접근하려는 요청이 유효한 경우에만 컨트롤러로 통과시킨다.
    3. @AuthenticationPrinciple을 이용해 인증 객체의 principle인 JwtAuthentication을 받아온다.
    4. 인증 객체에 대한 정보를 바탕으로 요청에 대한 응답을 반환한다.
profile
ㅇㅅㅇ

0개의 댓글