๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ“”๊ฐ•์˜ ์ •๋ฆฌ ๋…ธํŠธ

[Spring Cloud๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค] Section 4: ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ์™€ JWT ์ ์šฉ

apigateway ๊ตฌ์„ฑ

application.yml์— ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์ •๋ณด ์ถ”๊ฐ€

	- id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/login
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie # ์ฟ ํ‚ค ์‚ญ์ œ ํ›„ ์‹คํ–‰
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/users
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie # ์ฟ ํ‚ค ์‚ญ์ œ ํ›„ ์‹คํ–‰
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/**
            - Method=GET
          filters:
            - RemoveRequestHeader=Cookie # ์ฟ ํ‚ค ์‚ญ์ œ ํ›„ ์‹คํ–‰
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
            - AuthorizationHeaderFilter
  • user-service์— login๊ณผ users(ํšŒ์›๊ฐ€์ž…) ๊ธฐ๋Šฅ์€ ์ฟ ํ‚ค ์‚ญ์ œ ํ›„ 'AuthorizationHeaderFilter'๋ฅผ ์ ์šฉํ•˜์ง€ ์•Š์Œ. (๊ถŒํ•œ์ฒดํฌ๊ฐ€ ์—†๋‹ค๋Š” ๋œป)
  • ๋‚˜๋จธ์ง€ GET๋ฉ”์„œ๋“œ์— ๊ด€ํ•ด์„œ๋Š” 'AuthorizationHeaderFilter'๋ฅผ ์ ์šฉ

AuthorizationHeaderFilter (๊ถŒํ•œ ํ•„ํ„ฐ)

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
    Environment env;

    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    // login -> token -> users(with token) -> header์— ํ† ํฐ ๊ฒ€์‚ฌ
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
            }

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace("Bearer", "");
            if (!isJwtValid(jwt)) {
                return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
            }

            return chain.filter(exchange);
        };
    }

    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        log.error(err);
        return response.setComplete();
    }

    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;
        String subject = null;

        try {
            subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
                    .parseClaimsJws(jwt).getBody()
                    .getSubject();
        } catch (Exception exception) {
            returnValue = false;
        }

        if (subject == null || subject.isEmpty()) {
            returnValue = false;
        }

        return returnValue;
    }

    public static class Config {

    }
}

 

  • AbstractGatewayFilterFactory๋ฅผ ์ƒ์†๋ฐ›์•„์„œ ๊ตฌํ˜„
  • apply(Config config)๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ ์ ์šฉํ•  ํ•„ํ„ฐ๊ธฐ๋Šฅ ์ž‘์„ฑ
    • request์˜ ํ—ค๋”์˜ AUTHORIZATION์ •๋ณด๋ฅผ ํ™•์ธํ•˜์—ฌ .ymlํŒŒ์ผ์— ์ €์žฅ๋œ 'token.secret'์ •๋ณด๋ฅผ ์ด์šฉํ•ด jwt์ •๋ณด๋ฅผ ํŒŒ์‹ฑํ•ด๋ณธ๋‹ค.
    • ์œ ํšจํ•œ ์ •๋ณด๋ผ๋ฉด ๋‹ค์Œ ํ•„ํ„ฐ๋ฅผ ์ง„ํ–‰ํ•˜๊ณ  ์•„๋‹ˆ๋ผ๋ฉด Error๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

User-Service ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค

ํšŒ์›๊ฐ€์ž… API (POST user-service/users)

  • Controller
@PostMapping("/users")
public ResponseEntity<ResponseUser> createUser(@RequestBody RequestUser user) {
    ModelMapper mapper = new ModelMapper();
    mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
    UserDto userDto = mapper.map(user, UserDto.class);
    userService.createUser(userDto);

    ResponseUser responseUser = mapper.map(userDto, ResponseUser.class);

    return ResponseEntity.status(HttpStatus.CREATED).body(responseUser);
}

Client๋กœ ๋ถ€ํ„ฐ ๋ฐ›์€ RequestUser์ •๋ณด๋ฅผ UserDto๋กœ ๋ณ€ํ™˜ํ›„ ์„œ๋น„์Šค๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์‹คํ–‰ ํ›„ ๋ฐ›์€ dto์ •๋ณด๋ฅผ ResponseUser์ •๋ณด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ 201์ƒํƒœ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

  • Service
@Override
public UserDto createUser(UserDto userDto) {
    userDto.setUserId(UUID.randomUUID().toString());

    ModelMapper mapper = new ModelMapper();
    mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
    UserEntity userEntity = mapper.map(userDto, UserEntity.class);
    userEntity.setEncryptedPwd(passwordEncoder.encode(userDto.getPwd()));

    userRepository.save(userEntity);

    UserDto retrunUserDto = mapper.map(userEntity, UserDto.class);
    return retrunUserDto;
}

์ด ์„œ๋น„์Šค์—์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€ ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜จ ํŒจ์Šค์›Œ๋“œ์ •๋ณด๋ฅผ ์ธ์ฝ”๋”ฉํ•˜์—ฌ db์— ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„์ด๋‹ค.

 

ํšŒ์›์ •๋ณด ์กฐํšŒ API

@GetMapping("/users")
public ResponseEntity<List<ResponseUser>> getUser() {
    Iterable<UserEntity> userList = userService.getUserByAll();

    List<ResponseUser> result = new ArrayList<>();
    userList.forEach(v -> {
        result.add(new ModelMapper().map(v, ResponseUser.class));
    });

    return ResponseEntity.status(HttpStatus.OK).body(result);
}

@GetMapping("/users/{userId}")
public ResponseEntity<ResponseUser> getUser(@PathVariable("userId") String userId) {
    UserDto userDto = userService.getUserByUserId(userId);

    ResponseUser result = new ModelMapper().map(userDto, ResponseUser.class);
    return ResponseEntity.status(HttpStatus.OK).body(result);
}

ํšŒ์›์ •๋ณด๋ฅผ ์•„๋ฌด๋‚˜ ์กฐํšŒํ•˜๋ฉด ์•ˆ๋œ๋‹ค. ๋”ฐ๋ผ์„œ, ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•˜๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์ด ๊ณผ์ •์„ ํ•„ํ„ฐ๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

Security (๊ถŒํ•œ ์กฐํšŒ)

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private UserService userService;
    private Environment env;

    public AuthenticationFilter(AuthenticationManager authenticationManager,
                                UserService userService,
                                Environment env) {
        super.setAuthenticationManager(authenticationManager);
        this.userService = userService;
        this.env = env;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getEmail(),
                            creds.getPassword(),
                            new ArrayList<>())
                    );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // ์„œ๋น„์Šค์—์„œ ๋ฐ˜ํ™˜ํ•œ User๊ฐ์ฒด
        String userName = ((User)authResult.getPrincipal()).getUsername();
        UserDto userDetails = userService.getUserDetailsByEmail(userName);

        String token = Jwts.builder()
                .setSubject(userDetails.getUserId())
                .setExpiration(new Date(System.currentTimeMillis() +
                        Long.parseLong(env.getProperty("token.expiration_time"))))
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
                .compact();

        response.addHeader("token", token);
        response.addHeader("userId", userDetails.getUserId());

    }
}

'UsernamePasswordAuthenticationFilter'๋ฅผ ์ƒ์†ํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•œ๋‹ค. ์ด๋Š” '๋กœ๊ทธ์ธ ์‹œ ์‹คํ–‰๋˜๋Š” ํ•„ํ„ฐ' (/login) ์ด๋‹ค.

 

๋จผ์ € ์š”์ฒญ์‹œ ๊ถŒํ•œ์ฒดํฌ๋กœ attemptAuthentication(~)๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ–ˆ๋‹ค.

body์— ์‹ค๋ ค์˜จ ์ •๋ณด๋ฅผ RequestLogin์ •๋ณด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ '์ด๋ฉ”์ผ', 'ํŒจ์Šค์›Œ๋“œ' ์ •๋ณด๋ฅผ ์ด์šฉํ•œ ๊ถŒํ•œ์ •๋ณด๋ฅผ ์„ค์ •ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

userServiceImpl์—์„œ UserDetailService์˜ loadByUserName์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ User๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ์„ฑ๊ณต ์‹œ user์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์—ฌ jwt token์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. (successfulAuthentication๋ฉ”์„œ๋“œ)

 

๋งŒ๋“  jwt์ •๋ณด๋ฅผ header์˜ token์— ์ €์žฅํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•œ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์ด ์ •๋ณด๋ฅผ ์ด์šฉํ•˜์—ฌ ํšŒ์›์ •๋ณด์กฐํšŒ ์š”์ฒญ์— ์‚ฌ์šฉํ•œ๋‹ค.

 

WebSecurity ์„ค์ • ์ •๋ณด

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    private UserService userService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;

    public WebSecurity(UserService userService, BCryptPasswordEncoder bCryptPasswordEncoder, Environment env) {
        this.userService = userService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.env = env;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
//        http.authorizeRequests().antMatchers("/users/**").permitAll();
        http.authorizeRequests().antMatchers("/**").permitAll()
                .and()
                .addFilter(getAuthenticationFilter());

        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(authenticationManager(), userService, env);
        return authenticationFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
}

์ƒ์†๋ฐ›์€ ํ•„ํ„ฐ๋ฅผ ๋“ฑ๋กํ•˜๊ณ  ๋กœ๊ทธ์ธ ์‹œ ํ™œ์šฉํ•œ๋‹ค.

 

api-gateway -> ๊ถŒํ•œ์กฐํšŒ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ํ—ค๋”์— jwt์ •๋ณด๊ฐ€ validํ•œ์ง€ ๊ฒ€์‚ฌ ํ›„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ์š”์ฒญ ๋ณด๋ƒ„

user-service -> login์‹œ์—๋Š” username(or email)๋กœ jwt์ •๋ณด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ—ค๋”์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•œ๋‹ค.