1. Controller
@PostMapping("/login")
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
//login service 구현( JWT )
return ApiResponse.success(accountManager.login(request.toCommand()));
}
@PostMapping("/signup")
public ApiResponse<SignupResponse> signup(@Valid @RequestBody SignupRequest request) {
//signup service 구현
return ApiResponse.success(accountManager.signup(request.toCommand()));
}
@PostMapping("/logout")
public ApiResponse<Boolean> logout(@RequestHeader("RefreshToken") String refreshToken) {
return ApiResponse.success(accountManager.logout(refreshToken));
}
@PostMapping("/reissue")
public ApiResponse<String> reissue(@RequestHeader("RefreshToken") String refreshToken) {
return ApiResponse.success(accountManager.reissue(refreshToken));
}
Facade Pattern을 이용하여 구현하였습니다. Facade 패턴을 사용한 이유는, UserService의 복잡도를 줄이기 위해서 사용하였습니다.
2. Service
1) 회원가입
private final JpaAccountRepository accountRepository;
private final PasswordEncryptor encryptor;
public Account register(SignupCommand command) {
if (accountRepository.existsByEmail(command.getEmail())) {
throw new DuplicatedEmailException();
}
String EncryptedPassword = encryptor.encoder(command.getPassword());
Account account = Account.register(
command.getEmail(),
EncryptedPassword,
command.getName(),
command.getAge(),
command.getRole());
return accountRepository.save(account);
}
User에게 회원가입에 필요한 정보들을 전달받습니다.
보안을 위해 Password를 Jbcrypt 라이브러리를 이용하여 암호화하여 저장합니다.
//Encryptor
implementation 'org.mindrot:jbcrypt:0.4'
Controller에서 Service Layer로 인자를 전달할 때 SignupCommand로 변환하여 사용하고 있습니다.
SignupRequest -> SignupCommand로 변환하여 사용하는 이유는 Layer간의 책임을 확실히 하기 위해서입니다.
보통 Layer간의 이동 시 DTO를 사용합니다. Controller에서 사용하는 SignupRequest DTO가 Service layer에 그대로 사용하게 된다고 했을 때, Controller에 사용된 DTO가 Service Layer에서 사용하는 Repository까지 영향을 주게될 수도 있습니다. 그래서 Layer간의 종속성을 최소화 하기 위해 DTO를 변환하며 사용하였습니다.
2) 로그인
private final TokenGenerator tokenGenerator;
private final RedisRefreshTokenRepository tokenRepository;
public TokenResponse login(LoginCommand command) {
Account user = loginProcessor.login(command);
var tokens = tokenGenerator.generate(command.getEmail(), user.getRole());
tokenRepository.addRefreshToken(tokens.getRefreshToken());
return TokenResponse.from(tokens);
}
private final JpaAccountRepository accountRepository;
private final PasswordEncryptor encryptor;
public Account login(LoginCommand command) {
//로그인 서비스 구현
Account account = accountRepository.findByEmail(command.getEmail()).orElseThrow(() -> {
throw new EmailNotExistException();
});
if (!encryptor.checkPassword(command.getPassword(), account.getPassword())) {
throw new PasswordNotMatchException();
}
return account;
}
로그인 시 해당 User의 email와 password로 해당 유저의 validation 검사를 합니다.
로그인 시 Client에 AccessToken과 RefreshToken을 발급하고 RefreshToken은 AccessToken 재발급 시 사용하기 위해 Redis에 저장합니다.
3) 로그아웃
private final JwtTokenExtractor tokenExtractor;
private final RedisRefreshTokenRepository tokenRepository;
public Boolean logout(String refreshToken) {
var rt = tokenExtractor.extract(refreshToken).orElseThrow(IllegalArgumentException::new);
return tokenRepository.deleteRefreshToken(rt);
}
로그아웃 시 Client가 가지고 있는 RefreshToken을 Redis에서 삭제하여 해당 RefreshToken을 사용하지 못하도록 합니다.
❓RefreshToken을 지우더라도 AccessToken으로 인증/인가가 진행되지 않나요?
로그아웃하여 해당 RefreshToken이 삭제 되더라도 유효한 AccessToken을 탈취하여 사용하면 악의적으로 사용할 수 있게 됩니다. 해당 상황을 막기위해서는 두 가지 방법이 있습니다.
1) Access Token의 Expiration Time을 줄여 탈취가 될 확률을 줄이거나 탈취가 되더라도 사용할 수 있는 시간을 제한합니다.
2) AccessToken을 Blacklist로 등록합니다. 로그아웃 시 AccessToken을 저장소에 Blacklist로 추가하여 해당 AccessToken의 유효 검사를 Filter에서 진행하여 접근을 막습니다. 그리고 저장소에 저장할 때 Expiration 시간을 정하여 시간이 지나면 자동 삭제하도록 하면 됩니다 (Redis가 가장 좋아 보입니다!)
4) 토큰 재발급
private final RedisRefreshTokenRepository tokenRepository;
private final TokenGenerator tokenGenerator;
private final JwtTokenExtractor tokenExtractor;
private final JwtTokenParser tokenParser;
public String reissue(String refreshToken) {
var token = tokenExtractor.extract(refreshToken).orElseThrow(() -> {
throw new IllegalArgumentException();
});
if (!tokenRepository.existsRefreshToken(token)) {
throw new RefreshTokenNotExistsException();
}
AuthenticatedAccount user = tokenParser.parse(token);
Set<Role> role = user.getRole().stream().map(Role::valueOf).collect(Collectors.toSet());
return tokenGenerator.generateAccessToken(user.getUserId(), role);
}
Client에게 전달받은 RefreshToken을 검사하여 RefreshToken에 대한 유효검사를 하고, 정상이라면 AccessToken을 재발급 하는 로직입니다.
Redis 설정
@Repository
@RequiredArgsConstructor
public class RedisRefreshTokenRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final String PREFIX = "RT:";
public void addRefreshToken(String refreshToken) {
String key = PREFIX + refreshToken;
redisTemplate.opsForValue().set(key, refreshToken);
redisTemplate.expire(key, 12, TimeUnit.HOURS);
}
public Boolean deleteRefreshToken(String refreshToken) {
String key = PREFIX + refreshToken;
return redisTemplate.unlink(key);
}
public Boolean existsRefreshToken(String refreshToken) {
String key = PREFIX + refreshToken;
return redisTemplate.hasKey(key);
}
}
@Configuration
public class RedisConfiguration {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
// config.setPassword(RedisPassword.of(redisPassword));
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
Redis의 대한 설정은 위와 같이 사용하였습니다.
'프로젝트 > E-Commerce' 카테고리의 다른 글
[4] AccountService (2) SpringSecurity (0) | 2023.04.07 |
---|---|
[3] AccountService (1) 설계 (0) | 2023.04.02 |
[2] System Architecture (0) | 2023.03.31 |
(1) LastOrder Proejct 개요 (0) | 2023.03.31 |