feat: 로그인 시 refreshToken 추가 발행
This commit is contained in:
@@ -15,8 +15,10 @@ import org.springframework.context.annotation.PropertySource;
|
|||||||
public class JwtProperties {
|
public class JwtProperties {
|
||||||
|
|
||||||
private final String accessHeader;
|
private final String accessHeader;
|
||||||
|
private final String refreshHeader;
|
||||||
private final String prefix;
|
private final String prefix;
|
||||||
private final String secretKey;
|
private final String secretKey;
|
||||||
private final Integer tokenValidityInSeconds;
|
private final Integer accessTokenValidityInSeconds;
|
||||||
|
private final Integer refreshTokenValidityInSeconds;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,29 +31,43 @@ public class JwtProvider {
|
|||||||
private static final String AUTHORITIES_KEY = "auth";
|
private static final String AUTHORITIES_KEY = "auth";
|
||||||
private static final String AUTHORITIES_DELIMITER = ",";
|
private static final String AUTHORITIES_DELIMITER = ",";
|
||||||
|
|
||||||
private final long tokenValidityInMilliseconds;
|
private final long accessTokenValidityInMilliseconds;
|
||||||
|
private final long refreshTokenValidityInMilliseconds;
|
||||||
private final Key key;
|
private final Key key;
|
||||||
|
|
||||||
public JwtProvider(JwtProperties jwtProperties) {
|
public JwtProvider(JwtProperties jwtProperties) {
|
||||||
this.tokenValidityInMilliseconds = jwtProperties.getTokenValidityInSeconds();
|
this.accessTokenValidityInMilliseconds = jwtProperties.getAccessTokenValidityInSeconds() * 1000L;
|
||||||
|
this.refreshTokenValidityInMilliseconds = jwtProperties.getRefreshTokenValidityInSeconds() * 1000L;
|
||||||
|
|
||||||
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
|
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
|
||||||
this.key = Keys.hmacShaKeyFor(keyBytes);
|
this.key = Keys.hmacShaKeyFor(keyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String createToken(Authentication authentication) {
|
public String createAccessToken(Authentication authentication) {
|
||||||
|
// 만료시간 계산
|
||||||
|
long now = (new Date()).getTime();
|
||||||
|
Date accessTokenExpiresIn = new Date(now + this.accessTokenValidityInMilliseconds);
|
||||||
|
|
||||||
|
return createToken(authentication, accessTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createRefreshToken(Authentication authentication) {
|
||||||
|
// 만료시간 계산
|
||||||
|
long now = (new Date()).getTime();
|
||||||
|
Date refreshTokenExpiresIn = new Date(now + this.refreshTokenValidityInMilliseconds);
|
||||||
|
|
||||||
|
return createToken(authentication, refreshTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createToken(Authentication authentication, Date expiration) {
|
||||||
// 권한 정보 가져오기
|
// 권한 정보 가져오기
|
||||||
String authorities = generateStringToAuthorities(authentication);
|
String authorities = generateStringToAuthorities(authentication);
|
||||||
|
|
||||||
// 만료시간 계산
|
|
||||||
long now = new Date().getTime();
|
|
||||||
Date accessTokenExpiresIn = new Date(now + this.tokenValidityInMilliseconds);
|
|
||||||
|
|
||||||
// JWT 생성
|
// JWT 생성
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setSubject(authentication.getName()) // email
|
.setSubject(authentication.getName()) // email
|
||||||
.claim(AUTHORITIES_KEY, authorities) // payload
|
.claim(AUTHORITIES_KEY, authorities) // payload
|
||||||
.setExpiration(accessTokenExpiresIn) // 만료일
|
.setExpiration(expiration) // 만료일
|
||||||
.signWith(key, SignatureAlgorithm.HS512) // 서명 키 값
|
.signWith(key, SignatureAlgorithm.HS512) // 서명 키 값
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
|
|
||||||
.and()
|
.and()
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers(HttpMethod.POST, "/user").permitAll()
|
.antMatchers(HttpMethod.POST, "/api/user/login").permitAll()
|
||||||
.antMatchers(HttpMethod.POST, "/user/login").permitAll()
|
.antMatchers(HttpMethod.POST, "/api/user").permitAll()
|
||||||
.antMatchers("/l7check").permitAll()
|
.antMatchers("/l7check").permitAll()
|
||||||
.antMatchers("/actuator/health").permitAll()
|
.antMatchers("/actuator/health").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.annotation.Secured;
|
import org.springframework.security.access.annotation.Secured;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.PatchMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -50,7 +50,7 @@ public class UserController {
|
|||||||
return ResponseEntity.status(HttpStatus.OK).body(UserDeleteResponse.of(user));
|
return ResponseEntity.status(HttpStatus.OK).body(UserDeleteResponse.of(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping("/password")
|
@PutMapping("/password")
|
||||||
@Secured("ROLE_GUEST")
|
@Secured("ROLE_GUEST")
|
||||||
public ResponseEntity<UserChangePasswordResponse> changePassword(@RequestBody @Valid UserModifyPasswordRequest request) {
|
public ResponseEntity<UserChangePasswordResponse> changePassword(@RequestBody @Valid UserModifyPasswordRequest request) {
|
||||||
if (request.oldEqualNew()) {
|
if (request.oldEqualNew()) {
|
||||||
@@ -64,10 +64,11 @@ public class UserController {
|
|||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
|
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
|
||||||
String accessToken = userService.login(loginRequest.toAuthentication());
|
LoginResponse tokenDto = userService.login(loginRequest.toAuthentication());
|
||||||
|
|
||||||
response.setHeader(jwtProperties.getAccessHeader(), accessToken);
|
response.setHeader(jwtProperties.getAccessHeader(), tokenDto.getAccessToken());
|
||||||
return ResponseEntity.status(HttpStatus.OK).body(LoginResponse.of(accessToken));
|
response.setHeader(jwtProperties.getRefreshHeader(), tokenDto.getRefreshToken());
|
||||||
|
return ResponseEntity.status(HttpStatus.OK).body(tokenDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import lombok.Getter;
|
|||||||
public class LoginResponse {
|
public class LoginResponse {
|
||||||
|
|
||||||
private String accessToken;
|
private String accessToken;
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
public static LoginResponse of(String accessToken) {
|
public static LoginResponse of(String accessToken, String refreshToken) {
|
||||||
return new LoginResponse(accessToken);
|
return new LoginResponse(accessToken, refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ticketing.server.user.service;
|
|||||||
|
|
||||||
import com.ticketing.server.global.exception.NotFoundEmailException;
|
import com.ticketing.server.global.exception.NotFoundEmailException;
|
||||||
import com.ticketing.server.global.jwt.JwtProvider;
|
import com.ticketing.server.global.jwt.JwtProvider;
|
||||||
|
import com.ticketing.server.user.application.response.LoginResponse;
|
||||||
import com.ticketing.server.user.domain.User;
|
import com.ticketing.server.user.domain.User;
|
||||||
import com.ticketing.server.user.domain.repository.UserRepository;
|
import com.ticketing.server.user.domain.repository.UserRepository;
|
||||||
import com.ticketing.server.user.service.dto.ChangePasswordDTO;
|
import com.ticketing.server.user.service.dto.ChangePasswordDTO;
|
||||||
@@ -31,9 +32,9 @@ public class UserServiceImpl implements UserService {
|
|||||||
private final JwtProvider jwtProvider;
|
private final JwtProvider jwtProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String login(UsernamePasswordAuthenticationToken authenticationToken) {
|
public LoginResponse login(UsernamePasswordAuthenticationToken authenticationToken) {
|
||||||
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
|
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
|
||||||
return jwtProvider.createToken(authentication);
|
return LoginResponse.of(jwtProvider.createAccessToken(authentication), jwtProvider.createRefreshToken(authentication));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package com.ticketing.server.user.service.interfaces;
|
package com.ticketing.server.user.service.interfaces;
|
||||||
|
|
||||||
|
import com.ticketing.server.user.application.response.LoginResponse;
|
||||||
import com.ticketing.server.user.domain.User;
|
import com.ticketing.server.user.domain.User;
|
||||||
import com.ticketing.server.user.service.dto.ChangePasswordDTO;
|
import com.ticketing.server.user.service.dto.ChangePasswordDTO;
|
||||||
import com.ticketing.server.user.service.dto.DeleteUserDTO;
|
import com.ticketing.server.user.service.dto.DeleteUserDTO;
|
||||||
import com.ticketing.server.user.service.dto.SignUpDTO;
|
import com.ticketing.server.user.service.dto.SignUpDTO;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
|
|
||||||
public interface UserService {
|
public interface UserService {
|
||||||
|
|
||||||
String login(UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken);
|
LoginResponse login(UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken);
|
||||||
|
|
||||||
User register(@Valid SignUpDTO signUpDto);
|
User register(@Valid SignUpDTO signUpDto);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ jasypt:
|
|||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
access-header: ACCESS_TOKEN
|
access-header: ACCESS_TOKEN
|
||||||
|
refresh-header: REFRESH_TOKEN
|
||||||
prefix: Bearer
|
prefix: Bearer
|
||||||
secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK
|
secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK
|
||||||
token-validity-in-seconds: 86400
|
access-token-validity-in-seconds: 60 # 1분
|
||||||
|
refresh-token-validity-in-seconds: 259200 # 3일
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ class JwtPropertiesTest {
|
|||||||
// then
|
// then
|
||||||
assertAll(
|
assertAll(
|
||||||
() -> assertThat(jwtProperties.getAccessHeader()).isEqualTo("ACCESS_TOKEN")
|
() -> assertThat(jwtProperties.getAccessHeader()).isEqualTo("ACCESS_TOKEN")
|
||||||
|
, () -> assertThat(jwtProperties.getRefreshHeader()).isEqualTo("REFRESH_TOKEN")
|
||||||
, () -> assertThat(jwtProperties.getPrefix()).isEqualTo("Bearer")
|
, () -> assertThat(jwtProperties.getPrefix()).isEqualTo("Bearer")
|
||||||
, () -> assertThat(jwtProperties.getTokenValidityInSeconds()).isEqualTo(86400)
|
, () -> assertThat(jwtProperties.getAccessTokenValidityInSeconds()).isEqualTo(60)
|
||||||
|
, () -> assertThat(jwtProperties.getRefreshTokenValidityInSeconds()).isEqualTo(259200)
|
||||||
, () -> assertThat(jwtProperties.getSecretKey()).isNotEmpty());
|
, () -> assertThat(jwtProperties.getSecretKey()).isNotEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class TokenProviderTest {
|
|||||||
new UsernamePasswordAuthenticationToken("ticketing@gmail.com", "123456", Collections.singleton(grantedAuthority));
|
new UsernamePasswordAuthenticationToken("ticketing@gmail.com", "123456", Collections.singleton(grantedAuthority));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
String token = tokenProvider.createToken(authenticationToken);
|
String token = tokenProvider.createAccessToken(authenticationToken);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(token).isNotEmpty();
|
assertThat(token).isNotEmpty();
|
||||||
@@ -57,7 +57,7 @@ class TokenProviderTest {
|
|||||||
new UsernamePasswordAuthenticationToken("ticketing@gmail.com", "123456", Collections.singleton(grantedAuthority));
|
new UsernamePasswordAuthenticationToken("ticketing@gmail.com", "123456", Collections.singleton(grantedAuthority));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
String token = tokenProvider.createToken(authenticationToken);
|
String token = tokenProvider.createAccessToken(authenticationToken);
|
||||||
Authentication authentication = tokenProvider.getAuthentication(token);
|
Authentication authentication = tokenProvider.getAuthentication(token);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class UserControllerTest {
|
|||||||
|
|
||||||
SignUpRequest signUpRequest = new SignUpRequest("ticketing", "ticketing@gmail.com", "qwe123", "010-2240-7920");
|
SignUpRequest signUpRequest = new SignUpRequest("ticketing", "ticketing@gmail.com", "qwe123", "010-2240-7920");
|
||||||
|
|
||||||
mvc.perform(post("/user")
|
mvc.perform(post("/api/user")
|
||||||
.content(asJsonString(signUpRequest))
|
.content(asJsonString(signUpRequest))
|
||||||
.contentType(MediaType.APPLICATION_JSON));
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
}
|
}
|
||||||
@@ -63,14 +63,15 @@ class UserControllerTest {
|
|||||||
LoginRequest request = new LoginRequest("ticketing@gmail.com", "qwe123");
|
LoginRequest request = new LoginRequest("ticketing@gmail.com", "qwe123");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ResultActions actions = mvc.perform(post("/user/login")
|
ResultActions actions = mvc.perform(post("/api/user/login")
|
||||||
.content(asJsonString(request))
|
.content(asJsonString(request))
|
||||||
.contentType(MediaType.APPLICATION_JSON));
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
actions.andDo(print())
|
actions.andDo(print())
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(header().exists("ACCESS_TOKEN"));
|
.andExpect(header().exists("ACCESS_TOKEN"))
|
||||||
|
.andExpect(header().exists("REFRESH_TOKEN"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -80,7 +81,7 @@ class UserControllerTest {
|
|||||||
LoginRequest request = new LoginRequest("ticketing@gmail.com", "qwe1234");
|
LoginRequest request = new LoginRequest("ticketing@gmail.com", "qwe1234");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
ResultActions actions = mvc.perform(post("/user/login")
|
ResultActions actions = mvc.perform(post("/api/user/login")
|
||||||
.content(asJsonString(request))
|
.content(asJsonString(request))
|
||||||
.contentType(MediaType.APPLICATION_JSON));
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ jasypt:
|
|||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
access-header: ACCESS_TOKEN
|
access-header: ACCESS_TOKEN
|
||||||
|
refresh-header: REFRESH_TOKEN
|
||||||
prefix: Bearer
|
prefix: Bearer
|
||||||
secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK
|
secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK
|
||||||
token-validity-in-seconds: 86400
|
access-token-validity-in-seconds: 60
|
||||||
|
refresh-token-validity-in-seconds: 259200
|
||||||
|
|||||||
Reference in New Issue
Block a user