MSA Security 인증, 인가 구현

0

TIL

목록 보기
160/183

상황 1

회원가입을 진행한 유저가 로그인을 요청할 때


회원가입과 로그인은 권한에 대한 인증 처리가 필요하지 않으므로

@Component
@Slf4j
public class LocalJwtAuthenticationFilter implements GlobalFilter {

    @Value("${jwt.secret.key}")
    private String secretKey;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (exchange.getRequest().getURI().getPath().equals("/api/v1/auth/signup")
                || exchange.getRequest().getURI().getPath().equals("/api/v1/auth/login")) {
            return chain.filter(exchange);
        }

        String token = extractToken(exchange);

        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String username = getClaimsFromToken(token).getSubject();
        String role = getClaimsFromToken(token).get("role", String.class);

        ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
                .header("X-User-Name", username)  // 사용자명 헤더 추가
                .header("X-User-Role", role)    // 권한 정보 헤더 추가
                .build();

        ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
        return chain.filter(modifiedExchange);
    }
    
    // ...
    
}

if문을 통해 회원가입과 로그인에 대한 요청 접근은 필터를 통과하지 않도록 처리한다.


spring:
  main:
    web-application-type: reactive
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: auth  # 라우트 식별자
          uri: lb://auth  # 'auth'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/api/v1/auth/** # 경로로 들어오는 요청을 이 라우트로 처리

라우팅 설정으로 auth의 엔드포인트로 접근하는 요청은 auth 서비스로 요청을 보내고


Service Class에서 존재하는 회원인지 확인 후 토큰을 생성한다.

@Slf4j
@Component
public class JwtUtil {

    private static final String BEARER_PREFIX = "Bearer ";

    @Value("${jwt.expiration}")
    private Long expiration;

    @Value("${jwt.secret.key}")
    private String key;

    private SecretKey secretKey;

    @PostConstruct
    private void init() {
        byte[] bytes = Base64.getDecoder().decode(key);
        secretKey = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(Long userId, UserRole role) {
        Date now = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .subject(userId.toString())
                        .claim("role", role.name())
                        .expiration(new Date(now.getTime() + expiration))
                        .issuedAt(now)
                        .signWith(secretKey)
                        .compact();
    }

}

토큰에 담기는 정보(userId, role)는 String으로 반환된다.

생성된 토큰은 유저의 id, role, 발급일, 만료시각 등의 정보를 담아 ResponseEntity로 header에 담겨 사용자에게 반환된다.



상황 2

로그인한 유저가 header로 jwt를 가지고 권한이 필요한 api에 접근을 요청한다면(여기서는 Auth 서비스 안에 UserController를 추가하여 endpoint만 달라지도록 설정하였다.)

GateWay에서는 인증 처리만 진행한 뒤 각각의 서비스에 넘겨주어 각 서비스에서 인가 확인을 진행한다.


우선 GateWay로 들어온 요청은 필터 안에서 토큰에 대한 유효성 검사를 진행한다.

	private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }

        return null;
    }

    private boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode((CharSequence) secretKey));
            Jws<Claims> claimsJws = Jwts.parser()
                    .verifyWith(key)
                    .build().parseSignedClaims(token);
            log.info("#####payload :: " + claimsJws.getPayload().toString());

            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.error(e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT 토큰 만료");
        } catch (UnsupportedJwtException e) {
            log.error("지원하지 않는 JWT");
        } catch (IllegalArgumentException e) {
            log.error("잘못된 JWT");
        }
        return false;
    }

검증이 완료된 토큰에서 getClaimsFromToken 메서드를 사용해 서비스에 넘겨줄 정보들을 header에 담아 넘겨주고

	@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (exchange.getRequest().getURI().getPath().equals("/api/v1/auth/signup")
                || exchange.getRequest().getURI().getPath().equals("/api/v1/auth/login")) {
            return chain.filter(exchange);
        }

        String token = extractToken(exchange);

        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        Long userId = Long.valueOf(getClaimsFromToken(token).getSubject());
        String role = getClaimsFromToken(token).get("role", String.class);

        ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
                .header("X-User-Id", String.valueOf(userId))  // 사용자 아이디 헤더 추가
                .header("X-User-Roles", role)    // 권한 정보 헤더 추가
                .build();

        ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
        return chain.filter(modifiedExchange);
    }
    
    // ...
    
    public Claims getClaimsFromToken(String token) {
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode((CharSequence) secretKey));
        return Jwts.parser()
                .verifyWith(key)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

각각의 서비스에서는 받아온 정보들을 Context에 저장해 사용한다.

@Configuration
@EnableMethodSecurity
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomPreAuthFilter customPreAuthFilter;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // CSRF disable

                // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
                .sessionManagement((sessionManagement) ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                
                // 모든 요청 인증처리
                .authorizeHttpRequests((authorizeHttpRequests) ->
                        authorizeHttpRequests.anyRequest().authenticated()
                )

                // 필터 관리
                .addFilterBefore(customPreAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
@Component
public class CustomPreAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 헤더에서 사용자 정보와 역할(Role)을 추출
        String userId = request.getHeader("X-User-Id");
        String rolesHeader = request.getHeader("X-User-Role");

        if (userId != null && rolesHeader != null) {
            // rolesHeader에 저장된 역할을 SimpleGrantedAuthority로 변환
            List<SimpleGrantedAuthority> authorities = Arrays.stream(rolesHeader.split(","))
                    .map(role -> new SimpleGrantedAuthority(role.trim()))
                    .collect(Collectors.toList());

            // 사용자 정보를 기반으로 인증 토큰 생성
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userId, null, authorities);

            // SecurityContext에 인증 정보 설정
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        } else {
            // 빈 권한 처리
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                    null,
                    null,
                    AuthorityUtils.NO_AUTHORITIES // 빈 권한 목록
            );

            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        // 필터 체인을 계속해서 진행
        filterChain.doFilter(request, response);
    }

}

0개의 댓글

관련 채용 정보