Project/2023-02 캡스톤디자인
[Spring Security] SpringBoot 3.1.x Redis 설정 (with JWT)
48965
2024. 4. 8. 16:11
SpringBoot에서 인증(Authentication) 및 인가(Authorization) 필터를 구현하면서 JWT를 도입하였고, Refresh 토큰의 저장소로 Redis를 채택하여 구현하려고 한다.
1. build.gradle 내부 redis 및 JWT 의존성 추가
// Redis
implementation group: 'org.springframework.data', name: 'spring-data-redis', version: '3.1.3'
implementation group: 'io.lettuce', name: 'lettuce-core', version: '6.2.6.RELEASE'
// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation group: 'org.json', name: 'json', version: '20231013'
사용 중인 SpringBoot의 버전과 호환성을 잘 고려해서 redis와 jwt의 버전 의존성을 추가해주면 된다.
2. application.yml 설정
# Redis 설정
spring:
data:
redis:
host: [ip address]
port: 6379
# JWT 설정
jwt:
secret: [시크릿 키 설정]
accessTokenExpiration: 7200000 # 2시간 (밀리초 단위)
refreshTokenExpiration: 259200000 # 3일 (밀리초 단위)
application.yml 파일에 redis 및 jwt 설정을 추가한다.
redis의 기본 포트 번호는 6379를 사용하며, host에는 redis가 사용중인 환경에서의 ip 주소를 기입해주면 된다.
jwt 설정에서는 사용할 시크릿 키 기입 후 엑세스 토큰의 만료시간과 리프레시 토큰의 만료시간을 설정해준다.
3. RedisConfig 클래스 생성
@Configuration
@EnableRedisRepositories
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory,
RedisSerializer<Object> springSessionDefaultRedisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(springSessionDefaultRedisSerializer);
return redisTemplate;
}
}
redisConnectionFactory() 메서드
- 해당 메서드는 Redis와의 연결을 관리하는 RedisConnectionFactory 인스턴스를 생성한다.
- LettuceConnectionFactory는 Lettuce 라이브러리를 사용하여 Redis 서버에 연결하는 팩토리이다.
redisTemplate() 메서드
- 해당 메서드는 Redis 연산을 수행하는 데 사용되는 RedisTemplate 인스턴스를 생성한다.
- RedisTemplate은 Redis 데이터 액세스 코드를 간소화하는 데 사용되며, 다양한 Redis 명령을 메서드로 제공한다.
- setConnectionFactory 메서드를 호출하여 RedisTemplate에 RedisConnectionFactory를 연결한다. 이를 통해 RedisTemplate이 Redis 연산을 수행할 때 해당 커넥션 팩토리를 사용하여 Redis 서버와 통신할 수 있게 된다.
- 여기서는 setKeySerializer와 setValueSerializer 메서드는 Redis에 저장되는 키와 값의 직렬화 방식을 설정하였다.
- Hash 데이터 구조를 사용할 경우 setHashKeySerializer와 setHashValueSerializer를 통해 해시 키와 값의 직렬화 방식도 설정할 수 있다.
- setDefaultSerializer는 모든 경우에 적용되는 기본 직렬화를 설정한다. 해당 코드에서는 모든 데이터 타입에 StringRedisSerializer를 사용한다.
4. TokenService 클래스 생성
@Slf4j
@Service
public class TokenService {
private final RedisTemplate<String, String> redisTemplate;
public TokenService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Value("${jwt.secret}")
private String jwtSecretKey;
private Key key;
@Value("${jwt.accessTokenExpiration}")
private long accessTokenExpiration;
@Value("${jwt.refreshTokenExpiration}")
private long refreshTokenExpiration;
private static final String JWT_TYPE = "JWT";
private static final String ALGORITHM = "HS256";
private static final String LOGIN_ID = "loginId";
private static final String USERNAME = "username";
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(jwtSecretKey.getBytes(StandardCharsets.UTF_8));
}
public String generateJwtAccessToken(UserDto userDto) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + accessTokenExpiration);
String accessToken = Jwts.builder()
.setHeader(createHeader())
.setClaims(createClaims(userDto))
.setSubject(String.valueOf(userDto.email()))
.setIssuer("user")
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(expireDate)
.compact();
return accessToken;
}
@Transactional
public String generateJwyRefreshToken(UserDto userDto){
Date now = new Date();
Date expireDate = new Date(now.getTime() + refreshTokenExpiration);
String refreshToken = Jwts.builder()
.setHeader(createHeader())
.setClaims(createClaims(userDto))
.setSubject(String.valueOf(userDto.email()))
.setIssuer("profile")
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(expireDate)
.compact();
// redis에 저장
redisTemplate.opsForValue().set(
userDto.email(),
refreshToken,
refreshTokenExpiration,
TimeUnit.MILLISECONDS
);
return refreshToken;
}
public boolean isValidToken(String token) {
try {
Claims claims = getClaimsFormToken(token);
return true;
} catch (ExpiredJwtException expiredJwtException) {
log.error("Token Expired", expiredJwtException);
return false;
} catch (JwtException jwtException) {
log.error("Token Tampered", jwtException);
return false;
} catch (NullPointerException npe) {
log.error("Token is null", npe);
return false;
}
}
private static Map<String, Object> createHeader() {
Map<String, Object> header = new HashMap<>();
header.put("typ", JWT_TYPE);
header.put("alg", ALGORITHM);
header.put("regDate", System.currentTimeMillis());
return header;
}
private static Map<String, Object> createClaims(UserDto userDto) {
// 공개 클래임에 사용자의 이름과 이메일을 설정해서 정보를 조회할 수 있다.
Map<String, Object> claims = new HashMap<>();
log.info("loginId : " + userDto.email());
log.info("username : " + userDto.name());
claims.put(LOGIN_ID, userDto.email());
claims.put(USERNAME, userDto.name());
return claims;
}
private Claims getClaimsFormToken(String token) {
return Jwts.parserBuilder().setSigningKey(key)
.build().parseClaimsJws(token).getBody();
}
public String getUserEmailFromToken(String token) {
Claims claims = getClaimsFormToken(token);
return claims.get(LOGIN_ID).toString();
}
public static String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}else{
throw new ProfileApplicationException(ErrorCode.AUTH_TOKEN_NOT_FOUND);
}
}
}
- init(): 클래스의 초기화 단계에서 호출되어 JWT 서명에 사용될 키를 설정한다.
- generateJwtAccessToken(UserDto userDto): 사용자 정보를 기반으로 JWT 엑세스 토큰을 생성하고 반환한다. 토큰은 사용자의 이메일을 주제(subject)로 사용하고, 지정된 만료 시간을 갖는다.
- generateJwyRefreshToken(UserDto userDto): 사용자 정보를 기반으로 JWT 리프레시 토큰을 생성하고, 이를 Redis에 저장한 후 토큰을 반환한다. 토큰은 사용자의 이메일을 주제로 사용하고, 엑세스 토큰과 다른 별도의 만료 시간을 갖게 된다.
- isValidToken(String token): 제공된 토큰이 유효한지 검사하는 메서드. 토큰의 만료, 변조 등을 확인하고, 상태에 따라 boolean 값을 반환한다.
- createHeader(): JWT 헤더를 생성합니다. 토큰의 타입과 알고리즘을 지정하는 역할을 수행한다.
- createClaims(UserDto userDto): 사용자 정보를 기반으로 JWT 클레임을 생성하는 메서드. 사용자의 이메일과 이름을 포함한 정보를 클레임으로 설정한다.
- getClaimsFormToken(String token): 토큰을 파싱하여 클레임을 추출하는 메서드. 해당 메서드는 토큰의 유효성 검사 후 클레임 정보를 반환한다.
- getUserEmailFromToken(String token): 토큰에서 사용자의 이메일 정보를 추출한다. 이는 클레임에서 loginId를 통해 얻어진다.
- getTokenFromRequest(HttpServletRequest request): HTTP 요청에서 'Authorization' 헤더를 통해 Bearer 토큰을 추출한다. 토큰이 존재하지 않거나 형식이 올바르지 않은 경우 예외를 발생시킨다.
여기까지 구성이 완료되었으면, redis 세팅이 완료되었으며 인증 과정에서 JWT를 사용할 수 있게 된 것이다.
다음 포스팅에서 이어서 인증 및 인가 과정 구현을 마무리 할 예정이다.