3 분 소요

저번에 만들었던 user-service에 로그인 성공 처리 로직을 추가해보자.

개요

스크린샷 2022-10-03 오후 2 21 30

  1. 사용자가 입력한 emailpasswordAuthenticationFilter에 전달되면, attemptAuthentication() 메서드가 가장 먼저 처리한다.
  2. 그 다음으로, 사용자가 입력한 emailpasswordUsernamePasswordAuthenticationToken의 형태로 바꿔서 사용한다.
  3. 그리고 UserDetailService를 구현하고 있는 클래스에서 loadUserByUsername() 메서드가 실행된다.
  4. loadUserByUsername() 메서드 안에서 findByEnail()을 통해 db에서 UserEntity를 가져오고, 이를 Spring Security에 있는 User 객체로 변경해서 사용한다.
  5. 마지막으로, 모든 과정이 끝나고 정상적으로 로그인이 되었다면 AuthenticationFilter에서 successfulAuthentication() 메서드 내에서 토큰을 발행한다.
  6. 그런데, 사용자 email으로 토큰을 만들게 아니라, userId(=UUID)로 토큰을 만들고 싶기 때문에,
    successfulAuthentication() 메서드 안에서 getUserDetailsByEmail()을 호출해 사용자 정보(UserDto)를 가져온다.
  7. 6번에서 반환되어진 UserDto 안에 포함된 userId(=UUID)를 이용해 토큰(jwt)을 발행해 response의 헤더에 추가한다.

user-service

pom.xml

토큰을 생성하기 위해 dependency에 jwt를 추가한다.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

application.yml

token:
  expiration_time: 86400000 # 60 * 60 * 24 * 1000 # 하루짜리 토큰
  secret: user_token

만료시간 계산은 millisecond로 한다.(초 단위 * 1000)

  • 하루짜리 토큰 생성
    - 60(초) * 60(분) * 24(시간) = 60 * 60 * 24 (second)
    - 60 * 60 * 24 * 1000 (millisecond)

AuthenticationFilter

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;
    }
    ...   
    /**
     * 로그인 성공 후 토큰(jwt)을 생성한다.
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        String username = ((User)authResult.getPrincipal()).getUsername(); // 이때 username 는 사용자 email 이다. 
        UserDto userDto = userService.getUserDetailsByEmail(username); // 🌟 email 이 아닌 userId(=UUID)를 이용해 토큰을 생성하기 위해서 userDto 를 받아온다.

        String token = Jwts.builder() // 🌟 토큰 생성
            .setSubject(userDto.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의 헤더에 토큰 추가
        response.addHeader("userId", userDto.getUserId()); // 🌟 토큰의 위변조 확인을 위해 userId도 추가
    }
}

UserService

UserService.java

public interface UserService extends UserDetailsService {
    ...
    UserDto getUserDetailsByEmail(String email);
}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    UserRepository userRepository;
    BCryptPasswordEncoder passwordEncoder;

    @Autowired
    public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) { // BCryptPasswordEncoder -> UserServiceApplication 에 빈 등록
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }
    ...
    @Override
    public UserDto getUserDetailsByEmail(String email) {
        UserEntity userEntity = userRepository.findByEmail(email);

        if (userEntity == null) {
            throw new UsernameNotFoundException("email 정보가 없습니다.");
        }

        return new ModelMapper().map(userEntity, UserDto.class); // 🌟 userEntity 를 UserDto 형태로 변환하여 반환한다.
    }
}

WebSecurity

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    private final UserService userService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;
    ...
    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter
            = new AuthenticationFilter(authenticationManager(), userService, env);

        return authenticationFilter;
    }
}

테스트

아래 사진처럼, 200 OK를 반환하고, 헤더에 tokenuserId 값이 담겨있는 것을 확인할 수 있다.
스크린샷 2022-10-03 오후 3 29 20

gateway-service

pom.xml

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- API, java.xml.bind module -->
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.2</version>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.2</version>
</dependency>

application.yml

게이트웨이에서는 expiration_time은 필요없다.

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - 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 # 🌟 로그인과 회원가입 api를 제외한 나머지 api에 인증 필터를 추가한다.

...

token:
  secret: user_token # 🌟 토큰 secret 추가

AuthorizationHeaderFilter

filter/AuthorizationHeaderFilter.java

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

    Environment env;

    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class); // 부모의 생성자를 호출해 현재 가지고 있는 Config 정보를 전달
        this.env = env;
    }

    public static class Config {
        // Put configuration properties here
    }

    @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(org.springframework.http.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(); // subject 추출
        } 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();
    }
}

테스트

아래와 같이 토큰을 넣고 요청하니 성공적으로 실행되었다.
스크린샷 2022-10-03 오후 5 34 53

gateway-service에 아래와 같은 에러메시지가 뜬다면?

javax/xml/bind/DatatypeConverter
스크린샷 2022-10-03 오후 5 13 27

구글링해보니 아래 코드를 추가하라고 나와있는데, 나는 아래 코드를 추가해도 해결되지 않았다.

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
</dependency>

위 코드를 제거하고 아래 코드를 추가하니 해결되었다.

<!-- API, java.xml.bind module -->
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.2</version>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.2</version>
</dependency>

https://stackoverflow.com/questions/43574426/how-to-resolve-java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기