4 분 소요

Users Microservice 기능 추가 - Login

/user-service/login URI 로 POST 방식으로 사용자의 이메일과 비밀번호를 받아 로그인 처리하는 기능을 추가하자.

로그인 처리를 위해AuthenticationFilter를 만들것이다. 이 클래스는 스프링 시큐리티의 UsernamePasswordAuthenticationFilter를 상속받는다. attemptAuthentication(), successfulAuthentication()를 구현하자.

RequestLogin

@Data
public class RequestLogin {

    @NotNull(message = "Email cannot be null")
    @Size(min = 2, message = "Email not be less than two characters")
    @Email
    private String email;

    @NotNull(message = "Password cannot be null")
    @Size(min = 2, message = "Password must be equals or greater than 8 characters")
    private String password;
}

RequestLogin 클래스는 사용자의 로그인 정보를 받는다.

AuthenticationFilter

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @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 {

    }
}

ObjectMapper().readValue(request.getInputStream(), RequestLogin.class); 전달되어진 InputStream에 값이 있을 때 그 값을 RequestLogin 클래스 타입으로 바꿔준다.

UserServiceImpl 수정

public class UserServiceImpl implements UserService{

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

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

        if (userEntity==null){
            throw new UsernameNotFoundException(username);
        }
        return new User(userEntity.getEmail(), userEntity.getEncryptedPwd(),
                true, true, true,true,
                new ArrayList<>());
    }
    ...
}

WebSecurity

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(HttpSecurity http) throws Exception { // <- 권한
        http.csrf().disable();
//        http.authorizeHttpRequests().antMatchers("/users/**").permitAll();
        http.authorizeRequests().antMatchers("/**")
                .hasIpAddress("192.168.0.2")// IP 제한 <- 본인 IP 변경
                .and()
                .addFilter(getAuthenticationFilter()); // 이 필터 통과한 데이터만
        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception{
        AuthenticationFilter authenticationFilter = new AuthenticationFilter();
        authenticationFilter.setAuthenticationManager(authenticationManager());

        return authenticationFilter;
    }

    // db_pwd(encrypted)==input__pwd(encrypted)
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception { // <- 인증
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
}

IP 제한과 필터를 통과한 request data 만 권한을 부여하고 작업을 진행한다.

Routes 정보 변경

api gateway service의 유저 서비스 라우트 설정을 변경하자

application.yml

spring:
  application:
    name: apigateway-service

  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
#        - id: user-service
#          uri: lb://USER-SERVICE
#          predicates:
#            - Path=/user-service/**
        - 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}

앞선 설정에서는 유저 서비스를 이용하려면 /user-service/*_ 같이 /user-serivce가 있어야 했다. api gateway에서 우리가 원하는 형태로 값을 변경해 보낼 수 있기 때문에 RewritePath=/user-service/(?\<segment>._), /$\{segment} 로 /user-service/ 로 들어온 uri를 변경하였다.

Postman 을 통해 POST로 http://127.0.0.1:8000/user-service/users 로 회원가입을 하고 http://127.0.0.1:8000/user-service/login 으로 로그인 성공을 확인하였다.

인증 성공 시 사용자에게 Token 을 발행시키자.

JWT 생성

AuthenticationFilter 와 successfulAuthentication() 수정

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
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        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());
    }

토큰을 만들 때 token.secret을 사용해 토큰을 암호화 한다.

application.yml 토큰정보 추가

token:
  expiration_time: 86400000 # 1 day
  secret: user_token

이제 로그인 요청을 보내게 되면 response의 헤더에 token : eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2NTgxY2Y0Mi1mZDMxLTQ1NzAtYjhiYi0wM2QzYTc0YWViMDIiLCJleHAiOjE2NTUxMTAxNzJ9.pzNQdDvJIjdn5mtD_JepvaSwHO_f2vUPiVAhkQwgIoLqzuWTNghJk0heMgVho32SJ-0iKfH9wQYt2eK5j20WFw userId : 6581cf42-fd31-4570-b8bb-03d3a74aeb02

정보가 추가된다. userId로 token을 만들어 냈다.

전통적인 인증 시스템에서는 쿠키와 세션을 이용해 인증처리를 하였다. 이런 세션과 쿠키를 사용했을 때 타 기종(모바일 어플리케이션)에서 유효하게 사용할 수 없는 문제(공유 불가)가 있다. JSON 이나 XML 과 같은 포맷이 필요하다.

이런 문제를 해결하기 위해 Token 기반 인증 시스템이 나왔다. 클라이언트에게 토큰을 발급하고 클라이언트는 이 토큰을 가지면 권한을 갖게 되어 다시 서버에 요청을 했을 때 서버에서 토큰이 유효한지 검사하고 서비스를 제공한다.

JWT 의 장점은 다음과 같다

  • 클라이언트 독립적인 서비스(stateless)
  • CDN (Content Delivery Network)
  • NO Cookie-Session (No CSRF, 사이트간 요청 위조)
  • 지속적인 토큰 저장

1

서로 다른 마이크로서비스에서 JWT 토큰을 데이터베이스에 각각 저장하면 다른 서버에서도 그 JWT를 발급받아 사용가능하여 세션간 공유가 가능해진다.

Api gateway service에 AuthorizationHeaderFilter 를 추가하자

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

    Environment env;

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

    public static class Config{

    }


    // login -> token -> user(with token) -> header(include token)
    @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 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 ex) {
            returnValue=false;
        }

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

    //Mono ,Flux -> Spring WebFlux
    private Mono<Void> onError(ServerWebExchange exchange,
                               String err,
                               HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();
    }
}

이제 이 필터를 user-service에서 get 메소드로 올 때 적용을 한다.

application.yml

- id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/**
            - Method=GET
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/user-service/(?<segment>.*), /$\{segment}
            - AuthorizationHeaderFilter