🙏 API 명세서와 기능 구현
- 회원 로그인을 Security를 통해 진행한다. 또한 로그인을 성공하면 토큰과 userId를 반환받을 예정이다. 여기서 userId는 전달하면 안되는데 체크를 위해 가져오는 것이다.
🙏 AuthenticationFilter 추가
▶ 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 = 8, message = "Password must be equal or grater than 8 characters")
private String password;
}
- 오로지 로그인을 위해 전달 받는 객체로 제한 조건을 넣어준 것을 볼 수 있다.
▶ AuthenticationFilter
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private UserService userService;
private Environment env;
public AuthenticationFilter(AuthenticationManager authenticationManager,
UserService userService, Environment env) {
super(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);
}
}
- UsernamePasswordAuthenticationFilter 를 상속받아 로그인 인증을 진행하는 필터를 생성한다.
- 그 과정에 attemptAuthentication 을 Override 해야한다. 여기서 들어오는 데이터를 UsernamePasswordAuthenticationToken에 데이터를 담아 비교하려는 객체에 담는다.
- 해당 토큰은 AuthenticationManager에 전달되고 Provider 에서 전달 받은 DB의 사용자 정보와 비교한다.
▶ UserService
public interface UserService extends UserDetailsService {
UserDto createUser(UserDto userDto);
UserDto getUserByUserId(String userId);
Iterable<UserEntity> getUserByAll();
UserDto getUserDetailsByEmail(String userName);
}
- UserDetailsService 를 상속받아 현재 DB의 유저 데이터를 비교하기 위해 loaduserbyusername를 구현해준다.
▶ UserserviceImpl ( loadUserByUsername() 구현 )
@Service
public class UserServiceImpl implements UserService{
...
@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<>());
}
- User 객체에 현재 등록된 사용자의 email 과 암호화된 비밀번호를 가져오는 것을 볼 수 있다. ArrayList 는 현재 사용자의 권한을 나타낸다. 이 객체는 추후에 Manager에서 비교할 객체로써 1차로 담기는 것이다.
▶ WebSecurity 에 등록된 사용자 인증 로직 제작
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
....
http.addFilter(getAuthenticationFilter(authenticationManager));
}
private AuthenticationFilter getAuthenticationFilter(AuthenticationManager authenticationManager) throws Exception{
return new AuthenticationFilter(authenticationManager, userService, env);
}
- filterChain에 우리가 사용하고자 하는 비교 객체(userService)를 등록해 주어야 한다. Filter에서는 Manager에 UsernamePasswordAuthenticationToken을 비교할 Manager를 전달해주어야 하는데 이를 filterchain에서 구현해주는 것을 확인 할 수 있다.
- builder 를 통해 manager의 구성을 진행하고 비교에 사용할 userService 를 등록하고 비밀번호의 암호화를 위해 bCrypt를 넣어서 manager를 완전히 build 해주는 것을 볼 수 있다.
- 그리고 getAuthentication 메소드에 해당 manager를 전달해주어 필터를 작동하게 한다.
🙏 Route 정보 변경 (API Gateway)
▶ API Gateway 의 yml 파일 설정
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}
- yml 파일의 설정을 바꿔주는 것을 볼 수 있다.
- RemoveRequestHeader=Cookie : POST로 전달되어 오는 값을 매번 새로운 데이터처럼 인식을 하기 위해 값을 초기화 한다.
- RewritePath=/user-service/(?<segment>.*), /$\{segment} : uri 에 user-service가 서비스에 같이 들어가는 것 때문에 서비스에서 따로 처리를 해주어야했지만 해당 설정을 통해 user-service를 제외한 나머지만을 서비스로 요청보내는 것을 확인 할 수 있다. 이로 인해 Controller 의 requestMapping에 값을 "/" 로 변경해도 문제가 없다
🙏 로그인 시 JWT 토큰 제공
▶ JWT 를 사용하는 이유
전통적인 인증 시스템
기존의 웹앱은 클라이언트한테 보여주는 웹앱의 기술들은 서버단에 구현되어 있는 기술이다. 모바일 기기가 유명해지면서 별도의 실행환경과 개발환경이 많이 변화되었다. 또한 서로의 연동에 의해 다양한 개발 환경을 신경써야 하는 반면 세션과 쿠키를 각자의 환경에 맞추기가 쉽지 않다는 것이다.
- 장점
- 클라이언트의 독립적인 서비스가 가능 (Stateless)
- CDN을 통해 중간에 캐시 서버를 둔다
- 쿠키와 세션의 사용이 줄어들면서 CSRF의 위험이 줄어들었다
▶ 성공 로직 제작
@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);
byte[] secretKeyBytes = Base64.getEncoder().encode(env.getProperty("token.secret").getBytes());
SecretKey secretKey = Keys.hmacShaKeyFor(secretKeyBytes);
String token = Jwts.builder()
.subject(userDetails.getUserId())
.expiration(Date.from(now().plusMillis(Long.parseLong(env.getProperty("token.expiration_time")))))
.issuedAt(Date.from(now()))
.signWith(secretKey)
.compact();
response.addHeader("token", token);
response.addHeader("userId", userDetails.getUserId());
}
- 비교가 완료되어 사용자의 정보 인증이 완료되었기 때문에 User의 토큰이 담긴 Authentication객체의 데이터를 뽑아 JWT 토큰에 담아두는 것을 볼 수 있다.
▶ JWT 라이브러리 및 설정 추가
- jsonwebtoken 관련 라이브러리를 추가해준다
- 토큰 설정 정보를 추가해주었다. JWT의 secretkey를 설정하면서 512바이트 길이를 사용하기위해 임의의 긴 단어를 설정해주어야 한다.
token:
expiration_time: 86400000
secret: dfe4a993c087c80b0ee8f11509e8c6712e8949592....
🙏 API Gateway의 Authorization 처리
- 유저 서비스에서 시큐리티에 의해 인증 정보를 처리하면서 토큰을 발급하게 되는데 해당 토큰은 추후에 유저뿐 아니라 다른 서비스에서도 사용하게 된다. 때문에 각각의 서비스에서 모두 인가를 확인하는 것 보다 중간 단계인 API Gateway에 필터를 달아 특정 요청에 Filter 를 등록하여 인가처리를 진행한다.
▶ JWT 라이브러리 및 설정 추가
- JWT로 인증과 인가를 진행하기 때문에 API Gateway또한 라이브러리를 등록해주어야 사용할 수 있다.
▶ AuthorizationHeaderFilter
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private Environment env;
public static class Config { }
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "No authrization 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) {
byte[] secretKeyBytes = Base64.getEncoder().encode(env.getProperty("token.secret").getBytes());
SecretKeySpec signingKey = new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS512.getJcaName());
boolean returnValue = true;
String subject = null;
try {
JwtParser jwtParser = Jwts.parserBuilder()
.setSigningKey(signingKey)
.build();
subject = jwtParser.parseClaimsJws(jwt).getBody().getSubject();
} catch (Exception e) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
// Mono, Flux -> Spring 5.0 의 새로운 프레임워크 WebFlux 이다.
// 단일 : Mono, 다중 : Flux
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
- apply 메소드
- 기존의 GatewayFilter 처럼 작성을 해주어야 Filter의 기능을 진행한다. 거기 안에서 토큰의 인증을 진행한다.
- onError를 통해 요청의 헤더에 토큰이 존재하는 지 확인한다
- 토큰에는 Bearer 가 붙어있기 때문에 인증을 위해 없애고 토큰의 확인을 위해 isJwtValid를 진행한다.
- isJwtValid 메소드
- 전달받은 토큰을 secretkey를 통해 해독하고 안에 있는 사용자의 정보를 subject 에 담는다
- 만약 토큰의 정보가 불안정하다면 onError를 진행한다.
- onError 메소드
- 토큰이 존재하지 않기 때문에 서비스까지 들어가지 못하고 바로 반환하는 것을 확인 할 수 있다.
- status 코드를 401 을 통해 인증불가로 전달한다.
Mono 와 Flux
Gateway의 구성은 Spring MVC 패턴과는 딱봐도 거리가 멀어보이는 것을 확인 할 수 있다.
따라서 HttpServlet 을 사용하는 MVC가 아닌 Spring Web Flux를 사용함으로 비동기 방식을 사용한다
비동기 처리 방식에서는 데이터를 처리하는 방법이 2가지가 존재한다
● Mono - 단일값
● Flux - 다중값
▶ API Gateway 의 yml파일
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter
- filter의 마지막 부분을 보면 AuthorizationHeaderFilter라는 필터를 등록한것을 볼 수 있다. 이를 통해 해당 요청은 필터를 지나쳐야 한다라고 등록하게 되는데 중요한것은 회원가입과 로그인의 과정에서는 토큰을 받을 수 없다 때문에 해당 요청과 관련된 route 설정은 AuthorizationFilter를 등록하지 않는다.
'MSA > MSA 강좌 - 이도원 강사님' 카테고리의 다른 글
👨👧👦8. 설정 정보와 암호화 처리 (Encryption 과 Decryption) (1) | 2024.04.03 |
---|---|
👨👧👦6. Spring Cloud Config (0) | 2024.04.01 |
👨👧👦 4. User & Catalogs & Orders Microservice (0) | 2024.03.29 |
👨👧👦 3. UserMicroservice ( 사용자 서비스 제작 ) - 1 (0) | 2024.03.28 |
👨👧👦 2. API Gateway Service (feat. Spring Cloud gateway) (0) | 2024.03.27 |