This commit is contained in:
mindol1004
2024-10-02 13:55:25 +09:00
parent 10e0f0bb97
commit f626065b47
10 changed files with 122 additions and 167 deletions

View File

@@ -1,22 +1,24 @@
package com.spring.domain.user.entity;
import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -25,8 +27,7 @@ import lombok.NoArgsConstructor;
@Table(name = "AGENT_USER")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentUser {
public class AgentUser implements UserDetails {
@Id
@GeneratedValue(generator = "uuid2")
@@ -34,9 +35,6 @@ public class AgentUser {
@Column(name = "ID", nullable = false, columnDefinition = "BINARY(16)")
private UUID id;
// @OneToOne(mappedBy = "agentUser", cascade = CascadeType.ALL, orphanRemoval = true)
// private AgentUserToken agentUserToken;
@Column(name = "USER_ID", nullable = false, length = 50)
private String userId;
@@ -58,4 +56,43 @@ public class AgentUser {
this.userRole = userRole;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(AgentUserRole.values())
.filter(role -> Arrays.asList(this.userRole).contains(role))
.map(AgentUserRole::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getPassword() {
return this.userPassword;
}
@Override
public String getUsername() {
return this.userId;
}
}

View File

@@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor;
public enum UserRule implements ErrorRule {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
USER_ID_CONFLICT(HttpStatus.CONFLICT, "중복된 아이디 입니다.");
private final HttpStatus status;

View File

@@ -7,8 +7,11 @@ import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.domain.user.entity.AgentUserToken;
public interface AgentUserTokenRepository extends JpaRepository<AgentUserToken, UUID> {
Optional<AgentUserToken> findByIdAndReissueCountLessThan(UUID id, long count);
Optional<AgentUserToken> findByRefreshToken(String refreshToken);
}

View File

@@ -1,4 +1,4 @@
package com.spring.infra.security.service;
package com.spring.domain.user.service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -6,10 +6,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.error.UserRule;
import com.spring.domain.user.repository.AgentUserRepository;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityExceptionRule;
import lombok.RequiredArgsConstructor;
@@ -22,9 +20,8 @@ public class UserPrincipalService implements UserDetailsService {
@Transactional(readOnly = true)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AgentUser user = agentUserRepository.findByUserId(username)
.orElseThrow(() -> new UsernameNotFoundException(SecurityExceptionRule.USER_UNAUTHORIZED.getMessage()));
return UserPrincipal.valueOf(user);
return agentUserRepository.findByUserId(username)
.orElseThrow(() -> new UsernameNotFoundException(UserRule.USER_UNAUTHORIZED.getMessage()));
}
}

View File

@@ -0,0 +1,44 @@
package com.spring.domain.user.service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserToken;
import com.spring.domain.user.error.UserNotFoundException;
import com.spring.domain.user.repository.AgentUserRepository;
import com.spring.domain.user.repository.AgentUserTokenRepository;
import com.spring.infra.security.service.RefreshTokenService;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class UserRefreshTokenService implements RefreshTokenService {
private final AgentUserRepository agentUserRepository;
private final AgentUserTokenRepository agentUserTokenRepository;
@Transactional
@Override
public void saveRefreshToken(UserDetails user, String refreshToken) {
AgentUser agentUser = agentUserRepository.findByUserId(user.getUsername()).orElseThrow(UserNotFoundException::new);
agentUserTokenRepository.findById(agentUser.getId())
.ifPresentOrElse(
it -> it.updateRefreshToken(refreshToken),
() -> agentUserTokenRepository.save(
AgentUserToken.builder()
.agentUser(agentUser)
.refreshToken(refreshToken)
.build()
)
);
}
@Override
public String findByRefreshToken(String refreshToken) {
throw new UnsupportedOperationException("Unimplemented method 'findByRefreshToken'");
}
}

View File

@@ -1,115 +0,0 @@
package com.spring.infra.security.domain;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserRole;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Spring Security의 UserDetails 인터페이스를 구현한 사용자 주체(Principal) 클래스입니다.
* <p>애플리케이션의 사용자 정보를 Spring Security에서 사용할 수 있는 형태로 변환합니다.</p>
*
* @author mindol
* @version 1.0
*/
@Getter
@RequiredArgsConstructor
public final class UserPrincipal implements UserDetails {
private final transient AgentUser agentUser;
/**
* AgentUser 객체로부터 UserPrincipal 객체를 생성합니다.
*
* @param agentUser 변환할 AgentUser 객체
* @return 생성된 UserPrincipal 객체
*/
public static UserPrincipal valueOf(AgentUser agentUser) {
return new UserPrincipal(agentUser);
}
/**
* 사용자의 권한 목록을 반환합니다.
*
* @return 사용자의 GrantedAuthority 컬렉션
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(AgentUserRole.values())
.filter(role -> Arrays.asList(agentUser.getUserRole()).contains(role))
.map(AgentUserRole::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
/**
* 계정이 만료되지 않았는지 확인합니다.
*
* @return 계정 만료 여부 (true: 만료되지 않음)
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 계정이 잠기지 않았는지 확인합니다.
*
* @return 계정 잠금 여부 (true: 잠기지 않음)
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 자격 증명(비밀번호)이 만료되지 않았는지 확인합니다.
*
* @return 자격 증명 만료 여부 (true: 만료되지 않음)
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 계정이 활성화되어 있는지 확인합니다.
*
* @return 계정 활성화 여부 (true: 활성화됨)
*/
@Override
public boolean isEnabled() {
return true;
}
/**
* 사용자의 비밀번호를 반환합니다.
* 이 구현에서는 null을 반환합니다.
*
* @return 사용자 비밀번호 (null)
*/
@Override
public String getPassword() {
return agentUser.getUserPassword();
}
/**
* 사용자의 로그인 ID를 반환합니다.
*
* @return 사용자 로그인 ID
*/
@Override
public String getUsername() {
return agentUser.getUserName();
}
}

View File

@@ -7,9 +7,9 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
@@ -19,8 +19,8 @@ import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.config.SecurityURI;
import com.spring.infra.security.dto.SignResponse;
import com.spring.infra.security.jwt.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;
import com.spring.infra.security.service.RefreshTokenService;
import lombok.RequiredArgsConstructor;
@@ -29,6 +29,7 @@ import lombok.RequiredArgsConstructor;
public class SigninSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenService jwtTokenService;
private final RefreshTokenService refreshTokenService;
private final ObjectMapper objectMapper;
private RequestCache requestCache = new HttpSessionRequestCache();
@@ -39,13 +40,12 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
Authentication authentication
) throws IOException, ServletException {
String accessToken = jwtTokenService.generateAccessToken(response, authentication);
jwtTokenService.generateRefreshToken(response, authentication);
jwtTokenService.generateAccessToken(response, authentication);
refreshTokenService.saveRefreshToken((UserDetails) authentication.getPrincipal(), jwtTokenService.generateRefreshToken(response, authentication));
SavedRequest savedRequest = requestCache.getRequest(request, response);
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : SecurityURI.REDIRECT_URI.getUri();
response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write(objectMapper.writeValueAsString(SignResponse.of(true, targetUrl)));

View File

@@ -4,7 +4,6 @@ import java.security.Key;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.EntityNotFoundException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -15,17 +14,12 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserToken;
import com.spring.domain.user.repository.AgentUserRepository;
import com.spring.domain.user.repository.AgentUserTokenRepository;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.UserPrincipalService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@@ -43,9 +37,7 @@ public class JwtTokenService {
private final JwtTokenUtil jwtTokenUtil;
private final JwtTokenGenerator jwtTokenGenerator;
private final UserPrincipalService userPrincipalService;
private final AgentUserRepository agentUserRepository;
private final AgentUserTokenRepository agentUserTokenRepository;
private final UserDetailsService userDetailsService;
private final Key accessSecretKey;
private final Key refreshSecretKey;
private final long refreshExpiration;
@@ -53,16 +45,12 @@ public class JwtTokenService {
public JwtTokenService(
JwtTokenUtil jwtTokenUtil,
JwtTokenGenerator jwtTokenGenerator,
UserPrincipalService userPrincipalService,
AgentUserRepository agentUserRepository,
AgentUserTokenRepository agentUserTokenRepository,
UserDetailsService userDetailsService,
JwtProperties jwtProperties
) {
this.jwtTokenUtil = jwtTokenUtil;
this.jwtTokenGenerator = jwtTokenGenerator;
this.userPrincipalService = userPrincipalService;
this.agentUserRepository = agentUserRepository;
this.agentUserTokenRepository = agentUserTokenRepository;
this.userDetailsService = userDetailsService;
this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret());
this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
@@ -91,17 +79,6 @@ public class JwtTokenService {
@Transactional
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
AgentUser agentUser = agentUserRepository.findById(user.getAgentUser().getId()).orElseThrow(() -> new EntityNotFoundException("AgentUser not found"));
agentUserTokenRepository.findById(agentUser.getId())
.ifPresentOrElse(
it -> it.updateRefreshToken(refreshToken),
() -> agentUserTokenRepository.save(
AgentUserToken.builder().agentUser(agentUser).refreshToken(refreshToken).build()
)
);
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
return refreshToken;
@@ -196,7 +173,7 @@ public class JwtTokenService {
*/
@Transactional(readOnly = true)
public Authentication getAuthentication(String token) {
UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token));
UserDetails principal = userDetailsService.loadUserByUsername(getUserPk(token));
return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
}

View File

@@ -5,13 +5,13 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.UserPrincipalService;
import lombok.RequiredArgsConstructor;
@@ -20,14 +20,14 @@ import lombok.RequiredArgsConstructor;
public class UserAuthenticationProvider implements AuthenticationProvider {
private final PasswordEncoder passwordEncoder;
private final UserPrincipalService userPrincipalService;
private final UserDetailsService userDetailsService;
@Transactional(readOnly = true)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
UserDetails user = userPrincipalService.loadUserByUsername(username);
UserDetails user = userDetailsService.loadUserByUsername(username);
if (isNotMatches(password, user.getPassword())) {
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD.getMessage());
}

View File

@@ -0,0 +1,11 @@
package com.spring.infra.security.service;
import org.springframework.security.core.userdetails.UserDetails;
public interface RefreshTokenService {
void saveRefreshToken(UserDetails user, String refreshToken);
String findByRefreshToken(String refreshToken);
}