[E-commerce App] Users Microservice - 로그인 성공 처리 (JWT 생성)
저번에 만들었던 user-service
에 로그인 성공 처리 로직을 추가해보자.
개요
- 사용자가 입력한
email
과password
가AuthenticationFilter
에 전달되면,attemptAuthentication()
메서드가 가장 먼저 처리한다. - 그 다음으로, 사용자가 입력한
email
과password
를UsernamePasswordAuthenticationToken
의 형태로 바꿔서 사용한다. - 그리고
UserDetailService
를 구현하고 있는 클래스에서loadUserByUsername()
메서드가 실행된다. loadUserByUsername()
메서드 안에서findByEnail()
을 통해 db에서UserEntity
를 가져오고, 이를 Spring Security에 있는User
객체로 변경해서 사용한다.- 마지막으로, 모든 과정이 끝나고 정상적으로 로그인이 되었다면
AuthenticationFilter
에서successfulAuthentication()
메서드 내에서 토큰을 발행한다. - 그런데, 사용자
email
으로 토큰을 만들게 아니라,userId
(=UUID
)로 토큰을 만들고 싶기 때문에,
successfulAuthentication()
메서드 안에서getUserDetailsByEmail()
을 호출해 사용자 정보(UserDto
)를 가져온다. - 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
를 반환하고, 헤더에 token
과 userId
값이 담겨있는 것을 확인할 수 있다.
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();
}
}
테스트
아래와 같이 토큰을 넣고 요청하니 성공적으로 실행되었다.
gateway-service
에 아래와 같은 에러메시지가 뜬다면?
javax/xml/bind/DatatypeConverter
구글링해보니 아래 코드를 추가하라고 나와있는데, 나는 아래 코드를 추가해도 해결되지 않았다.
<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>
💛 개인 공부 기록용 블로그입니다. 👻