jwt를 구현했다는 전제하에 redis를 활용하여 해보겠다.
쿠키세션에 저장소가 추가된다고 했는데 결국 redis도 저장소를 써야된다.
다른곳에서 쓰던가 그 서버에 쓰던가 그 차이 인데 결국쓰긴 쓴다는 것이지 저장소를 쓰지 않는다는 말이 아니다. 기억해 두자
그러니 다른말로 하면 redis가 아닌 스프링에서 제공해주는 JpaRepository를 활용해서도 가능하고 컴퓨터에서 다른 데이터베이스를 활용해도 가능하다
private String grantType;
private String accessToken;
private String refreshToken;
private Long refreshTokenExpirationTime;
그 테이블에 저 4개의 컬럼을 가지고 있으면 redis대신에 사용이 가능하다.
key부분을 grantType으로 해주는정도?
프론트 없이 백엔드로만 하는것임..
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.1'
redis사용을 위한 의존성 부분으로
2번째는 redis를 도커나 추가없이 사용하기 위해서 넣어주는것이다.
@Configuration
@EnableRedisRepositories
public class RedisConfig {
private final RedisProperties redisProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
@Component
public class LocalRedisConfig {
private RedisServer redisServer;
@Value("${spring.data.redis.port}")
private int port;
@PostConstruct
public void redisServer() throws IOException {
redisServer = new RedisServer(port);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
LocalRedisConfig클래스의 경우 도커나 종합개발 환경(IDE)에 데이터베이스를 따로 만들어주지 않고 실행할때 실행시켜주는 클래스다.
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
private final RedisTemplate<String, Object> redisBlackListTemplate;
public void setRefreshToken(String key, Object o, Long minutes) {
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(o.getClass()));
redisTemplate.opsForValue().set(key, o, minutes, TimeUnit.MINUTES);
}
public Object getRefreshToken(String key) {
return redisTemplate.opsForValue().get(key);
}
public boolean deleteRefreshToken(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void setBlackList(String key, Object o, Long minutes) {
redisBlackListTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(o.getClass()));
redisBlackListTemplate.opsForValue().set(key, o, minutes, TimeUnit.MINUTES);
}
public boolean hasKeyBlackList(String key) {
return Boolean.TRUE.equals(redisBlackListTemplate.hasKey(key));
}
public Object getBlackList(String key) {
return redisBlackListTemplate.opsForValue().get(key);
}
public boolean deleteBlackList(String key) {
return Boolean.TRUE.equals(redisBlackListTemplate.delete(key));
}
}
key는 유저가 될수도 있고 access token이 될수도 있다.
넣는 사람이 무엇을 넣느냐에 따라 달라진다.
이 클래스를 통해 redis에
set을 이용해 값을 넣고 get을 이용해 가져오며
delete를 사용해 삭제하고 haskey를 사용해 있는지 확인하는
메소드들이다
boolean refreshToken = redisUtil.hasKeyBlackList(token);
if(refreshToken){
throw new IllegalArgumentException("Please Login again.");
}
토큰 검사하고 다음 부분에 추가해주면되는데 setAuthentication(SecurityContextHolder 에 넣기전에) 바로 전에 넣어줘서
이게 로그인 된것인지 로그아웃한 것인지 알수있게 해주면 된다.
public LoginResponseDto createToken(String username, UserRoleEnum role) {
long ACCESS_TOKEN_TIME = 60 * 30 * 1000L;
long REFRESH_TOKEN_EXPIRE_TIME = 2 * 24 * 60 * 60 * 1000L;
Date date = new Date();
//권한 가져오기
// BEARER : 인증 타입중 하나로 JWT 또는 OAuth에 대한 토큰을 사용 (RFC 6750 문서 확인)
String accessToken = BEARER_PREFIX + Jwts.builder()
.setSubject(username) // 토큰 용도
.claim(AUTHORIZATION_KEY, role) // payload에 들어갈 정보 조각들
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME)) // 만료시간 설정
.setIssuedAt(date) // 토큰 발행일
.signWith(key, signatureAlgorithm) // key변수 값과 해당 알고리즘으로 sign
.compact(); // 토큰 생성
String refreshToken = Jwts.builder()
.setExpiration(new Date(date.getTime() + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key,signatureAlgorithm)
.compact();
return LoginResponseDto.builder()
.grantType(BEARER_PREFIX)
.accessToken(accessToken)
.refreshToken(refreshToken)
.refreshTokenExpirationTime(REFRESH_TOKEN_EXPIRE_TIME)
.build();
}
ACCESS_TOKEN과 같이 넣어주지 말고 따로 해줘도 상관없다.
public MessageResponseDto login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
//이름과 유저인지 관리자인지 구분한 토큰을 가져오는 부분
LoginResponseDto msg = userService.login(loginRequestDto);
//문자열 token에 가져온 정보를 넣어주는 부분
String token = msg.getAccessToken();
//헤더를 통해 토큰을 발급해 주는 부분
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
return new MessageResponseDto("로그인 되었습니다.");
}
@DeleteMapping("/logout")
public MessageResponseDto logout(@AuthenticationPrincipal UserDetailsImpl userDetails
, HttpServletRequest request){
String accessToken = jwtUtil.resolveToken(request);
return new MessageResponseDto(userService.logout(accessToken, userDetails.getUsername()));
}
삭제하고 넣기도 하는데 삭제가 들어가니 DeleteMapping을 써주자
@Transactional
public String logout(String accessToken, String username) {
// if(redisUtil.hasKey(accessToken)){
// redisUtil.deleteRefreshToken(username);
// }
if(redisUtil.getRefreshToken(accessToken) != null){
redisUtil.deleteRefreshToken(username);
}
// 레디스에 accessToken 사용못하도록 등록
Long expiration = jwtUtil.getExpiration(accessToken);
redisUtil.setBlackList(accessToken, "logout", expiration);
return "로그아웃 완료";
}
취향에 따라서 has로 있는지 없는지 확인하던지 null인지 아닌지 확인하면된다.