filters:
RemoveRequestHeader=Cookie
RewirtePath=/user-service/(?<segment>.*) , /$\{segment}
위에서 구현할 클래스들을 직접 구현해보자.
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public AuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin creds = new ObjectMapper().readValue(request.getInputStream() , RequestLogin.class);
// 인증 정보로 만들기 UsernamePasswordAuthenticationToken 으로 변경해야한다.
// 사용자가 입력한 이메일,비밀번호 값을 스프링 시큐리티에서 사용할 수 있는 값으로 변경하기 위한 것이다.
// 바꾼후 Manager 에게 그 객체를 넘긴다.
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(creds.getEmail() , creds.getPassword(),new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// WebSecurity.class
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
// Manager 에다가 userDetailsService , passwordEncoder 넣어준다.
// pwd 필드도 bCryptPasswordEncoder 되서 들어간다.
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
http.csrf( (csrf) -> csrf.disable());
http.authorizeHttpRequests((authz) -> authz
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll()
).authenticationManager(authenticationManager);
http.addFilter(getAuthenticationFilter(authenticationManager));
http.headers((headers) -> headers.frameOptions((frameOptions) -> frameOptions.sameOrigin()));
return http.build();
}
// input_pwd 와 db_pwd 를 비교하기 위해 encrypted 를 하고 비교해야한다.
private AuthenticationFilter getAuthenticationFilter(AuthenticationManager authenticationManager) {
return new AuthenticationFilter(authenticationManager);
}
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
// Manager 에다가 userDetailsService , passwordEncoder 를 등록한다.
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder);
이때 현재 우리가 만든 UserService 는 UserDetalisService 를 상속받지 않았기 때문에 컴파일 에러가 발생하기 때문에 UserService 에 UserDetalisService 를 상속하고 loadUserByUsername() 메소드를 구현하면 된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// username == loginId 와 같다고 생각하면 된다. 예제에서는 email
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<>());
}
# apigateway-service.application.yml
spring:
application:
name: apigateway-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}
postman 으로 /user-service/users 로 회원가입을 하고 /user-service/login 을 해본 결과 정상적으로 동작하는 것을 확인했다.
JSON 으로 email , password 정보를 넣고 /login 을 실행
JWT 는 userId 를 가지고 발급할 예정이다.
사용자 인증을 위한 검색 메소드를 추가한다. (Repository)
-> Email 로 User 객체를 찾는 메소드
JWT 에서 설정해야되는게 어떠한 내용으로 토큰을 만들것인지를 정해야한다.(setSubject())
token 과 userId 를 response 헤더에 넣는다.
@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);
}
<!-- jjwt 라이브러리 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version> <!-- 최신 버전 확인 필요 -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version> <!-- 최신 버전 확인 필요 -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version> <!-- 최신 버전 확인 필요 -->
<scope>runtime</scope>
</dependency>
token:
expiration_time: 86400000
secret: user_tokenuser_tokenuser_tokenuser_tokenuser_tokenuser_tokenuser_tokenuser_tokenuser_tokenuser_token
@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()
// userId 로 token 을 만들겠다.
.setSubject(userDetails.getUserId())
// 현재 시간에서 yml 파일에 있는 시간을 더한 시간까지 만료일 설정
.setExpiration(Date.from(now().plusMillis(Long.parseLong(env.getProperty("token.expiration_time")))))
.signWith(secretKey)
.compact();
response.addHeader("token" , token);
response.addHeader("userId" , userDetails.getUserId());
}
POSTMAN 으로 해본 결과 token 이 잘 발급되고 header 에 포함되서 넘어온 것을 확인할 수 있다.
문제점
1. 세션과 쿠키는 모바일 애플리케이션에서 유효하게 사용할 수 없다.
2. 렌더링된 HTML 페이지가 반환되지만, 모바일 애플리케이션에서는 JSON 과 같은 포멧이 필요하다.
문제를 생각해보면 모바일 app 을 개발할 때 많이 쓰는 React 기술이 있다.
인증 정보에 아무것도 전달하지 않게 되면 401 오류를 발생하도록 한다.
추가할 내용은 apigateway-service 에다가 JWT 토큰의 유효성을 검사할 수 있는 기능을 추가한다.
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {
}
@Override
public GatewayFilter apply(Config config) {
// login -> token -> 해당 token 을 가지고 요청 -> 그 token 은 header 에 포함
return (exchange, chain) -> {
// exchange 에서 request , response 를 얻을 수 있다.
ServerHttpRequest request = exchange.getRequest();
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
// 에러를 반환
return onError(exchange , "No authorization header" , HttpStatus.UNAUTHORIZED);
}
// 이 안에 token 값이 있을 것
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
// Bearer 부분을 "" 로 없애면서 jwt 만 가져온다.
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;
// jwt.io 에서 token 으로 subject 를 알 수 있다.
String subject = null;
try {
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(signingKey).build();
// Subject 를 추출했고, 올바른 값인지 확인하면 된다.
subject = jwtParser.parseClaimsJws(jwt).getBody().getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject==null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
// Mono(단일값) , Flux(다중값) -> Spring WebFlux 에서 나오는 개념
// API 처리할 때 비동기 방식으로 처리
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
// Mono 타입으로 반환 -> setComplete()
return response.setComplete();
}
}
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter