Skip to content

Commit df5ac86

Browse files
author
HyungJooKim
committed
[:sparkles: feat] jwt 추가
1.회원가입, 로그인을 위한 jwt 관련 class 추가
1 parent 259d10c commit df5ac86

File tree

5 files changed

+291
-0
lines changed

5 files changed

+291
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.readydance.backend.jwt;
2+
3+
import com.readydance.backend.dto.UserPrincipal;
4+
import org.springframework.security.authentication.AuthenticationManager;
5+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.core.AuthenticationException;
8+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
9+
10+
import javax.servlet.FilterChain;
11+
import javax.servlet.http.Cookie;
12+
import javax.servlet.http.HttpServletRequest;
13+
import javax.servlet.http.HttpServletResponse;
14+
import java.io.IOException;
15+
import java.util.ArrayList;
16+
17+
/**
18+
* JWT를 이용한 로그인 인증
19+
* 로그인에 성공했을 시 User 정보로(payload 에 담아) JWT Token 을 생성하고 JWT 토큰을 응답 쿠키에 넣어준다.
20+
*/
21+
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
22+
23+
private final AuthenticationManager authenticationManager;
24+
25+
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
26+
super(authenticationManager);
27+
this.authenticationManager = authenticationManager;
28+
}
29+
30+
/**
31+
* 로그인 인증 시도
32+
*/
33+
@Override
34+
public Authentication attemptAuthentication(
35+
HttpServletRequest request,
36+
HttpServletResponse response
37+
) throws AuthenticationException {
38+
// 로그인할 때 입력한 username과 password를 가지고 authenticationToken를 생성한다.
39+
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
40+
request.getParameter("username"),
41+
request.getParameter("password"),
42+
new ArrayList<>()
43+
);
44+
return authenticationManager.authenticate(authenticationToken);
45+
}
46+
47+
/**
48+
* 인증에 성공했을 때 사용
49+
* JWT Token 을 생성해서 쿠키에 넣는다.
50+
*/
51+
@Override
52+
protected void successfulAuthentication(
53+
HttpServletRequest request,
54+
HttpServletResponse response,
55+
FilterChain chain,
56+
Authentication authResult
57+
) throws IOException {
58+
UserPrincipal user = (UserPrincipal) authResult.getPrincipal();
59+
String token = JwtUtils.createToken(user);
60+
// 쿠키 생성(쿠키이름 + 값)
61+
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, token);
62+
cookie.setMaxAge(JwtProperties.EXPIRATION_TIME); // 쿠키의 만료시간 설정 (현재 10분으로 설정해 놓음)
63+
cookie.setPath("/"); //사용할 수 있는 경로 설정 (root)
64+
response.addCookie(cookie); //응답에 쿠키를 넣는다.
65+
response.sendRedirect("/"); //redirection url 을 root 로 설정
66+
}
67+
68+
@Override
69+
protected void unsuccessfulAuthentication(
70+
HttpServletRequest request,
71+
HttpServletResponse response,
72+
AuthenticationException failed
73+
) throws IOException {
74+
response.sendRedirect("/login"); //실패 시 다시 login 페이지로 돌아갈 수 있도록 redirection 설정
75+
}
76+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.readydance.backend.jwt;
2+
3+
import io.jsonwebtoken.security.Keys;
4+
import org.springframework.data.util.Pair;
5+
6+
import java.nio.charset.StandardCharsets;
7+
import java.security.Key;
8+
import java.util.Map;
9+
import java.util.Random;
10+
11+
/**
12+
* JWT Key를 제공하고 조회
13+
*/
14+
public class JwtKey {
15+
/**
16+
* Kid-Key List 외부로 절대 유출되어서는 안되어 따라서 Key Rolling 적용
17+
* Key Rolling : Secret Key 를 여러개 사용하고 수시로 수정을 해주어서 안전한 상태로 유지
18+
* Secret Key 1개에 Unique Id (kid) 를 연결시켜 JWT 토큰을 만들 때 kid 를 포함하여 제공 하고 토큰을 해석할 때 kid 를 통해 signature 를 검증
19+
*/
20+
private static final Map<String, String> SECRET_KEY_SET = Map.of(
21+
"key1", "SpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFunSpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFun"
22+
);
23+
/*
24+
"key2", "GoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurity",
25+
"key3", "HelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurity"
26+
*/
27+
//Todo 알고리즘 추가해서
28+
private static final String[] KID_SET = SECRET_KEY_SET.keySet().toArray(new String[0]);
29+
private static Random randomIndex = new Random();
30+
31+
/**
32+
* 위 3가지 키 set 중에서 SECRET_KEY_SET 에서 랜덤한 KEY 를 가져온다.
33+
*
34+
* @return kid와 key Pair
35+
*/
36+
public static Pair<String, Key> getRandomKey() {
37+
String kid = KID_SET[randomIndex.nextInt(KID_SET.length)];
38+
String secretKey = SECRET_KEY_SET.get(kid);
39+
return Pair.of(kid, Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)));
40+
}
41+
42+
/**
43+
* kid 를 통해 Secret Key를 찾는다.
44+
*
45+
* @param kid kid
46+
* @return Key
47+
*/
48+
public static Key getKey(String kid) {
49+
//SECRET_KEY 를 통해 객체 생성
50+
String key = SECRET_KEY_SET.getOrDefault(kid, null);
51+
if (key == null)
52+
return null;
53+
//Secret_key 의 길이에 따라서 적절한 암호화 방식을 선택해줌
54+
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
55+
}
56+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.readydance.backend.jwt;
2+
3+
/**
4+
* JWT 기본 설정값
5+
*/
6+
public class JwtProperties {
7+
public static final int EXPIRATION_TIME = 600000; // 10분
8+
public static final String COOKIE_NAME = "JWT-AUTHENTICATION";
9+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.readydance.backend.jwt;
2+
3+
import com.readydance.backend.dto.UserPrincipal;
4+
import io.jsonwebtoken.*;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.data.redis.core.RedisTemplate;
8+
import org.springframework.data.util.Pair;
9+
import org.springframework.stereotype.Service;
10+
11+
import javax.servlet.http.HttpServletRequest;
12+
import java.security.Key;
13+
import java.util.Date;
14+
15+
// Json 웹 토큰을 생성하고 확인
16+
@Service
17+
@Slf4j
18+
public class JwtUtils {
19+
20+
private Pair<String, Key> accessTokenSecret = JwtKey.getRandomKey();;
21+
22+
private Pair<String, Key> refreshTokenSecret = JwtKey.getRandomKey();;
23+
24+
25+
private static final String HEADER_NAME = "Authorization";
26+
27+
@Autowired
28+
RedisTemplate<String, Object> redisTemplate;
29+
30+
public enum TokenType { ACCESS_TOKEN, REFRESH_TOKEN }
31+
32+
/**
33+
* 토큰에서 username 찾기
34+
*
35+
* @param token 토큰
36+
* @return username
37+
*/
38+
public static String getUsername(String token) {
39+
// parsing 을 통해 jwtToken 에서 username 을 찾는다.
40+
return Jwts.parserBuilder()
41+
//secret key 를 꺼내와 검증 실패 시 SignatureException 발생, 만료 시 ExpiredJwtException 발생
42+
.setSigningKeyResolver(SigningKeyResolver.instance)
43+
.build()
44+
.parseClaimsJws(token)//토큰을 주입(파싱)
45+
.getBody()
46+
.getSubject(); // username
47+
}
48+
49+
/**
50+
* user로 토큰 생성
51+
* HEADER(JWT 를 검증하는데 필요한 정보를 가진 객체) : alg(signature 에 사용한 암호화 알고리즘), kid
52+
* PAYLOAD(실질적으로 인증에 필요한 데이터를 저장) : sub(유저 네임), iat(토큰 발행 시간), exp(토큰 만료 시간)
53+
* 인증할 떄 payload 에 있는 username 을 가져와서 조회할 때 사용한다.
54+
* SIGNATURE(jwt token 이 올바른지에 대한 일종의 서명) : JwtKey.getRandomKey 로 구한 Secret Key 로 HS512 해시(암호화)
55+
* signature 는 header 와 payload 를 합친 뒤 비밀키로 hash 를 생성하여 암호화 한다.
56+
* @param user 유저
57+
* @return jwt token
58+
*/
59+
public static String createToken(UserPrincipal user) {
60+
Claims claims = Jwts.claims().setSubject(user.getUsername()); // subject
61+
Date now = new Date(); // 현재 시간
62+
Pair<String, Key> key = JwtKey.getRandomKey();
63+
// JWT Token 생성
64+
return Jwts.builder()
65+
.setClaims(claims) // subject 정보 저장
66+
.setIssuedAt(now) // 토큰 발행 시간 정보 저장
67+
.setExpiration(new Date(now.getTime() + JwtProperties.EXPIRATION_TIME)) // 토큰 만료 시간 설정 저장
68+
.setHeaderParam(JwsHeader.KEY_ID, key.getFirst()) // 헤더 설정 key.getFirst() : kid
69+
.signWith(key.getSecond()) // signature
70+
.compact();
71+
}
72+
73+
// Access Token 발급
74+
public String generateAccessToken(UserPrincipal userPrincipal) {
75+
return createToken(userPrincipal);
76+
}
77+
78+
// Refresh Token 발급
79+
public String generateRefreshToken(UserPrincipal userPrincipal) {
80+
return createToken(userPrincipal);
81+
}
82+
83+
// Request의 Header에서 token 파싱
84+
public String extractToken(HttpServletRequest request) {
85+
return request.getHeader(HEADER_NAME);
86+
}
87+
88+
// Jwt 토큰 유효성검사
89+
public boolean validateToken(String token) {
90+
Pair<String, Key> secretKey = accessTokenSecret;
91+
try {
92+
log.debug("validateToken's secretKey : " + secretKey);
93+
// 1. setSigningKey를 통해 디지털 서명되었는지를 확인한다.
94+
Jws<Claims> claims = Jwts.parser()
95+
.setSigningKey(secretKey.getSecond())
96+
.parseClaimsJws(token);
97+
// 2. 만료일자가 지났는지 확인한다.
98+
boolean isNotExpire = !claims.getBody().getExpiration().before(new Date()); // 만료되면 false를 반환
99+
// 3. 블랙리스트인지 확인한다.
100+
if (redisTemplate.opsForValue().get(token) != null) { // 블랙리스트에 access token이 존재할 경우
101+
log.info("이미 로그아웃 처리된 사용자입니다.");
102+
return false;
103+
}
104+
return isNotExpire;
105+
} catch (Exception e) {
106+
return false;
107+
}
108+
}
109+
110+
public Date getExpirationDate(String token, TokenType tokenType) {
111+
Pair<String, Key> secretKey;
112+
113+
if (tokenType == TokenType.ACCESS_TOKEN) {
114+
secretKey = accessTokenSecret;
115+
} else {
116+
secretKey = refreshTokenSecret;
117+
}
118+
119+
log.debug("getExpirationDate's secretKey : " + secretKey);
120+
return Jwts.parser()
121+
.setSigningKey(secretKey.getSecond())
122+
.parseClaimsJws(token)
123+
.getBody()
124+
.getExpiration();
125+
}
126+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.readydance.backend.jwt;
2+
3+
import io.jsonwebtoken.Claims;
4+
import io.jsonwebtoken.JwsHeader;
5+
import io.jsonwebtoken.SigningKeyResolverAdapter;
6+
7+
import java.security.Key;
8+
9+
/**
10+
* JwsHeader 를 통해 Signature 검증에 필요한 Key 를 가져오는 코드를 구현
11+
*/
12+
public class SigningKeyResolver extends SigningKeyResolverAdapter {
13+
public static SigningKeyResolver instance = new SigningKeyResolver();
14+
15+
@Override
16+
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
17+
//header 에서 kid 를 찾고 kid 로 secret key 를 가져옴
18+
//즉 secret key 를 꺼내와 검증을 할 수 있게 도와주는 역할
19+
String kid = jwsHeader.getKeyId();
20+
if (kid == null)
21+
return null;
22+
return JwtKey.getKey(kid);
23+
}
24+
}

0 commit comments

Comments
 (0)