Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f0776fc

Browse files
committed
feat: 인터셉터 JWT 헤더로 발급 처리, 추후 REDIS로 관리 추가
1 parent 13c3ce9 commit f0776fc

17 files changed

+394
-21
lines changed

‎member_request.http

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,27 @@ POST http://localhost:8081/members
33
Content-Type: application/json
44

55
{
6-
"email": "seok@gmail.com",
7-
"password": "1234",
8-
"name": "seokrae"
6+
"email": "seok@gmail.com",
7+
"password": "1234",
8+
"name": "seokrae"
99
}
1010

11+
### 로그인
12+
POST http://localhost:8081/login
13+
Content-Type: application/json
14+
15+
{
16+
"email": "seok@gmail.com",
17+
"password": "1234"
18+
}
1119

1220
### 사용자 수정
1321
PUT http://localhost:8081/members/1
1422
Content-Type: application/json
1523

1624
{
17-
"password": "1234",
18-
"name": "seok"
25+
"password": "1234",
26+
"name": "seok"
1927
}
2028

2129

@@ -25,6 +33,6 @@ DELETE http://localhost:8081/members/1
2533
Content-Type: application/json
2634

2735
{
28-
"password": "1234",
29-
"name": "seok"
36+
"password": "1234",
37+
"name": "seok"
3038
}

‎src/main/java/com/example/springwebjwtredis/aop/GlobalExceptionHandler.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
package com.example.springwebjwtredis.aop;
22

3-
import org.springframework.http.HttpStatus;
43
import org.springframework.http.ResponseEntity;
5-
import org.springframework.web.bind.annotation.ControllerAdvice;
6-
import org.springframework.web.bind.annotation.ExceptionHandler;
7-
import org.springframework.web.bind.annotation.ResponseStatus;
8-
import org.springframework.web.bind.annotation.RestControllerAdvice;
94

10-
@ControllerAdvice
115
public class GlobalExceptionHandler {
126

13-
@ExceptionHandler(Exception.class)
14-
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7+
// @ExceptionHandler(Exception.class)
8+
// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
159
public ResponseEntity<String> defaultExceptionHandler(Exception e) {
1610
return ResponseEntity.internalServerError().body(e.getMessage());
1711
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.example.springwebjwtredis.aop.auth;
2+
3+
import com.example.springwebjwtredis.aop.jwt.JwtUtil;
4+
import com.example.springwebjwtredis.domain.MemberDto;
5+
import com.example.springwebjwtredis.dto.RequestLoginMember;
6+
import com.example.springwebjwtredis.service.MemberService;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.http.HttpMethod;
11+
import org.springframework.util.StreamUtils;
12+
import org.springframework.web.HttpRequestMethodNotSupportedException;
13+
import org.springframework.web.servlet.HandlerInterceptor;
14+
import org.springframework.web.servlet.ModelAndView;
15+
16+
import javax.servlet.ServletInputStream;
17+
import javax.servlet.http.HttpServletRequest;
18+
import javax.servlet.http.HttpServletResponse;
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
22+
@Slf4j
23+
public class AuthenticationInterceptor implements HandlerInterceptor {
24+
25+
private final JwtUtil jwtUtil;
26+
private final ObjectMapper mapper = new ObjectMapper();
27+
@Autowired
28+
private MemberService memberService;
29+
30+
public AuthenticationInterceptor(JwtUtil jwtUtil) {
31+
this.jwtUtil = jwtUtil;
32+
}
33+
34+
private RequestLoginMember getBodyStream(HttpServletRequest request) throws IOException {
35+
ServletInputStream inputStream = request.getInputStream();
36+
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
37+
return mapper.readValue(messageBody, RequestLoginMember.class);
38+
}
39+
40+
@Override
41+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
42+
log.info("AuthenticationInterceptor : preHandle()");
43+
44+
if (!request.getMethod().equals(HttpMethod.POST.name())) {
45+
throw new HttpRequestMethodNotSupportedException("해당 요청을 제공하지 않습니다.");
46+
}
47+
48+
RequestLoginMember loginMember = getBodyStream(request);
49+
MemberDto memberDto = memberService.findMember(loginMember.getEmail(), loginMember.getPassword());
50+
51+
if (memberDto == null) {
52+
return false;
53+
}
54+
55+
String tokens = jwtUtil.createTokens(memberDto);
56+
57+
response.addHeader("Authorization", "Bearer " + tokens);
58+
59+
return true;
60+
}
61+
62+
@Override
63+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
64+
log.info("AuthenticationInterceptor : postHandle()");
65+
}
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.springwebjwtredis.aop.auth;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.web.servlet.HandlerInterceptor;
5+
import org.springframework.web.servlet.ModelAndView;
6+
7+
import javax.servlet.http.HttpServletRequest;
8+
import javax.servlet.http.HttpServletResponse;
9+
10+
@Slf4j
11+
public class AuthorizationInterceptor implements HandlerInterceptor {
12+
13+
@Override
14+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
15+
log.info("AuthorizationInterceptor : preHandle()");
16+
return HandlerInterceptor.super.preHandle(request, response, handler);
17+
}
18+
19+
@Override
20+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
21+
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
22+
log.info("AuthorizationInterceptor : postHandle()");
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.springwebjwtredis.aop.exception;
2+
3+
public class PasswordNotEqualsException extends IllegalArgumentException {
4+
public PasswordNotEqualsException() {
5+
}
6+
7+
public PasswordNotEqualsException(String msg) {
8+
super(msg);
9+
}
10+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.example.springwebjwtredis.aop.jwt;
2+
3+
import com.example.springwebjwtredis.domain.MemberDto;
4+
import io.jsonwebtoken.*;
5+
import io.jsonwebtoken.security.Keys;
6+
import io.jsonwebtoken.security.SignatureException;
7+
import lombok.extern.slf4j.Slf4j;
8+
9+
import java.security.Key;
10+
import java.time.LocalDateTime;
11+
import java.time.ZoneId;
12+
import java.util.Date;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
@Slf4j
17+
public class JwtUtil {
18+
19+
private static final int TOKEN_DEFAULT_EXPIRED = 0;
20+
private static final int TOKEN_ACCESS_EXPIRED = 1;
21+
22+
private final String issue;
23+
private final Key key;
24+
private final Long expiredDate;
25+
26+
public JwtUtil(String issue, String secret, Long expiredDate) {
27+
this.issue = issue;
28+
this.key = Keys.hmacShaKeyFor(secret.getBytes());
29+
this.expiredDate = expiredDate;
30+
}
31+
32+
public String createTokens(MemberDto memberDto) {
33+
// DB 조회 시 오류 발생 처리 필요
34+
String email = memberDto.getEmail();
35+
// 엑세스 토큰 발급 및 헤더 저장
36+
String accessToken = generateToken(memberDto, TOKEN_ACCESS_EXPIRED);
37+
// accessTokenService.add(email, accessToken);
38+
// 리프레시 토큰 발급 및 DB 저장
39+
// String refreshToken = jwtUtils.generateToken(memberDto, JwtConst.REFRESH_EXPIRED);
40+
// refreshTokenService.add(email, refreshToken);
41+
return accessToken;
42+
}
43+
44+
/**
45+
* JWT 토큰 생성 메서드
46+
*
47+
* @param memberDto 현 프로젝트의 사용자 데이터
48+
* @return JWT { Header.Payload.Signature } 반환
49+
*/
50+
public String generateToken(MemberDto memberDto, Integer plusMinutes) {
51+
52+
return Jwts.builder()
53+
// JWT -> Header 생성 부분
54+
.setHeader(createHeader()) // 토큰의 타입 명시
55+
// JWT -> Signature 암호화 부분
56+
.signWith(key, SignatureAlgorithm.HS256) // 해싱 알고리즘으로 암호화 -> 서버에서 토큰 검증 시 signature에서 사용함
57+
// JWT -> Payload 생성 부분
58+
.setClaims(createClaims(memberDto)) // private claim -> 사용자 정보
59+
// JWT -> Payload -> public Claims
60+
.setIssuer(issue) // 토큰 발급
61+
.setSubject("jwt-service") // 토큰 제목
62+
.setAudience("seok") // 토큰 대상자
63+
.setIssuedAt(createDate(TOKEN_DEFAULT_EXPIRED)) // 토큰 발생 시간
64+
.setExpiration(createDate(plusMinutes)) // 만료시간
65+
66+
.compact();
67+
}
68+
69+
/**
70+
* @return 만료일자 반환
71+
*/
72+
private Date createDate(int plusTime) {
73+
Date date = Date.from(
74+
LocalDateTime.now()
75+
.plusMinutes(plusTime)
76+
.atZone(ZoneId.systemDefault())
77+
.toInstant()
78+
);
79+
log.info("Token 3: create{}: {}", (plusTime == TOKEN_DEFAULT_EXPIRED ? "issueAt Time" : "expireAt Time"), date.getTime());
80+
return date;
81+
}
82+
83+
/**
84+
* @return 헤더 설정
85+
*/
86+
private Map<String, Object> createHeader() {
87+
Map<String, Object> header = new HashMap<>();
88+
89+
header.put("typ", Header.JWT_TYPE);// 토큰의 타입 명시
90+
header.put("alg", SignatureAlgorithm.HS256); // 해싱 알고리즘으로 암호화 -> 서버에서 토큰 검증 시 signature에서 사용함
91+
92+
log.info("Token 1: createHeader : {}", header);
93+
return header;
94+
}
95+
96+
/**
97+
* @return 로그인시 Member의 값을 JWT 토큰을 만들기 위한 값을 추출하여 비공개 Claims으로 파싱
98+
* 현재 {"id": "userName"} 로 만듬
99+
*/
100+
private Map<String, Object> createClaims(MemberDto memberDto) {
101+
// 비공개 클레임으로 사용자의 이름과 이메일을 설정, 세션 처럼 정보를 넣고 빼서 쓸 수 있다.
102+
Map<String, Object> claims = new HashMap<>();
103+
104+
claims.put("id", memberDto.getEmail());
105+
// claims.put("role", memberDto.getRole());
106+
107+
log.info("Token 2: {}", claims);
108+
return claims;
109+
}
110+
111+
/**
112+
* @param token 사용자의 Request 값의 Header에 존재하는 Authorization: Bearer {JWT}
113+
* @return JWT의 payload decode
114+
*/
115+
private Claims getClaimsFormToken(String token) {
116+
log.info("token parse: {}", Jwts.parser().setSigningKey(key).parseClaimsJws(token));
117+
log.info("token Header: {}", Jwts.parser().setSigningKey(key).parseClaimsJws(token).getHeader());
118+
log.info("token Body: {}", Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody());
119+
return Jwts.parser()
120+
.setSigningKey(key)
121+
.parseClaimsJws(token).getBody();
122+
}
123+
124+
/**
125+
* @param token JWT의 Payload 값
126+
* @return payload에서 현재 프로젝트에서 설정한 사용자 값을 구분하는 값인 userEmail을 반환
127+
*/
128+
public String getUserNameFromToken(String token) {
129+
/* 복호화된 Payload로 id 확인 */
130+
Claims claims = getClaimsFormToken(token);
131+
return (String) claims.get("id");
132+
}
133+
134+
/**
135+
* @param token 사용자 정보를 HS256로 암호화한 JWT
136+
* @return token 복호화하여 Payload 값 반환
137+
*/
138+
public Claims getClaims(String token) {
139+
// Jws는 sign이 포함된 JWT를 말함
140+
return Jwts.parser()
141+
.setSigningKey(key)
142+
.parseClaimsJws(token)
143+
.getBody();
144+
}
145+
146+
/**
147+
* @param token 토큰의 유효성 검사
148+
*/
149+
public boolean isValidToken(String token) throws ExpiredJwtException {
150+
try {
151+
log.info("=================== Claims ===================");
152+
log.info("requestToken : {}", token);
153+
Claims claims = getClaimsFormToken(token);
154+
log.info("claims : {}", claims);
155+
log.info("expireTime : {}", Date.from(claims.getExpiration().toInstant())); // time은 정해진 포맷이 있어야 할 것 같은데 ?
156+
log.info("Id :" + claims.get("id"));
157+
log.info("Audience :" + claims.getAudience());
158+
log.info("Issuer :" + claims.getIssuer());
159+
log.info("IssuedAt :" + claims.getIssuedAt().getTime());
160+
log.info("Subject :" + claims.getSubject());
161+
log.info("NotBefore :" + claims.getNotBefore());
162+
log.info("=================== Claims ===================");
163+
return true;
164+
} catch (SignatureException exception) {
165+
log.error("SignatureException 오류");
166+
} catch (MalformedJwtException exception) {
167+
log.error("구조적인 문제가 있는 JWT인 경우");
168+
} catch (UnsupportedJwtException exception) {
169+
log.error("암호화된 JWT를 사용하는 애프리케이션에 암호화되지 않은 JWT가 전달되는 경우");
170+
/* access Token 예외 발생으로 인해 refreshToken 체크 시점 */
171+
// } catch (ExpiredJwtException exception) {
172+
// log.error("Token ExpiredJwtException");
173+
} catch (PrematureJwtException exception) {
174+
log.error("접근이 허용되기 전인 JWT가 수신된 경우");
175+
} catch (ClaimJwtException exception) {
176+
log.error("JWT 권한claim 검사가 실패했을 때");
177+
} catch (JwtException exception) {
178+
log.error("Token Tampered");
179+
} catch (NullPointerException exception) {
180+
log.error("Token is null");
181+
throw new RuntimeException("Token is null");
182+
}
183+
return false;
184+
}
185+
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
package com.example.springwebjwtredis.config;
22

3+
import com.example.springwebjwtredis.aop.jwt.JwtUtil;
34
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.context.annotation.Bean;
46
import org.springframework.context.annotation.Configuration;
57

68
@Configuration
79
public class JwtConfig {
10+
@Value(value = "${spring.application.name}")
11+
private String issue;
12+
813
@Value("${jwt.secret}")
914
private String secret;
1015

1116
@Value("${jwt.expired_date}")
12-
private String expiredDate;
17+
private Long expiredDate;
18+
19+
@Bean
20+
public JwtUtil jwtUtil() {
21+
return new JwtUtil(issue, secret, expiredDate);
22+
}
1323
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /