Skip to content

Commit a792ed5

Browse files
authored
Merge pull request #201 from JobDri-Developer/dev
dev to main
2 parents b9da29e + 2cf95c3 commit a792ed5

7 files changed

Lines changed: 159 additions & 27 deletions

File tree

src/main/java/com/jobdri/jobdri_api/domain/auth/service/AuthService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public LoginResponse login(LoginRequest request) {
9494
}
9595

9696
public LoginResponse issueTokens(User user) {
97-
String accessToken = jwtUtil.createAccessToken(user.getEmail(), user.getId());
97+
String accessToken = jwtUtil.createAccessToken(user.getEmail(), user.getId(), user.getRole());
9898
String refreshTokenValue = jwtUtil.createRefreshToken(user.getEmail());
9999

100100
saveRefreshToken(user.getId(), refreshTokenValue);
@@ -137,7 +137,7 @@ public ReissueTokenResponse reissueToken(ReissueTokenRequest request) {
137137
User user = userRepository.findById(accessUserId)
138138
.orElseThrow(() -> new GeneralException(GeneralErrorCode.USER_NOT_FOUND));
139139

140-
String newAccessToken = jwtUtil.createAccessToken(user.getEmail(), user.getId());
140+
String newAccessToken = jwtUtil.createAccessToken(user.getEmail(), user.getId(), user.getRole());
141141
String newRefreshToken = jwtUtil.createRefreshToken(user.getEmail());
142142

143143
saveRefreshToken(user.getId(), newRefreshToken);

src/main/java/com/jobdri/jobdri_api/domain/auth/service/CustomOAuth2UserService.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.jobdri.jobdri_api.domain.user.entity.SocialType;
55
import com.jobdri.jobdri_api.domain.user.entity.User;
66
import com.jobdri.jobdri_api.domain.user.repository.UserRepository;
7-
import lombok.RequiredArgsConstructor;
87
import org.springframework.security.core.authority.SimpleGrantedAuthority;
98
import org.springframework.security.crypto.password.PasswordEncoder;
109
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
@@ -17,14 +16,17 @@
1716

1817
import java.util.List;
1918
import java.util.Map;
20-
import java.util.UUID;
2119

2220
@Service
23-
@RequiredArgsConstructor
2421
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
2522

2623
private final UserRepository userRepository;
27-
private final PasswordEncoder passwordEncoder;
24+
private final String encodedDummyPassword;
25+
26+
public CustomOAuth2UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
27+
this.userRepository = userRepository;
28+
this.encodedDummyPassword = passwordEncoder.encode("SOCIAL_LOGIN_ONLY_ACCOUNT");
29+
}
2830

2931
@Override
3032
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
@@ -47,7 +49,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
4749
User.createSocialUser(
4850
name,
4951
email,
50-
createDummyPassword(),
52+
encodedDummyPassword,
5153
SocialType.GOOGLE,
5254
socialId
5355
)
@@ -59,8 +61,4 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
5961
List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()))
6062
);
6163
}
62-
63-
private String createDummyPassword() {
64-
return passwordEncoder.encode(UUID.randomUUID().toString());
65-
}
6664
}

src/main/java/com/jobdri/jobdri_api/domain/user/entity/User.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,21 @@ public static User createSocialUser(
9797
.build();
9898
}
9999

100+
public static User authenticatedPrincipal(
101+
Long id,
102+
String email,
103+
UserRole role
104+
) {
105+
return User.builder()
106+
.id(id)
107+
.email(email)
108+
.password("")
109+
.role(role)
110+
.socialType(SocialType.LOCAL)
111+
.credit(0)
112+
.build();
113+
}
114+
100115
public void increaseCredit(int amount) {
101116
this.credit += amount;
102117
}

src/main/java/com/jobdri/jobdri_api/global/jwt/JwtAuthenticationFilter.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.jobdri.jobdri_api.global.jwt;
22

3+
import com.jobdri.jobdri_api.domain.user.entity.User;
4+
import com.jobdri.jobdri_api.domain.user.entity.UserRole;
35
import com.jobdri.jobdri_api.global.security.UserDetailsServiceImpl;
46
import com.jobdri.jobdri_api.global.logging.LoggingMdcKeys;
57
import com.jobdri.jobdri_api.global.security.UserDetailsImpl;
@@ -49,10 +51,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4951

5052
if (jwtUtil.validateToken(token)) {
5153
Claims claims = jwtUtil.getClaimsFromToken(token);
52-
5354
String email = jwtUtil.getEmailFromToken(claims);
5455

55-
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
56+
UserDetails userDetails = createUserDetails(claims);
5657

5758
Authentication authentication = new UsernamePasswordAuthenticationToken(
5859
userDetails,
@@ -72,6 +73,18 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
7273
filterChain.doFilter(request, response);
7374
}
7475

76+
private UserDetails createUserDetails(Claims claims) {
77+
String email = jwtUtil.getEmailFromToken(claims);
78+
Long userId = jwtUtil.getUserIdFromToken(claims);
79+
UserRole role = jwtUtil.getRoleFromToken(claims);
80+
81+
if (userId != null && role != null) {
82+
return new UserDetailsImpl(User.authenticatedPrincipal(userId, email, role));
83+
}
84+
85+
return userDetailsService.loadUserByUsername(email);
86+
}
87+
7588
private void populateUserLoggingContext(UserDetails userDetails) {
7689
if (userDetails instanceof UserDetailsImpl userDetailsImpl) {
7790
MDC.put(LoggingMdcKeys.USER_ID, String.valueOf(userDetailsImpl.getUser().getId()));

src/main/java/com/jobdri/jobdri_api/global/jwt/JwtUtil.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jobdri.jobdri_api.global.jwt;
22

3+
import com.jobdri.jobdri_api.domain.user.entity.UserRole;
34
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
45
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
56
import io.jsonwebtoken.Claims;
@@ -43,12 +44,12 @@ public void init() {
4344
key = Keys.hmacShaKeyFor(bytes);
4445
}
4546

46-
public String createAccessToken(String email, Long userId) {
47-
return createToken(email, userId, accessTokenTime);
47+
public String createAccessToken(String email, Long userId, UserRole role) {
48+
return createToken(email, userId, role, accessTokenTime);
4849
}
4950

5051
public String createRefreshToken(String email) {
51-
return createToken(email, null, refreshTokenTime);
52+
return createToken(email, null, null, refreshTokenTime);
5253
}
5354

5455
public long getRefreshTokenTime() {
@@ -60,7 +61,7 @@ public long getRemainingTime(String token) {
6061
return claims.getExpiration().getTime() - System.currentTimeMillis();
6162
}
6263

63-
private String createToken(String email, Long userId, long expireTime) {
64+
private String createToken(String email, Long userId, UserRole role, long expireTime) {
6465
Date now = new Date();
6566
Date expireDate = new Date(now.getTime() + expireTime);
6667

@@ -73,6 +74,9 @@ private String createToken(String email, Long userId, long expireTime) {
7374
if (userId != null) {
7475
builder.claim("userId", userId);
7576
}
77+
if (role != null) {
78+
builder.claim("role", role.name());
79+
}
7680

7781
return builder.compact();
7882
}
@@ -110,6 +114,22 @@ public String getEmailFromToken(Claims claims) {
110114
return claims.getSubject();
111115
}
112116

117+
public Long getUserIdFromToken(Claims claims) {
118+
return claims.get("userId", Long.class);
119+
}
120+
121+
public UserRole getRoleFromToken(Claims claims) {
122+
String role = claims.get("role", String.class);
123+
if (role == null || role.isBlank()) {
124+
return null;
125+
}
126+
try {
127+
return UserRole.valueOf(role);
128+
} catch (IllegalArgumentException exception) {
129+
throw new GeneralException(GeneralErrorCode.INVALID_TOKEN, "유효하지 않은 권한 정보입니다.");
130+
}
131+
}
132+
113133
public Claims getClaimsFromExpiredToken(String token) {
114134
try {
115135
return Jwts.parser()

src/main/resources/logback-spring.xml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@
77
<property name="APPLICATION_LOG_FILE" value="${LOG_PATH}/application.log" />
88
<property name="AUDIT_LOG_FILE" value="${AUDIT_LOG_PATH}/audit.log" />
99
<property name="LOG_PATTERN_DATE" value="yyyy-MM-dd'T'HH:mm:ss.SSSXXX" />
10+
<property
11+
name="CONSOLE_LOG_PATTERN"
12+
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg %mdc%n%ex{full}" />
1013

1114
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
12-
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
13-
<timeZone>Asia/Seoul</timeZone>
14-
<includeMdc>true</includeMdc>
15-
<customFields>{"service":"${APP_NAME}"}</customFields>
16-
<fieldNames>
17-
<timestamp>timestamp</timestamp>
18-
<logger>logger_name</logger>
19-
<thread>thread</thread>
20-
<stackTrace>stack_trace</stackTrace>
21-
</fieldNames>
15+
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
16+
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
17+
<charset>UTF-8</charset>
2218
</encoder>
2319
</appender>
2420

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.jobdri.jobdri_api.global.jwt;
2+
3+
import com.jobdri.jobdri_api.domain.user.entity.User;
4+
import com.jobdri.jobdri_api.domain.user.entity.UserRole;
5+
import com.jobdri.jobdri_api.global.security.UserDetailsImpl;
6+
import com.jobdri.jobdri_api.global.security.UserDetailsServiceImpl;
7+
import io.jsonwebtoken.Claims;
8+
import jakarta.servlet.FilterChain;
9+
import org.junit.jupiter.api.AfterEach;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.data.redis.core.StringRedisTemplate;
13+
import org.springframework.mock.web.MockHttpServletRequest;
14+
import org.springframework.mock.web.MockHttpServletResponse;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.Mockito.mock;
19+
import static org.mockito.Mockito.never;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.when;
22+
23+
class JwtAuthenticationFilterTest {
24+
25+
private final JwtUtil jwtUtil = mock(JwtUtil.class);
26+
private final UserDetailsServiceImpl userDetailsService = mock(UserDetailsServiceImpl.class);
27+
private final StringRedisTemplate redisTemplate = mock(StringRedisTemplate.class);
28+
private final JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil, userDetailsService, redisTemplate);
29+
30+
@AfterEach
31+
void tearDown() {
32+
SecurityContextHolder.clearContext();
33+
}
34+
35+
@Test
36+
@DisplayName("role claim이 있으면 DB 조회 없이 인증 객체를 구성한다")
37+
void authenticatesWithoutDatabaseLookupWhenRoleClaimExists() throws Exception {
38+
MockHttpServletRequest request = new MockHttpServletRequest();
39+
request.addHeader(JwtUtil.AUTHORIZATION_HEADER, JwtUtil.BEARER_PREFIX + "access-token");
40+
MockHttpServletResponse response = new MockHttpServletResponse();
41+
FilterChain filterChain = mock(FilterChain.class);
42+
Claims claims = mock(Claims.class);
43+
44+
when(redisTemplate.hasKey("Blacklist:access-token")).thenReturn(false);
45+
when(jwtUtil.substringToken(JwtUtil.BEARER_PREFIX + "access-token")).thenReturn("access-token");
46+
when(jwtUtil.validateToken("access-token")).thenReturn(true);
47+
when(jwtUtil.getClaimsFromToken("access-token")).thenReturn(claims);
48+
when(jwtUtil.getEmailFromToken(claims)).thenReturn("user@example.com");
49+
when(jwtUtil.getUserIdFromToken(claims)).thenReturn(42L);
50+
when(jwtUtil.getRoleFromToken(claims)).thenReturn(UserRole.USER);
51+
52+
filter.doFilter(request, response, filterChain);
53+
54+
verify(userDetailsService, never()).loadUserByUsername("user@example.com");
55+
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull();
56+
assertThat(SecurityContextHolder.getContext().getAuthentication().getPrincipal())
57+
.isInstanceOf(UserDetailsImpl.class);
58+
UserDetailsImpl principal = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
59+
assertThat(principal.getUser().getId()).isEqualTo(42L);
60+
assertThat(principal.getUser().getEmail()).isEqualTo("user@example.com");
61+
assertThat(principal.getUser().getRole()).isEqualTo(UserRole.USER);
62+
verify(filterChain).doFilter(request, response);
63+
}
64+
65+
@Test
66+
@DisplayName("구형 access token은 기존처럼 DB 조회로 호환한다")
67+
void fallsBackToDatabaseLookupWhenRoleClaimMissing() throws Exception {
68+
MockHttpServletRequest request = new MockHttpServletRequest();
69+
request.addHeader(JwtUtil.AUTHORIZATION_HEADER, JwtUtil.BEARER_PREFIX + "legacy-token");
70+
MockHttpServletResponse response = new MockHttpServletResponse();
71+
FilterChain filterChain = mock(FilterChain.class);
72+
Claims claims = mock(Claims.class);
73+
UserDetailsImpl userDetails = new UserDetailsImpl(User.authenticatedPrincipal(7L, "legacy@example.com", UserRole.USER));
74+
75+
when(redisTemplate.hasKey("Blacklist:legacy-token")).thenReturn(false);
76+
when(jwtUtil.substringToken(JwtUtil.BEARER_PREFIX + "legacy-token")).thenReturn("legacy-token");
77+
when(jwtUtil.validateToken("legacy-token")).thenReturn(true);
78+
when(jwtUtil.getClaimsFromToken("legacy-token")).thenReturn(claims);
79+
when(jwtUtil.getEmailFromToken(claims)).thenReturn("legacy@example.com");
80+
when(jwtUtil.getUserIdFromToken(claims)).thenReturn(7L);
81+
when(jwtUtil.getRoleFromToken(claims)).thenReturn(null);
82+
when(userDetailsService.loadUserByUsername("legacy@example.com")).thenReturn(userDetails);
83+
84+
filter.doFilter(request, response, filterChain);
85+
86+
verify(userDetailsService).loadUserByUsername("legacy@example.com");
87+
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNotNull();
88+
verify(filterChain).doFilter(request, response);
89+
}
90+
}

0 commit comments

Comments
 (0)