[BooTakHae] 인증/인가 로직 리팩토링

Kim Hyen Su·2024년 5월 21일

BooTakHae

목록 보기
11/22
post-thumbnail

개요

이번 포스팅에서는 User-Service와 API Gateway에 구현한 인증/인가 관련 로직을 리팩토링 하겠습니다.

해당 로직은 기존에 권한 확인에 대한 로직을 별도로 구현하지 않았습니다.

즉, JWT 인증 및 검증 처리만 구현되어 있던 상태였습니다.

기존에 이처럼 구현한 이유는 권한에 따라 제한할 리소스가 없었기 때문입니다. 즉, 인증 절차(회원가입 및 로그인)만 거치면 모든 리소스에 접근이 가능하도록 구현해놨습니다.

하지만, 이벤트 상품을 추가하는 로직이 생기면서 관리자 권한의 필요성이 조금 느껴졌습니다.

이에 따라, 기능의 확장성을 고려하여 권한을 확인하는 로직을 추가하려 합니다.

기존 로직

기존의 로직은 위처럼 인증 처리 후 JWT 발급 및 JWT 검증 후에는 모든 리소스에 접근이 가능하도록 구현되어 있습니다.

수정 후 로직

위 그림과 같이 권한에 따라 다른 필터를 생성하여 리소스 접근을 제한하도록 구현했습니다.

다음과 같이 jwt 토큰 내 roles 라는 이름의 권한이 저장된 것을 확인할 수 있습니다.

User-Service


loadUserByUsername

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username)
                .orElseThrow(()->new UsernameNotFoundException(username));

        Collection<SimpleGrantedAuthority> authorities = new ArrayList<>(); // 추가
        authorities.add(new SimpleGrantedAuthority(userEntity.getRole().name())); // 추가

        return new User(userEntity.getEmail(), userEntity.getPassword(),
                true,true,true,true, authorities); // 수정
    }

successfulAuthentication

 @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        log.debug("로그인 성공 후 처리");

        UserDetails user = ((User) authResult.getPrincipal());

        List<String> roleList = user.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .toList();

        String roles = roleList.stream().collect(Collectors.joining(",")); // 추가

        String email = user.getUsername();

        UserDto userDetails = tokenProvider.getUserDetailsByEmail(email);

        String accessToken = tokenProvider.createAccessToken(userDetails.getUserId(), roles);
        Date expiredTime = tokenProvider.getExpiredTime(accessToken);
        String refreshToken = tokenProvider.createRefreshToken(roles);

        refreshTokenService.saveTokenInfo(userDetails.getUserId(), refreshToken);

        response.setContentType("application/json");

        // body 설정
        Map<String, Object> tokens = Map.of(
                "userId", userDetails.getUserId(),
                "accessToken", accessToken,
                "expiredTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiredTime)
        );

        new ObjectMapper().writeValue(response.getOutputStream(), tokens);
    }

createAccessToken

public String createAccessToken(String userId, String roles){
        return Jwts.builder()
                .subject(userId)
                .claim("roles", roles) // 추가
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + Long.parseLong(ACCESS_EXPIRED_TIME)*1000L))
                .signWith(getSigningKey())
                .compact();
    }

API Gateway


AdminAccessFilter

@Slf4j
@Component
public class AdminAccessFilter  extends AbstractGatewayFilterFactory<AdminAccessFilter.Config> implements Ordered {

    TokenProvider tokenProvider;

    public AdminAccessFilter(Environment env, TokenProvider tokenProvider) {
        super(AdminAccessFilter.Config.class);
        this.tokenProvider = tokenProvider;
    }

    @Override
    public int getOrder() {
        return 2;
    }

    public static class Config {

    }

    @Override
    public GatewayFilter apply(AdminAccessFilter.Config config) {
        return (exchange, chain) -> {

            String jwt = tokenProvider.getToken(exchange.getRequest());

            log.debug("access-token 검증 : 권한 확인");
            String roles = tokenProvider.getRolesByToken(jwt);
            if (Objects.isNull(roles) || !roles.contains("ADMIN")) {
                return ErrorResponse.onError(exchange, ErrorResponse.of(ErrorCode.NOT_ACCESSIBLE_AUTHORITY));
            }

            return chain.filter(exchange);
        };
    }
}

AuthorizationHeaderFilter를 통해 토큰 검증을 우선 수행하기 위해 Ordered 인터페이스를 구현하여 필터 간에 우선순위를 적용했습니다.

application.yml

          # user-service 공통
        - id: user-service
          uri: http://localhost:포트번호/
          predicates:
            - Path=/user-service/**
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
            - name: AuthorizationHeaderFilter
            - name: AdminAccessFilter # 추가

테스트를 위해서 잠시 User-Service로 들어오는 요청에 AdminAccessFilter를 거치도록 설정하겠습니다.

테스트 및 결과

서버를 기동하여 Postman과 Debugger를 활용하여 요청 후 결과가 어떻게 출려되는지 확인하겠습니다.

회원가입과 로그인을 진행해줍니다.

그 다음 User-Service의 health-check로 요청을 보내보겠습니다.

디버거 모드에서 jwt에 포함된 roles라는 값이 "USER" 인 것을 확인하였고, 결과는 다음과 같이 403 오류가 발생한 것을 확인했습니다.

DB의 Role을 수정한 뒤, 재로그인 하여 토큰을 재발급하도록 하겠습니다.

동일하게 User-Service의 health-check 요청을 보내겠습니다.

roles가 ADMIN인 것도 확인되고, 정상적으로 응답이 발생한것도 확인했습니다.

profile
백엔드 서버 엔지니어

0개의 댓글