feat(user-service): access token, refresh token 검증

- jwt 패키지 추가
- refresh token을 통해 access token 생성 로직 추가
- jwt token provider 추가
This commit is contained in:
bum12ark
2022-02-16 11:42:25 +09:00
parent 72c52e4b58
commit b6274466b6
16 changed files with 353 additions and 33 deletions

View File

@@ -1,8 +1,12 @@
package com.justpickup.userservice;
import com.justpickup.userservice.domain.user.dto.StoreOwnerDto;
import com.justpickup.userservice.domain.user.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableEurekaClient
@@ -12,4 +16,13 @@ public class UserServiceApplication {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
CommandLineRunner run(UserService userService) {
return args -> {
StoreOwnerDto park = StoreOwnerDto.builder()
.email("test@gmail.com").password("1234").name("Park").phoneNumber("010-1234-5678")
.build();
userService.saveStoreOwner(park);
};
}
}

View File

@@ -0,0 +1,11 @@
package com.justpickup.userservice.domain.jwt.exception;
import com.justpickup.userservice.global.exception.CustomException;
import org.springframework.http.HttpStatus;
public class TokenRefreshException extends CustomException {
public TokenRefreshException(String message) {
super(HttpStatus.FORBIDDEN, message);
}
}

View File

@@ -0,0 +1,8 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
public interface RefreshTokenService {
void updateRefreshToken(Long id, String refreshToken);
JwtTokenDto refreshJwtToken(String accessToken, String refreshToken);
}

View File

@@ -0,0 +1,73 @@
package com.justpickup.userservice.domain.jwt.service;
import com.justpickup.userservice.domain.jwt.exception.TokenRefreshException;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.domain.user.entity.User;
import com.justpickup.userservice.domain.user.exception.NotExistUserException;
import com.justpickup.userservice.domain.user.repository.UserRepository;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class RefreshTokenServiceImpl implements RefreshTokenService {
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
@Override
public void updateRefreshToken(Long id, String refreshToken) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + id + "는 없는 사용자입니다."));
user.changeRefreshToken(refreshToken);
}
@Transactional
@Override
public JwtTokenDto refreshJwtToken(String accessToken, String refreshToken) {
String userId = jwtTokenProvider.getUserId(accessToken);
User user = userRepository.findById(Long.valueOf(userId))
.orElseThrow(() -> new NotExistUserException("사용자 고유번호 : " + userId + "는 없는 사용자입니다."));
// refresh token 검증
if (!jwtTokenProvider.validateJwtToken(refreshToken)) {
// 익셉션 발생 - 로그 아웃 후 로그인 페이지로 이동 처리
user.deleteRefreshToken();
throw new TokenRefreshException("Not validate jwt token = " + refreshToken);
}
String userRefreshTokenId = user.getRefreshTokenId();
if (!jwtTokenProvider.equalRefreshTokenId(userRefreshTokenId, refreshToken)) {
// 익셉션 발생 - 로그인 아웃 후 로그인 페이지로 이동 처리
user.deleteRefreshToken();
throw new TokenRefreshException("Not equal jwt token! user = " + userRefreshTokenId +
", refreshToken = " + refreshToken);
}
Authentication authentication = jwtTokenProvider.getAuthentication(user.getEmail());
List<String> roles = authentication.getAuthorities()
.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String newAccessToken = jwtTokenProvider.createJwtAccessToken(userId, "/refreshToken", roles);
String newRefreshToken = jwtTokenProvider.createJwtRefreshToken();
user.changeRefreshToken(newRefreshToken);
return JwtTokenDto.builder()
.accessToken(newAccessToken)
.refreshToken(newRefreshToken)
.build();
}
}

View File

@@ -0,0 +1,115 @@
package com.justpickup.userservice.domain.jwt.utils;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
@Value("${token.access-expired-time}")
private long ACCESS_EXPIRED_TIME;
@Value("${token.refresh-expired-time}")
private long REFRESH_EXPIRED_TIME;
@Value("${token.secret}")
private String SECRET;
public String createJwtAccessToken(String userId, String uri, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("roles", roles);
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuer(uri)
.compact();
}
public String createJwtRefreshToken() {
Claims claims = Jwts.claims();
claims.put("value", UUID.randomUUID());
return Jwts.builder()
.addClaims(claims)
.setExpiration(
new Date(System.currentTimeMillis() + REFRESH_EXPIRED_TIME)
)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public Authentication getAuthentication(String email) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
}
public String getUserId(String token) {
try {
return getClaimsFromJwtToken(token).getBody().getSubject();
} catch (ExpiredJwtException expiredJwtException) {
return expiredJwtException.getClaims().getSubject();
}
}
private Jws<Claims> getClaimsFromJwtToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
}
public String getRefreshTokenId(String token) {
try {
return getClaimsFromJwtToken(token).getBody().get("value").toString();
} catch (ExpiredJwtException expiredJwtException) {
return expiredJwtException.getClaims().get("value").toString();
}
}
public boolean validateJwtToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
return false;
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
return false;
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
return false;
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
return false;
}
}
public boolean equalRefreshTokenId(String refreshTokenId, String refreshToken) {
String compareToken = this.getRefreshTokenId(refreshToken);
return refreshTokenId.equals(compareToken);
}
}

View File

@@ -0,0 +1,53 @@
package com.justpickup.userservice.domain.jwt.web;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.user.dto.JwtTokenDto;
import com.justpickup.userservice.global.dto.Result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
@GetMapping("/refreshToken")
public ResponseEntity<Result> refreshToken(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@RequestHeader("REFRESH-TOKEN") String refreshToken) {
JwtTokenDto jwtTokenDto = refreshTokenServiceImpl.refreshJwtToken(accessToken, refreshToken);
return ResponseEntity.ok(Result.createSuccessResult(new RefreshTokenResponse(jwtTokenDto)));
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class RefreshTokenResponse {
private String accessToken;
private String refreshToken;
public RefreshTokenResponse(JwtTokenDto jwtTokenDto) {
this.accessToken = jwtTokenDto.getAccessToken();
this.refreshToken = jwtTokenDto.getRefreshToken();
}
}
@PostMapping("/logout")
public ResponseEntity<Result> logout(@RequestHeader("X-AUTH-TOKEN") String accessToken,
@RequestHeader("REFRESH-TOKEN") String refreshToken) {
log.info("########### logout!");
// TODO: 2022/02/16 logout 구현 필요
return ResponseEntity.ok(Result.createSuccessResult("success"));
}
}

View File

@@ -12,7 +12,8 @@ public class CustomerDto extends UserDto {
}
@Builder
public CustomerDto(Long id, String email, String password, String name, String phoneNumber, String dtype) {
super(id, email, password, name, phoneNumber, dtype);
public CustomerDto(Long id, String email, String password, String name,
String phoneNumber, String dtype, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
}
}

View File

@@ -0,0 +1,16 @@
package com.justpickup.userservice.domain.user.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
public class JwtTokenDto {
private String accessToken;
private String refreshToken;
@Builder
public JwtTokenDto(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}

View File

@@ -9,8 +9,8 @@ public class StoreOwnerDto extends UserDto {
@Builder
public StoreOwnerDto(Long id, String email, String password, String name,
String phoneNumber, String dtype, String businessNumber) {
super(id, email, password, name, phoneNumber, dtype);
String phoneNumber, String dtype, String businessNumber, String refreshTokenId) {
super(id, email, password, name, phoneNumber, dtype, refreshTokenId);
this.businessNumber = businessNumber;
}
}

View File

@@ -11,6 +11,7 @@ public abstract class UserDto {
private String name;
private String phoneNumber;
private String dtype;
private String refreshTokenId;
// == 생성 메소드 == //
public UserDto(Customer customer) {
@@ -20,12 +21,14 @@ public abstract class UserDto {
this.phoneNumber = customer.getPhoneNumber();
}
public UserDto(Long id, String email, String password, String name, String phoneNumber, String dtype) {
public UserDto(Long id, String email, String password, String name, String phoneNumber,
String dtype, String refreshTokenId) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
this.phoneNumber = phoneNumber;
this.dtype = dtype;
this.refreshTokenId = refreshTokenId;
}
}

View File

@@ -14,8 +14,9 @@ import javax.persistence.Table;
public class StoreOwner extends User {
private String businessNumber;
public StoreOwner(String email, String password, String name, String phoneNumber, String businessNumber) {
super(email, password, name, phoneNumber);
public StoreOwner(String email, String password, String name, String phoneNumber,
String businessNumber, String refreshTokenId) {
super(email, password, name, phoneNumber, refreshTokenId);
this.businessNumber = businessNumber;
}
}

View File

@@ -26,13 +26,24 @@ public abstract class User extends BaseEntity {
private String phoneNumber;
private String refreshTokenId;
@Column(insertable = false, updatable = false)
private String dtype;
public User(String email, String password, String name, String phoneNumber) {
public User(String email, String password, String name, String phoneNumber, String refreshTokenId) {
this.email = email;
this.password = password;
this.name = name;
this.phoneNumber = phoneNumber;
this.refreshTokenId = refreshTokenId;
}
public void changeRefreshToken(String refreshToken) {
this.refreshTokenId = refreshToken;
}
public void deleteRefreshToken() {
this.refreshTokenId = null;
}
}

View File

@@ -55,8 +55,9 @@ public class UserServiceImpl implements UserService, UserDetailsService {
String encode = passwordEncoder.encode(storeOwnerDto.getPassword());
StoreOwner storeOwner = new StoreOwner(storeOwnerDto.getEmail(), encode, storeOwnerDto.getName(),
storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber());
storeOwnerDto.getPhoneNumber(), storeOwnerDto.getBusinessNumber(), storeOwnerDto.getRefreshTokenId());
return userRepository.save(storeOwner);
}
}

View File

@@ -1,9 +1,9 @@
package com.justpickup.userservice.global.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import com.justpickup.userservice.global.dto.LoginRequest;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
@@ -19,7 +19,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -30,6 +30,8 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
// login 리퀘스트 패스로 오는 요청을 판단
@Override
@@ -55,30 +57,25 @@ public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFil
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
org.springframework.security.core.userdetails.User user = (User) authResult.getPrincipal();
String accessToken = Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(
new Date(System.currentTimeMillis() + 10 * 60 * 1000)
)
.signWith(SignatureAlgorithm.HS512, "your-256-bit-secret")
.setIssuer(request.getRequestURI())
.addClaims(Map.of("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())))
.compact();
List<String> roles = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String refreshToken = Jwts.builder()
.setSubject(user.getUsername())
.setExpiration(
new Date(System.currentTimeMillis() + 30 * 60 * 1000)
)
.signWith(SignatureAlgorithm.HS512, "your-256-bit-secret")
.setIssuer(request.getRequestURI())
.compact();
String userId = user.getUsername();
String accessToken = jwtTokenProvider.createJwtAccessToken(userId, request.getRequestURI(), roles);
String refreshToken = jwtTokenProvider.createJwtRefreshToken();
refreshTokenServiceImpl.updateRefreshToken(Long.valueOf(userId), jwtTokenProvider.getRefreshTokenId(refreshToken));
Map<String, String> tokens = Map.of(
"access_token", accessToken,
"refresh_token", refreshToken
);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}

View File

@@ -1,5 +1,7 @@
package com.justpickup.userservice.global.security;
import com.justpickup.userservice.domain.jwt.service.RefreshTokenServiceImpl;
import com.justpickup.userservice.domain.jwt.utils.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
@@ -18,6 +20,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenServiceImpl refreshTokenServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
@@ -26,12 +30,20 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter(authenticationManagerBean());
LoginAuthenticationFilter loginAuthenticationFilter =
new LoginAuthenticationFilter(authenticationManagerBean(), jwtTokenProvider, refreshTokenServiceImpl);
loginAuthenticationFilter.setFilterProcessesUrl("/login");
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().anyRequest().permitAll();
http.logout()
.logoutUrl("/logout")
.deleteCookies("");
http.addFilter(loginAuthenticationFilter);
http.addFilterBefore(new HeaderAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}

View File

@@ -12,7 +12,7 @@ spring:
jpa:
hibernate:
ddl-auto: validate
ddl-auto: create
generate-ddl: true
open-in-view: false
properties:
@@ -34,4 +34,9 @@ logging:
# jpa query, parameter 로그 (p6spy)
decorator.datasource.p6spy:
enable-logging: true
enable-logging: true
token:
access-expired-time: 3600000
refresh-expired-time: 604800000
secret: my-secret