
오늘은 바쁜 나머지 정신 없이 하면서 짰던 UserDeteils를 커스텀해서 다루는 로직을 알아보겠다.
@Getter
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
// /**
// * 해당 유저의 권한 목록
// */
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(() -> "ROLE_USER");
return authorities;
}
// /**
// * 이후의 필드들도 오버라이딩으로 주입
// */
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username));
return new CustomUserDetails(user);
}
}
SecurityConfig
@Bean 빈주입
public AuthenticationManager
authenticationManager(AuthenticationConfiguration
authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
TokenProvider
@Slf4j
@Component
public class TokenProvider {
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 14;// 2주
String token = AuthEnum.GRANT_TYPE.getValue();
private final Key key;
private CustomUserDetailsService detailsService;
public TokenProvider(@Value("${JWT_SECRET_KEY}") String secretKey , CustomUserDetailsService detailsService) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.detailsService = detailsService;
}
/**
* 유저 정보를 통해 토큰 생성
*/
public TokenDto generateToken(Authentication authentication) {
log.info("generateToken start");
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); // 30분
Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); // 14일
String accessToken = token + Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", "USER")
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.setIssuedAt(new Date(now))
.compact();
log.info(accessToken);
String refreshToken = token + Jwts.builder()
.setExpiration(refreshTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.setIssuedAt(new Date(now))
.setSubject(authentication.getName())
.compact();
log.info("accessToken: {}", accessToken);
return TokenDto.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
/**
* 토큰에서 유저 정보 추출
*/
public Authentication getAuthentication(String token) {
String username = parseClaims(token).getSubject();
CustomUserDetails userDetails = detailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails,"", userDetails.getAuthorities());
}
/**
* 토큰 정보 검증
*/
public boolean validateToken(String token) {
log.info("validateToken start");
log.info("token: {}", token);
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
// refresh token 활용해서 재발급
log.info("Expired JWT Token", e);
throw e;
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
}
return false;
}
private Claims parseClaims(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}```
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService implements LogoutHandler {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final MailManager mailManager;
@Transactional
// Access Token 리프레시
public TokenDto reissue(String refreshToken) {
Optional<User> user = userRepository.findByRefreshToken(refreshToken);
if(user!=null && !user.get().getRefreshToken().equals(refreshToken)){
throw new RuntimeException("잘못된 토큰입니다.");
}else if(user.get().isExpired()){
throw new RuntimeException("폐지된 토큰입니다.");
}
Authentication authentication = tokenProvider.getAuthentication(refreshToken.substring(7));
// String resolveToken = resolveToken(user.get().getRefreshToken());
TokenDto tokenDto = tokenProvider.generateToken(authentication);
user.get().updateToken(tokenDto.getRefreshToken());
return tokenDto;
}
@Transactional
@Override
public void logout(HttpServletRequest request, HttpServletResponse response , Authentication authentication) {
String authHeader = request.getHeader(AuthEnum.ACCESS_TOKEN.getValue());
if (authHeader == null && !authHeader.startsWith(AuthEnum.GRANT_TYPE.getValue())) {
throw new RuntimeException("알수 없는 access token.");
}
String accessToken = authHeader.substring(7);
String username = tokenProvider.getUsername(accessToken);
User refreshToken = userRepository.findByUsername(username).orElse(null);
refreshToken.setExpired(true);
}
@PostMapping("/reissue")
public ResponseEntity<String> reissue(HttpServletRequest request,HttpServletResponse response) {
String refreshToken = request.getHeader("refresh-token");
TokenDto token = authService.reissue(refreshToken);
response.setHeader(AuthEnum.ACCESS_TOKEN.getValue(), token.getAccessToken());
response.setHeader(AuthEnum.REFRESH_TOKEN.getValue(), token.getRefreshToken());
return ResponseEntity.ok("재발급완료");
}