[Spring Cloud MSA] Spring Cloud를 통한 MSA 개발(8) - User Service 로그인
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, 사이트간 요청 위조)
- 지속적인 토큰 저장
서로 다른 마이크로서비스에서 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