반응형
본 내용은 인프런의 이도원 님의 강의 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)" 내용을 바탕으로 정리한 내용입니다.
RequestLogin 생성
package cohttp://m.example.UserService.vo;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 사용자 로그인 정보
*/
@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 and less than 16 characters")
private String password;
}
- 사용자 로그인 정보를 저장하기 위한 객체
UserRepository 수정
package cohttp://m.example.UserService.jpa;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CrudRepository<UserEntity, Long> {
UserEntity findByUserId(String userId);
UserEntity findByEmail(String username);
}
- 이메일로 사용자 정보 조회하는 메서드 추가
UserService 수정
package com.example.UserService.service;
import cohttp://m.example.UserService.dto.UserDto;
import cohttp://m.example.UserService.jpa.UserEntity;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
UserDto createUser(UserDto userDto);
UserDto getUserByUserId(String userId);
Iterable<UserEntity> getUserByAll();
}
- Spring Security에서 사용자의 인증과 관련된 데이터를 로드하기 위한 인터페이스UserDetailsService를 상속
UserServiceImpl 수정
package com.example.UserService.service;
import cohttp://m.example.UserService.dto.UserDto;
import cohttp://m.example.UserService.jpa.UserEntity;
import cohttp://m.example.UserService.jpa.UserRepository;
import cohttp://m.example.UserService.vo.ResponseOrder;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@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<>());
}
@Override
public UserDto createUser(UserDto userDto) {
// 랜덤으로 ID 생성
userDto.setUserId(UUID.randomUUID().toString());
ModelMapper mapper = new ModelMapper();
// 값들이 정확히 같을 때만 엔티티 클래스로 변환하도록 설정
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = mapper.map(userDto, UserEntity.class);
userEntity.setEncryptedPwd(bCryptPasswordEncoder.encode(userDto.getPwd()));
userRepository.save(userEntity);
UserDto returnUserDto = mapper.map(userEntity, UserDto.class);
return returnUserDto;
}
/**
* 사용자 ID로 사용자 정보 조회
* @param userId
* @return
*/
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if(userEntity == null) {
throw new UsernameNotFoundException("User not found");
}
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
List<ResponseOrder> orderList = new ArrayList<>();
userDto.setOrders(orderList);
return userDto;
}
/**
* 전체 사용자 조회
* @return
*/
@Override
public Iterable<UserEntity> getUserByAll() {
return userRepository.findAll();
}
}
- 사용자 정보를 반환하는 loadUserByUsername 메서드 구현
AuthenticationFilter
package cohttp://m.example.UserService.filter;
import cohttp://m.example.UserService.vo.RequestLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.ArrayList;
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/**
* 사용자 인증
* @param request
* @param response
* @return
* @throws AuthenticationException
*/ @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// request는 post 형태라 inputstream 으로 확인
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);
}
}
/**
* 인증성공 후 처리
* @param request
* @param response
* @param chain
* @param authentication
* @throws IOException
* @throws ServletException
*/ @Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response
, FilterChain chain, Authentication authentication) throws IOException, ServletException{
}
}
- 인증 필터 생성
WebSecurityConfig 수정
package cohttp://m.example.UserService.config;
import cohttp://m.example.UserService.filter.AuthenticationFilter;
import com.example.UserService.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import java.util.function.Supplier;
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private static final String ALLOWED_IP_ADDRESS_MATCHER = "127.0.0.1";
private final UserService userService;
private final Environment env;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final AuthenticationConfiguration authenticationConfiguration;
/**
* 요청에 대한 인증 처리
* 특정 ip이외에서 접속할 경우 모든 요청은 인증이 필요하다.
* @param http
* @return
* @throws Exception
*/ @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/**").access(this::hasIpAddress)// 여기에 특정 IP 주소를 지정
.anyRequest().authenticated()
)
.addFilter(getAuthenticationFilter()) // getAuthenticationFilter() 메서드를 구현해야 합니다.
.csrf(CsrfConfigurer::disable); // CSRF 보호 비활성화
return http.build();
}
/**
* 요청한 IP와 접속을 허용하는 IP가 일치하는지 확인
* @param authentication
* @param object
* @return
*/
private AuthorizationDecision hasIpAddress(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return new AuthorizationDecision(
//!(authentication.get() instanceof AnonymousAuthenticationToken) &&
ALLOWED_IP_ADDRESS_MATCHER.equals(object.getRequest().getRemoteAddr())
);
}
/**
* 사용자 정의 인증 필터를 생성하고 인증관리자를 설정한다.
* @return
* @throws Exception
*/ private AuthenticationFilter getAuthenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter = new AuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
return authenticationFilter;
}
/**
* 사용자 서비스와 비밀번호 인코더 설정
* @param auth
* @throws Exception
*/ @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
}
}
GatewayService
application.yml 수정
routes:
# - id: UserService # 사용자 서비스
# uri: lb://UserService
# predicates:
# - Path=/userService/**
- id: UserService # 사용자 로그인
uri: lb://UserService
predicates:
- Path=/userService/login
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/userService/?(?<segment>.*), /$\{segment} # /userService 패턴이 들어오면 제거해서 호출
- id: UserService
uri: lb://UserService
predicates:
- Path=/userService/users
- Method=POST
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/userService/?(?<segment>.*), /$\{segment} # /userService 패턴이 들어오면 제거해서 호출
- id: UserService
uri: lb://UserService
predicates:
- Path=/userService/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/userService/?(?<segment>.*), /$\{segment} # /userService 패턴이 들어오면 제거해서 호출
- userSerivce 패턴으로 서버를 호출할떄 userService를 제거하고 userService를 호출한다.
결과
![[Pasted image 20240624210800.png]]
- login을 구현하지 않아도 스프링 시큐리티를 사용하면 자동으로 인증을 진행한다.
- 200 OK 응답이 나오므로 로그인에 성공했다.
JWT 토큰 적용
기존 로그인 방식의 문제점
- 세션과 쿠키는 모바일 애플리케이션에서 유효하게 사용할 수 없음(공유 불가)
- 랜더링된 HTMI 페이지가 반환되지만 , 모바일 애플리케이션에서는 JSON과 같은 포맷이 필요하다.
JWT(Json Web Token)
- 인증 헤더 내에서 사용되는 토큰 포맷이다.
- 두 개의 시스템끼리 안전한 방법으로 통신이 가능하다.
- https://jwt.io 사이트에서 토큰 값 확인이 가능하다.
- 클라이언트의 독립적인 서비스(statelss)가 가능하다.
UserService 수정
pom.xml 추가
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
application.yml 추가
token:
expiration_time: 864000000
secret: my-256-bit-secret-key-which-is-long
JwtTokenProvider 생성
package com.example.UserService.security;
import cohttp://m.example.UserService.dto.UserDto;
import com.example.UserService.service.UserService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private final UserService userService;
// application.yml에서 값 가져오기
@Value("${token.secret}")
private String jwtSecret;
@Value("${token.expiration_time}")
private long jwtExpirationInMs;
/**
* response에 JWT 토큰 추가
* @param response
* @param authentication
* @throws IOException
*/ public void generateTokenAndAddToResponse(HttpServletResponse response, Authentication authentication) throws IOException {
User userDetails = (User) authentication.getPrincipal();
UserDto user = userService.getUserDetailsByEmail(userDetails.getUsername());
// JWT 토큰 생성
String token = generateToken(user.getUserId());
// HTTP 응답에 토큰과 사용자 ID를 헤더로 추가
response.addHeader("Authorization", "Bearer " + token);
response.addHeader("userId", user.getUserId());
}
/**
* JWT 토큰 생성
* @param userId
* @return
*/
public String generateToken(String userId) {
SecretKey secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationInMs))
.signWith(secretKey, SignatureAlgorithm.ES256)
.compact();
}
}
AuthenticationFilter 수정
package com.example.UserService.security;
import com.example.UserService.service.UserService;
import cohttp://m.example.UserService.vo.RequestLogin;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.util.ArrayList;
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private UserService userService;
private JwtTokenProvider jwtTokenProvider;
public AuthenticationFilter(UserService userService
, AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider){
this.userService = userService;
super.setAuthenticationManager(authenticationManager);
this.jwtTokenProvider = jwtTokenProvider;
}
/**
* 사용자 인증
* @param request
* @param response
* @return
* @throws AuthenticationException
*/ @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// request는 post 형태라 inputstream 으로 확인
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);
}
}
/**
* 인증성공 후 처리
* @param request
* @param response
* @param chain
* @param authentication
* @throws IOException
* @throws ServletException
*/ @Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response
, FilterChain chain, Authentication authentication) throws IOException, ServletException{
// JWT 토큰 생성 및 HTTP 응답에 추가
jwtTokenProvider.generateTokenAndAddToResponse(response, authentication);
}
}
GatewayService 수정
pom.xml 수정
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
- jwt 라이브러리 추가
AuthorizationHeaderFilter 생성
package cohttp://m.example.GatewayService.filter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.security.Key;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
// application.properties 또는 application.yml에서 값 가져오기
@Value("${token.secret}")
private String jwtSecret;
public AuthorizationHeaderFilter() {
super(Config.class);
}
/**
* 필터링 로직을 정의 Authorization 헤더에서 JWT 토큰을 추출하여 검증한다.
* @param config
* @return
*/
@Override
public GatewayFilter apply(Config config){
// Custom Pre Filter
return (exchange, chain) -> {
// ServletHttp 를 비동기 방식에서는 ServerHttp 로 사용한다.
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);
};
};
/**
* 에러 발생시 로그 출력
* @param exchange
* @param err
* @param httpStatus
* @return
*/
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
/**
* JWT 토큰의 유효성을 검사
* @param jwt
* @return
*/
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
SecretKey secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
try {
// jwtSecret을 사용하여 JWT 파서 설정
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
subject = claims.getSubject();
} catch (Exception ex) {
returnValue = false;
}
if(subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
public static class Config{
// Put the configuration properties
}
}
- JWT 토큰을 검증하는 커스텀 필터
application.yml
- id: UserService
uri: lb://UserService
predicates:
- Path=/userService/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/userService/(?<segment>.*), /$\{segment} # /userService 패턴이 들어오면 제거해서 호출
- AuthorizationHeaderFilter # 인증 필터 추가
token:
secret: my-256-bit-secret-key-which-is-long
- /userService/** 패턴의 url GET 방식 호출시 AuthorizationHeaderFilter를 통해 토큰을 인증
- token secret 키 추가
결과
![[Pasted image 20240629105205.png]]
- http://localhost:userService/login 호출 시 정상적으로 로그인됨
![[Pasted image 20240629104624.png]]
- http://localhost:8080/userService/users GET 방식 호출시 JWT 인증토큰이 없어 401 에러 나옴
![[Pasted image 20240629125925.png]]
- JWT 토큰 값을 넘겨 인증 받을 경우 url 정상 호출됨
반응형
'Cloud > MSA' 카테고리의 다른 글
[MSA] Spring Cloud Bus (0) | 2024.12.02 |
---|---|
[MSA] Spring Cloud Config (0) | 2024.12.02 |
[MSA] MicroService 구현(주문 서비스) (1) | 2024.11.27 |
[MSA] MicroService 구현(상품 서비스) (0) | 2024.11.27 |
[MSA] MicroService 구현(사용자 서버 기능 추가) (0) | 2024.11.26 |