토큰 파싱

This commit is contained in:
Hyojin Ahn 2025-11-19 12:13:29 -05:00
parent 0b6b790425
commit a1ca9d1328
44 changed files with 597 additions and 1303 deletions

View File

@ -9,10 +9,10 @@
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.goi</groupId>
<artifactId>auth-service</artifactId>
<artifactId>crm-rest-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>Demo project for Spring Boot</description>
<name>Customer Service</name>
<description>Customer Service REST Api</description>
<properties>
<java.version>17</java.version>
</properties>

View File

@ -8,37 +8,11 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
@EntityScan(basePackages = {"com.goi.erp"})
@EnableJpaRepositories(basePackages = {"com.goi.erp"})
@EntityScan(basePackages = {"com.goi.erp.entity"})
@EnableJpaRepositories(basePackages = {"com.goi.erp.repository"})
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
// @Bean
// public CommandLineRunner commandLineRunner(
// AuthenticationService service
// ) {
// return args -> {
// var admin = RegisterRequest.builder()
// .firstname("Admin")
// .lastname("Admin")
// .email("admin@mail.com")
// .password("password")
// .role(ADMIN)
// .build();
// System.out.println("Admin token: " + service.register(admin).getAccessToken());
//
// var manager = RegisterRequest.builder()
// .firstname("Admin")
// .lastname("Admin")
// .email("manager@mail.com")
// .password("password")
// .role(MANAGER)
// .build();
// System.out.println("Manager token: " + service.register(manager).getAccessToken());
//
// };
// }
}

View File

@ -1,30 +0,0 @@
package com.goi.erp.auditing;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.goi.erp.employee.EmployeeDetails;
import java.util.Optional;
public class ApplicationAuditAware implements AuditorAware<Integer> {
@Override
public Optional<Integer> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null ||
!authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
// EmployeeDetails로 캐스팅
EmployeeDetails employeeDetails = (EmployeeDetails) authentication.getPrincipal();
// 내부 PK(empId) 반환
return Optional.ofNullable(employeeDetails.getEmployee().getEmpId());
}
}

View File

@ -1,37 +0,0 @@
package com.goi.erp.auth;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
// @PostMapping("/register")
// public ResponseEntity<AuthenticationResponse> register(
// @RequestBody RegisterRequest request
// ) {
// return ResponseEntity.ok(service.register(request));
// }
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
return ResponseEntity.ok(service.authenticate(request));
}
@PostMapping("/refresh-token")
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
service.refreshToken(request, response);
}
}

View File

@ -1,16 +0,0 @@
package com.goi.erp.auth;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String empLoginId; // 기존 email 대신
private String empLoginPassword;
}

View File

@ -1,19 +0,0 @@
package com.goi.erp.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("refresh_token")
private String refreshToken;
}

View File

@ -1,158 +0,0 @@
package com.goi.erp.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goi.erp.config.JwtService;
import com.goi.erp.token.Token;
import com.goi.erp.token.TokenRepository;
import com.goi.erp.token.TokenType;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeRepository;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.role.RolePermissionRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
private final TokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final RolePermissionRepository rolePermissionRepository;
private final JwtService jwtService;
// private final AuthenticationManager authenticationManager;
// public AuthenticationResponse register(RegisterRequest request) {
// var user = User.builder().firstname(request.getFirstname()).lastname(request.getLastname())
// .email(request.getEmail()).password(passwordEncoder.encode(request.getPassword()))
// .role(request.getRole()).build();
// var savedUser = repository.save(user);
// var jwtToken = jwtService.generateToken(user);
// var refreshToken = jwtService.generateRefreshToken(user);
// saveUserToken(savedUser, jwtToken);
// return AuthenticationResponse.builder().accessToken(jwtToken).refreshToken(refreshToken).build();
// }
// 로그인 처리
public AuthenticationResponse authenticate(AuthenticationRequest request) {
// 1. Employee 조회
Employee employee = employeeRepository.findByEmpLoginId(request.getEmpLoginId())
.orElseThrow(() -> new RuntimeException("Employee not found"));
// 2. 비밀번호 검증
if (!passwordEncoder.matches(request.getEmpLoginPassword(), employee.getEmpLoginPassword())) {
throw new RuntimeException("Invalid password");
}
// 3. EmployeeRole 조회 Role 이름 리스트 생성
List<EmployeeRole> activeRoles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
List<String> roles = activeRoles.stream()
.map(er -> er.getRoleInfo().getRoleName())
.collect(Collectors.toList());
// 4. Role Permission 조회
List<String> permissions = activeRoles.stream()
.flatMap(er -> rolePermissionRepository.findByRoleId(er.getRoleInfo().getRoleId()).stream())
.map(rp -> rp.getPermissionInfo().getPermModule() + ":" + rp.getPermissionInfo().getPermAction() + ":" + rp.getPermissionInfo().getPermScope())
.distinct()
.collect(Collectors.toList());
// 5. generate token
String jwtToken = jwtService.generateToken(employee, roles, permissions);
String refreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
// 기존 토큰 회수 토큰 저장
revokeAllEmployeeTokens(employee);
saveEmployeeToken(employee, jwtToken);
return AuthenticationResponse.builder()
.accessToken(jwtToken)
.refreshToken(refreshToken)
.build();
}
// JWT 토큰 저장
private void saveEmployeeToken(Employee employee, String jwtToken) {
Token token = Token.builder()
.employee(employee)
.token(jwtToken)
.tokenType(TokenType.BEARER)
.expired(false)
.revoked(false)
.build();
tokenRepository.save(token);
}
// 기존 토큰 회수
private void revokeAllEmployeeTokens(Employee employee) {
List<Token> validTokens = tokenRepository.findAllValidTokenByEmployee(employee.getEmpId());
if (validTokens.isEmpty()) return;
validTokens.forEach(token -> {
token.setExpired(true);
token.setRevoked(true);
});
tokenRepository.saveAll(validTokens);
}
// 리프레시 토큰 처리
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return;
}
final String refreshToken = authHeader.substring(7);
String empLoginId = jwtService.extractUsername(refreshToken);
if (empLoginId != null) {
Employee employee = employeeRepository.findByEmpLoginId(empLoginId)
.orElseThrow(() -> new RuntimeException("Employee not found"));
if (jwtService.isTokenValid(refreshToken, employee)) {
// 3. EmployeeRole 조회 Role 이름 리스트 생성
List<EmployeeRole> activeRoles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
List<String> roles = activeRoles.stream()
.map(er -> er.getRoleInfo().getRoleName())
.collect(Collectors.toList());
// 4. Role Permission 조회
List<String> permissions = activeRoles.stream()
.flatMap(er -> rolePermissionRepository.findByRoleId(er.getRoleInfo().getRoleId()).stream())
.map(rp -> rp.getPermissionInfo().getPermModule() + ":" + rp.getPermissionInfo().getPermAction() + ":" + rp.getPermissionInfo().getPermScope())
.distinct()
.collect(Collectors.toList());
// 5. generate token
String accessToken = jwtService.generateToken(employee, roles, permissions);
revokeAllEmployeeTokens(employee);
saveEmployeeToken(employee, accessToken);
AuthenticationResponse authResponse = AuthenticationResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
new ObjectMapper().writeValue(response.getOutputStream(), authResponse);
}
}
}
}

View File

@ -1,21 +0,0 @@
package com.goi.erp.auth;
import com.goi.erp.user.Role;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String firstname;
private String lastname;
private String email;
private String password;
private Role role;
}

View File

@ -1,83 +1,21 @@
package com.goi.erp.config;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.goi.erp.auditing.ApplicationAuditAware;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.employee.EmployeeDetails;
import com.goi.erp.employee.EmployeeRepository;
import com.goi.erp.token.ApplicationAuditAware;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Employee employee = employeeRepository.findByEmpLoginId(username)
.orElseThrow(() -> new UsernameNotFoundException("Employee not found"));
// EmployeeRole 조회
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
// EmployeeDetails 생성
return new EmployeeDetails(employee, roles);
};
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Value("${application.security.jwt.secret-key}")
private String jwtSecret;
@Bean
public AuditorAware<Integer> auditorAware() {
return new ApplicationAuditAware();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
String rawPassword = "1111"; // 테스트할 비밀번호
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String hashedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Raw password: " + rawPassword);
System.out.println("Hashed password: " + hashedPassword);
// 확인용 matches 테스트
boolean matches = passwordEncoder.matches(rawPassword, hashedPassword);
System.out.println("Matches: " + matches);
return new ApplicationAuditAware(jwtSecret);
}
}

View File

@ -1,15 +1,11 @@
package com.goi.erp.config;
import com.goi.erp.employee.Employee;
import com.goi.erp.employee.EmployeeDetails;
import com.goi.erp.employee.EmployeeRepository;
import com.goi.erp.employee.EmployeeRole;
import com.goi.erp.employee.EmployeeRoleRepository;
import com.goi.erp.token.TokenRepository;
import com.goi.erp.token.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@ -18,19 +14,15 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
private final TokenRepository tokenRepository;
@Override
protected void doFilterInternal(
@ -39,52 +31,33 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@NonNull FilterChain filterChain
) throws ServletException, IOException {
// 인증 API는 필터링 제외
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
final String jwt;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
final String empUuidStr = jwtService.extractUsername(jwt);
final String jwt = authHeader.substring(7);
if (empUuidStr != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UUID empUuid = UUID.fromString(empUuidStr);
if (SecurityContextHolder.getContext().getAuthentication() == null && jwtService.isTokenValid(jwt)) {
// Employee 조회
Employee employee = employeeRepository.findByEmpUuid(empUuid)
.orElseThrow(() -> new RuntimeException("Employee not found"));
// Role 조회
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
EmployeeDetails employeeDetails = new EmployeeDetails(employee, roles);
// 토큰에서 empUuid와 권한 추출
String empUuid = jwtService.extractEmpUuid(jwt);
List<String> permissions = jwtService.extractPermissions(jwt);
List<SimpleGrantedAuthority> authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// DB 토큰 검증
boolean isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElse(false);
// JWT 유효성 검증 + DB 토큰 검증
if (jwtService.isTokenValid(jwt, employee) && isTokenValid) {
// 인증 정보 SecurityContextHolder에 세팅
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
employeeDetails,
null,
employeeDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
empUuid, // principal
null, // credentials
authorities
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}

View File

@ -1,132 +0,0 @@
package com.goi.erp.config;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.goi.erp.employee.Employee;
@Service
public class JwtService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
@Value("${application.security.jwt.refresh-token.expiration}")
private long refreshExpiration;
// =================== 기존 UserDetails용 ===================
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(Employee employee, List<String> roles, List<String> permissions) {
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", roles);
// Admin 계정 여부 확인
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
if (isAdmin) {
// Admin이면 permissions를 ALL로 단순화
extraClaims.put("permissions", List.of("ALL"));
} else {
// 일반 계정이면 상세 권한 넣기
extraClaims.put("permissions", permissions);
}
return buildToken(extraClaims, employee.getEmpUuid().toString(), jwtExpiration);
}
public String generateRefreshToken(Employee employee, List<String> roles, List<String> permissions) {
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("roles", roles);
// Admin 계정 여부 확인
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
if (isAdmin) {
// Admin이면 permissions를 ALL로 단순화
extraClaims.put("permissions", List.of("ALL"));
} else {
// 일반 계정이면 상세 권한 넣기
extraClaims.put("permissions", permissions);
}
return buildToken(extraClaims, employee.getEmpUuid().toString(), refreshExpiration);
}
private String buildToken(Map<String, Object> extraClaims, String subject, long expiration) {
return Jwts.builder().setClaims(extraClaims).setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256).compact();
}
public boolean isTokenValid(String token, Employee employee) {
final String username = extractUsername(token);
return (username.equals(employee.getEmpUuid().toString())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public static void main(String[] args) {
JwtService jwtService = new JwtService();
jwtService.secretKey = "D0HaHnTPKLkUO9ULL1Ulm6XDZjhzuFtvTCcxTxSoCS8=";
String token = "eyJhbGciOiJIUzI1NiJ9.eyJwZXJtaXNzaW9ucyI6WyJIOlI6UCIsIk86QzpBIiwiTzpSOkEiLCJPOlU6QSIsIk86RDpBIiwiUzpDOkEiLCJTOlI6QSIsIlM6VTpBIl0sInJvbGVzIjpbIk9wZXJhdGlvbnMgTWFuYWdlciJdLCJzdWIiOiJmZGE1NGZkZS03MTBmLTQ4ZDItYTRmYi00NzM2YjJhM2RhNWEiLCJpYXQiOjE3NjMxMzU4MzMsImV4cCI6MTc2MzIyMjIzM30.ie38b2JnkP3k4Vz7TzAwI7oRgOsIFYf0yMYADq5EhNM";
// user 정보
Claims claims = jwtService.extractAllClaims(token);
System.out.println("Subject (emp_uuid): " + claims.getSubject());
System.out.println("Roles: " + claims.get("roles"));
System.out.println("Roles: " + claims.get("permissions"));
System.out.println("IssuedAt: " + claims.getIssuedAt());
System.out.println("Expiration: " + claims.getExpiration());
// 모든 Claims 확인
// Claims claims = Jwts.parserBuilder()
// .setSigningKey(Keys.hmacShaKeyFor("<secret_key>".getBytes()))
// .build()
// .parseClaimsJws(token)
// .getBody();
System.out.println("Claims: " + claims);
}
}

View File

@ -1,40 +0,0 @@
package com.goi.erp.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
import com.goi.erp.token.TokenRepository;
@Service
@RequiredArgsConstructor
public class LogoutService implements LogoutHandler {
private final TokenRepository tokenRepository;
@Override
public void logout(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) {
final String authHeader = request.getHeader("Authorization");
final String jwt;
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
return;
}
jwt = authHeader.substring(7);
var storedToken = tokenRepository.findByToken(jwt)
.orElse(null);
if (storedToken != null) {
storedToken.setExpired(true);
storedToken.setRevoked(true);
tokenRepository.save(storedToken);
SecurityContextHolder.clearContext();
}
}
}

View File

@ -0,0 +1,42 @@
package com.goi.erp.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableMethodSecurity // @PreAuthorize 사용 가능
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter; // JWT 인증 필터
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 비활성화 (API 서버라면 stateless)
.csrf(csrf -> csrf.disable())
// 세션 사용 안함
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 요청 권한 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/swagger-ui/**",
"/v3/api-docs/**"
).permitAll() // 인증 없이 접근 허용
.anyRequest().authenticated() // 나머지는 JWT 인증 필요
)
// JWT 필터 등록
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@ -1,81 +0,0 @@
package com.goi.erp.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import static com.goi.erp.user.Permission.ADMIN_CREATE;
import static com.goi.erp.user.Permission.ADMIN_DELETE;
import static com.goi.erp.user.Permission.ADMIN_READ;
import static com.goi.erp.user.Permission.ADMIN_UPDATE;
import static com.goi.erp.user.Permission.MANAGER_CREATE;
import static com.goi.erp.user.Permission.MANAGER_DELETE;
import static com.goi.erp.user.Permission.MANAGER_READ;
import static com.goi.erp.user.Permission.MANAGER_UPDATE;
import static com.goi.erp.user.Role.ADMIN;
import static com.goi.erp.user.Role.MANAGER;
import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {
private static final String[] WHITE_LIST_URL = {
"/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"};
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(req ->
req.requestMatchers(WHITE_LIST_URL)
.permitAll()
.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
.requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())
.requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())
.requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())
.anyRequest()
.authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout ->
logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
)
;
return http.build();
}
}

View File

@ -1,4 +1,4 @@
package com.goi.erp.demo;
package com.goi.erp.controller;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -0,0 +1,55 @@
package com.goi.erp.controller;
import com.goi.erp.dto.CustomerRequestDto;
import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.service.CustomerService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {
private final CustomerService customerService;
// CREATE
@PostMapping
public ResponseEntity<CustomerResponseDto> createCustomer(@RequestBody CustomerRequestDto requestDto) {
CustomerResponseDto responseDto = customerService.createCustomer(requestDto);
return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}
// READ ALL
@GetMapping
public ResponseEntity<List<CustomerResponseDto>> getAllCustomers() {
return ResponseEntity.ok(customerService.getAllCustomers());
}
// READ ONE
@GetMapping("/{uuid}")
public ResponseEntity<CustomerResponseDto> getCustomer(@PathVariable UUID uuid) {
return ResponseEntity.ok(customerService.getCustomerByUuid(uuid));
}
// UPDATE
@PutMapping("/{uuid}")
public ResponseEntity<CustomerResponseDto> updateCustomer(
@PathVariable UUID uuid,
@RequestBody CustomerRequestDto requestDto) {
return ResponseEntity.ok(customerService.updateCustomer(uuid, requestDto));
}
// DELETE
@DeleteMapping("/{uuid}")
public ResponseEntity<Void> deleteCustomer(@PathVariable UUID uuid) {
customerService.deleteCustomer(uuid);
return ResponseEntity.noContent().build();
}
}

View File

@ -1,19 +0,0 @@
package com.goi.erp.demo;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/demo-controller")
@Hidden
public class DemoController {
@GetMapping
public ResponseEntity<String> sayHello() {
return ResponseEntity.ok("Hello from secured endpoint");
}
}

View File

@ -1,50 +0,0 @@
package com.goi.erp.demo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/management")
@Tag(name = "Management")
public class ManagementController {
@Operation(
description = "Get endpoint for manager",
summary = "This is a summary for management get endpoint",
responses = {
@ApiResponse(
description = "Success",
responseCode = "200"
),
@ApiResponse(
description = "Unauthorized / Invalid Token",
responseCode = "403"
)
}
)
@GetMapping
public String get() {
return "GET:: management controller";
}
@PostMapping
public String post() {
return "POST:: management controller";
}
@PutMapping
public String put() {
return "PUT:: management controller";
}
@DeleteMapping
public String delete() {
return "DELETE:: management controller";
}
}

View File

@ -0,0 +1,30 @@
package com.goi.erp.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
@Data
public class CustomerRequestDto {
private String cusNo;
private String cusName;
private String cusStatus;
private Integer cusAreaId;
private String cusAddress1;
private String cusAddress2;
private String cusPostalCode;
private String cusCity;
private String cusProvince;
private BigDecimal cusGeoLat;
private BigDecimal cusGeoLon;
private String cusEmail;
private String cusPhone;
private String cusPhoneExt;
private LocalTime cusOpenTime;
private String cusNote;
private LocalDate cusContractDate;
private String cusContractedBy;
private LocalDate cusInstallDate;
private String cusInstallLocatoin;
}

View File

@ -0,0 +1,34 @@
package com.goi.erp.dto;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.UUID;
@Data
@Builder
public class CustomerResponseDto {
private UUID cusUuid;
private String cusNo;
private String cusName;
private String cusStatus;
private Integer cusAreaId;
private String cusAddress1;
private String cusAddress2;
private String cusPostalCode;
private String cusCity;
private String cusProvince;
private BigDecimal cusGeoLat;
private BigDecimal cusGeoLon;
private String cusEmail;
private String cusPhone;
private String cusPhoneExt;
private LocalTime cusOpenTime;
private String cusNote;
private LocalDate cusContractDate;
private String cusContractedBy;
private LocalDate cusInstallDate;
private String cusInstallLocatoin;
}

View File

@ -1,62 +0,0 @@
package com.goi.erp.employee;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Getter
public class EmployeeDetails implements UserDetails {
// UID는 객체를 직렬화 다시 역직렬화할 , 클래스 버전이 맞는지 확인하는 용도
private static final long serialVersionUID = 1L;
private final Employee employee;
private final List<GrantedAuthority> authorities;
public EmployeeDetails(Employee employee, List<EmployeeRole> roles) {
this.employee = employee;
// EmployeeRole GrantedAuthority 변환
this.authorities = roles.stream()
.map(er -> new SimpleGrantedAuthority(er.getRoleInfo().getRoleName()))
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return employee.getEmpLoginPassword();
}
@Override
public String getUsername() {
return employee.getEmpLoginId();
}
@Override
public boolean isAccountNonExpired() {
return true; // 필요에 따라 Employee 상태로 제어 가능
}
@Override
public boolean isAccountNonLocked() {
return !"I".equals(employee.getEmpStatus()); // : 'I' 잠금
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return "A".equals(employee.getEmpStatus()); // Active 상태만 사용 가능
}
}

View File

@ -1,30 +0,0 @@
package com.goi.erp.employee;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class EmployeeDetailsService implements UserDetailsService {
private final EmployeeRepository employeeRepository;
private final EmployeeRoleRepository employeeRoleRepository;
@Override
public UserDetails loadUserByUsername(String empLoginId) throws UsernameNotFoundException {
// Employee 조회
Employee employee = employeeRepository.findByEmpLoginId(empLoginId)
.orElseThrow(() -> new UsernameNotFoundException("Employee not found"));
// EmployeeRole 조회 (활성화된 역할만)
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
// EmployeeDetails에 Employee + 역할 전달
return new EmployeeDetails(employee, roles);
}
}

View File

@ -1,13 +0,0 @@
package com.goi.erp.employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
Optional<Employee> findByEmpLoginId(String empLoginId);
Optional<Employee> findByEmpUuid(UUID empUuid);
}

View File

@ -1,49 +0,0 @@
package com.goi.erp.employee;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.UUID;
import com.goi.erp.role.RoleInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "employee_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EmployeeRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emr_id")
private Integer emrId; // 내부 PK
@Column(name = "emr_uuid", unique = true, nullable = false)
private UUID emrUuid; // 외부용 UUID
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emr_emp_id", nullable = false) // DB 컬럼명 지정
private Employee employee;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emr_role_id", nullable = false) // DB 컬럼명 지정
private RoleInfo roleInfo;
@Column(name = "emr_revoked_at")
private LocalDateTime emrRevokedAt;
}

View File

@ -1,19 +0,0 @@
package com.goi.erp.employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface EmployeeRoleRepository extends JpaRepository<EmployeeRole, Integer> {
@Query("""
SELECT er
FROM EmployeeRole er
WHERE er.employee.id = :empId
AND er.emrRevokedAt IS NULL
""")
List<EmployeeRole> findActiveRolesByEmployeeId(Integer empId);
}

View File

@ -0,0 +1,93 @@
package com.goi.erp.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.UUID;
@Entity
@Table(name = "customer")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer cusId;
@Column(nullable = false, unique = true)
private UUID cusUuid;
@Column(length = 50)
private String cusNo;
@Column(nullable = false)
private String cusName;
@Column(length = 1)
private String cusStatus;
private Integer cusAreaId;
private String cusAddress1;
private String cusAddress2;
private String cusPostalCode;
private String cusCity;
private String cusProvince;
private BigDecimal cusGeoLat;
private BigDecimal cusGeoLon;
private String cusEmail;
private String cusPhone;
private String cusPhoneExt;
private LocalTime cusOpenTime;
private String cusNote;
private LocalDate cusContractDate;
private String cusContractedBy;
private LocalDate cusInstallDate;
private String cusInstallLocatoin;
private LocalDate cusLastPickupDate;
private Integer cusLastPickupQty;
private BigDecimal cusLastPickupLat;
private BigDecimal cusLastPickupLon;
private Integer cusFullCycle;
private Boolean cusFullCycleFlag;
private Integer cusFullCycleForced;
private LocalDate cusFullCycleForcedDate;
private String cusSchedule;
private String cusScheduledays;
private LocalDate cusLastPaidDate;
private BigDecimal cusRate;
private String cusPayMethod;
private String cusAccountNo;
private LocalDate cusIsccDate;
private LocalDate cusCorsiaDate;
private String cusHstNo;
private LocalDate cusTerminatedDate;
private String cusTerminationReason;
}

View File

@ -1,4 +1,4 @@
package com.goi.erp.employee;
package com.goi.erp.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@ -28,22 +28,10 @@ public class Employee {
@Column(name = "emp_uuid", unique = true, nullable = false)
private UUID empUuid; // 외부 키로 사용
@Column(name = "emp_login_id", unique = true, nullable = false)
private String empLoginId;
@Column(name = "emp_login_password", nullable = false)
private String empLoginPassword;
@Column(name = "emp_first_name")
private String empFirstName;
@Column(name = "emp_last_name")
private String empLastName;
@Column(name = "emp_dept_id")
private Integer empDeptId;
@Column(name = "emp_status", columnDefinition = "CHAR(1)")
private String empStatus;
}

View File

@ -1,46 +0,0 @@
package com.goi.erp.permission;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "permission_info",
uniqueConstraints = @UniqueConstraint(columnNames = {"perm_module", "perm_action", "perm_scope"}))
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PermissionInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "perm_id")
private Integer permId;
@Column(name = "perm_uuid", nullable = false, updatable = false)
private UUID permUuid;
@Column(name = "perm_module", nullable = false)
private String permModule;
@Column(name = "perm_action", nullable = false, length = 1)
private String permAction; // C/R/U/D
@Column(name = "perm_scope", nullable = false, length = 10)
private String permScope; // Self / Part / All
@Column(name = "perm_desc")
private String permDesc;
}

View File

@ -1,15 +0,0 @@
package com.goi.erp.permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface PermissionInfoRepository extends JpaRepository<PermissionInfo, Integer> {
Optional<PermissionInfo> findByPermUuid(UUID permUuid);
// 특정 모듈 + 액션 + 범위에 해당하는 권한 조회
Optional<PermissionInfo> findByPermModuleAndPermActionAndPermScope(String module, String action, String scope);
}

View File

@ -0,0 +1,13 @@
package com.goi.erp.repository;
import com.goi.erp.entity.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
Optional<Customer> findByCusUuid(UUID uuid);
}

View File

@ -1,48 +0,0 @@
package com.goi.erp.role;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "role_info")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RoleInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Integer roleId;
@Column(name = "role_uuid", unique = true, nullable = false)
private UUID roleUuid;
@Column(name = "role_name", unique = true, nullable = false)
private String roleName;
@Column(name = "role_desc")
private String roleDesc;
@Column(name = "role_level")
private Integer roleLevel;
@OneToMany(mappedBy = "roleInfo", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<RolePermission> rolePermissions;
}

View File

@ -1,14 +0,0 @@
package com.goi.erp.role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface RoleInfoRepository extends JpaRepository<RoleInfo, Integer> {
Optional<RoleInfo> findByRoleUuid(UUID roleUuid);
Optional<RoleInfo> findByRoleName(String roleName);
}

View File

@ -1,40 +0,0 @@
package com.goi.erp.role;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import com.goi.erp.permission.PermissionInfo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "role_permission")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RolePermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "rpr_id")
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rpr_role_id", nullable = false)
private RoleInfo roleInfo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rpr_perm_id", nullable = false)
private PermissionInfo permissionInfo;
}

View File

@ -1,26 +0,0 @@
package com.goi.erp.role;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface RolePermissionRepository extends JpaRepository<RolePermission, Integer> {
// 특정 Role의 모든 권한 조회
@Query("SELECT r FROM RolePermission r WHERE r.roleInfo.id = :roleId")
List<RolePermission> findByRoleId(@Param("roleId") Integer roleId);
// Role과 Permission 매핑 존재 여부 확인
@Query("""
SELECT CASE WHEN COUNT(r) > 0 THEN true ELSE false END
FROM RolePermission r
WHERE r.roleInfo.id = :roleId
AND r.permissionInfo.id = :permId
""")
boolean existsByRoleInfoAndPermissionInfo(@Param("roleId") Integer roleId,
@Param("permId") Integer permId);
}

View File

@ -0,0 +1,77 @@
package com.goi.erp.service;
import com.goi.erp.dto.CustomerRequestDto;
import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.entity.Customer;
import com.goi.erp.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
public CustomerResponseDto createCustomer(CustomerRequestDto dto) {
Customer customer = Customer.builder()
.cusUuid(UUID.randomUUID())
.cusName(dto.getCusName())
.cusNo(dto.getCusNo())
.cusStatus(dto.getCusStatus())
.cusAddress1(dto.getCusAddress1())
.cusAddress2(dto.getCusAddress2())
.cusCity(dto.getCusCity())
.cusProvince(dto.getCusProvince())
.build();
customer = customerRepository.save(customer);
return mapToDto(customer);
}
public List<CustomerResponseDto> getAllCustomers() {
return customerRepository.findAll().stream()
.map(this::mapToDto)
.collect(Collectors.toList());
}
public CustomerResponseDto getCustomerByUuid(UUID uuid) {
Customer customer = customerRepository.findByCusUuid(uuid)
.orElseThrow(() -> new RuntimeException("Customer not found"));
return mapToDto(customer);
}
public CustomerResponseDto updateCustomer(UUID uuid, CustomerRequestDto dto) {
Customer customer = customerRepository.findByCusUuid(uuid)
.orElseThrow(() -> new RuntimeException("Customer not found"));
customer.setCusName(dto.getCusName());
customer.setCusNo(dto.getCusNo());
customer.setCusStatus(dto.getCusStatus());
customer.setCusAddress1(dto.getCusAddress1());
customer.setCusAddress2(dto.getCusAddress2());
customer.setCusCity(dto.getCusCity());
customer.setCusProvince(dto.getCusProvince());
customerRepository.save(customer);
return mapToDto(customer);
}
public void deleteCustomer(UUID uuid) {
Customer customer = customerRepository.findByCusUuid(uuid)
.orElseThrow(() -> new RuntimeException("Customer not found"));
customerRepository.delete(customer);
}
private CustomerResponseDto mapToDto(Customer customer) {
return CustomerResponseDto.builder()
.cusUuid(customer.getCusUuid())
.cusName(customer.getCusName())
.cusNo(customer.getCusNo())
.cusStatus(customer.getCusStatus())
.build();
}
}

View File

@ -0,0 +1,85 @@
package com.goi.erp.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.data.domain.AuditorAware;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* auth-service에서 발급한 JWT 토큰 기반으로 현재 사용자(empId) 가져오는 AuditorAware 구현체
*
* - JPA auditing에서 사용자가 누군지 기록할 사용
* - SecurityContextHolder 없이도 동작 가능
* - HttpServletRequest에서 Authorization 헤더를 읽어 토큰 파싱
*/
public class ApplicationAuditAware implements AuditorAware<Integer> {
private final String jwtSecret;
public ApplicationAuditAware(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
/**
* 현재 요청을 수행하는 사용자의 empId 반환
* @return Optional<Integer> - empId가 없거나 토큰이 유효하지 않으면 Optional.empty()
*/
@Override
public Optional<Integer> getCurrentAuditor() {
HttpServletRequest request = getCurrentHttpRequest();
if (request == null) {
return Optional.empty();
}
String token = resolveToken(request);
if (token == null) {
return Optional.empty();
}
try {
// JWT 파싱
Claims claims = Jwts.parserBuilder()
// HMAC SHA 객체 생성 (secret 길이와 안전성 체크)
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token)
.getBody();
// 토큰에 empId 클레임이 있어야
Integer empId = claims.get("empId", Integer.class);
return Optional.ofNullable(empId);
} catch (Exception e) {
// 토큰 파싱/검증 실패 Optional.empty() 반환
return Optional.empty();
}
}
/**
* 현재 스레드의 HttpServletRequest 가져오기
* @return HttpServletRequest 또는 null
*/
private HttpServletRequest getCurrentHttpRequest() {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) return null;
return attrs.getRequest();
}
/**
* HttpServletRequest에서 Authorization 헤더의 Bearer 토큰 추출
* @param request 현재 HttpServletRequest
* @return JWT 문자열 또는 null
*/
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

View File

@ -0,0 +1,129 @@
package com.goi.erp.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.List;
import java.util.function.Function;
/**
* - DB 접근 없음
* - JWT에 포함된 empUuid, 이름, roles, permissions 추출
* - 토큰 유효기간 체크 가능
*/
@Service
public class JwtService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
/**
* empUuid(sub) 추출
*/
public String extractEmpUuid(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* firstName 추출
*/
public String extractFirstName(String token) {
return extractClaim(token, claims -> claims.get("firstName", String.class));
}
/**
* lastName 추출
*/
public String extractLastName(String token) {
return extractClaim(token, claims -> claims.get("lastName", String.class));
}
/**
* roles 리스트 추출
*/
@SuppressWarnings("unchecked")
public List<String> extractRoles(String token) {
return extractClaim(token, claims -> (List<String>) claims.get("roles"));
}
/**
* permissions 리스트 추출
*/
@SuppressWarnings("unchecked")
public List<String> extractPermissions(String token) {
return extractClaim(token, claims -> (List<String>) claims.get("permissions"));
}
/**
* 토큰 만료 여부 확인
*/
public boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new java.util.Date());
}
/**
* 토큰 유효성 검사 (만료 체크)
*/
public boolean isTokenValid(String token) {
return !isTokenExpired(token);
}
/**
* JWT에서 Claims 추출
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* JWT 전체 Claims 추출
*/
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey); // auth-service와 동일한 Base64 secret
return Keys.hmacShaKeyFor(keyBytes);
}
public static void main(String[] args) {
JwtService jwtService = new JwtService();
jwtService.secretKey = "D0HaHnTPKLkUO9ULL1Ulm6XDZjhzuFtvTCcxTxSoCS8=";
String token = "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdE5hbWUiOiJIeW9KaW4iLCJsYXN0TmFtZSI6IkFobiIsInBlcm1pc3Npb25zIjpbIkFMTCJdLCJyb2xlcyI6WyJBZG1pbiJdLCJzdWIiOiJhMjM4ZWQ0OC03OGRjLTQ2YWEtOWNiMC1hNWYxZGQyZDJmMTMiLCJpYXQiOjE3NjM1NzE2ODYsImV4cCI6MTc2MzY1ODA4Nn0.0fQT-I4dqHMapIEbxfy8X_SdCReYhUy6djnXUzw4gWc";
// user 정보
Claims claims = jwtService.extractAllClaims(token);
System.out.println("Subject (emp_uuid): " + claims.getSubject());
System.out.println("Roles: " + claims.get("roles"));
System.out.println("Roles: " + claims.get("permissions"));
System.out.println("IssuedAt: " + claims.getIssuedAt());
System.out.println("Expiration: " + claims.getExpiration());
System.out.println("FirstName: " + claims.get("firstName", String.class));
System.out.println("LastName: " + claims.get("LastName", String.class));
// 모든 Claims 확인
// Claims claims = Jwts.parserBuilder()
// .setSigningKey(Keys.hmacShaKeyFor("<secret_key>".getBytes()))
// .build()
// .parseClaimsJws(token)
// .getBody();
System.out.println("Claims: " + claims);
}
}

View File

@ -1,50 +0,0 @@
package com.goi.erp.token;
import com.goi.erp.employee.Employee;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "employee_token")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Token {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "emt_id")
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emt_emp_id", nullable = false)
private Employee employee;
@Column(name = "emt_token", nullable = false)
private String token;
@Enumerated(EnumType.STRING)
@Column(name = "emt_token_type")
private TokenType tokenType;
@Column(name = "emt_is_expired")
private boolean expired;
@Column(name = "emt_is_revoked")
private boolean revoked;
}

View File

@ -1,22 +0,0 @@
package com.goi.erp.token;
import com.goi.erp.employee.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface TokenRepository extends JpaRepository<Token, Integer> {
@Query("""
select t from Token t
where t.employee.id = :employeeId
and (t.expired = false or t.revoked = false)
""")
List<Token> findAllValidTokenByEmployee(Integer employeeId);
Optional<Token> findByToken(String token);
List<Token> findByEmployee(Employee employee);
}

View File

@ -1,5 +0,0 @@
package com.goi.erp.token;
public enum TokenType {
BEARER
}

View File

@ -1,15 +0,0 @@
package com.goi.erp.user;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ChangePasswordRequest {
private String currentPassword;
private String newPassword;
private String confirmationPassword;
}

View File

@ -1,22 +0,0 @@
package com.goi.erp.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"),
ADMIN_DELETE("admin:delete"),
MANAGER_READ("management:read"),
MANAGER_UPDATE("management:update"),
MANAGER_CREATE("management:create"),
MANAGER_DELETE("management:delete")
;
@Getter
private final String permission;
}

View File

@ -1,59 +0,0 @@
package com.goi.erp.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static com.goi.erp.user.Permission.ADMIN_CREATE;
import static com.goi.erp.user.Permission.ADMIN_DELETE;
import static com.goi.erp.user.Permission.ADMIN_READ;
import static com.goi.erp.user.Permission.ADMIN_UPDATE;
import static com.goi.erp.user.Permission.MANAGER_CREATE;
import static com.goi.erp.user.Permission.MANAGER_DELETE;
import static com.goi.erp.user.Permission.MANAGER_READ;
import static com.goi.erp.user.Permission.MANAGER_UPDATE;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public enum Role {
USER(Collections.emptySet()),
ADMIN(
Set.of(
ADMIN_READ,
ADMIN_UPDATE,
ADMIN_DELETE,
ADMIN_CREATE,
MANAGER_READ,
MANAGER_UPDATE,
MANAGER_DELETE,
MANAGER_CREATE
)
),
MANAGER(
Set.of(
MANAGER_READ,
MANAGER_UPDATE,
MANAGER_DELETE,
MANAGER_CREATE
)
)
;
@Getter
private final Set<Permission> permissions;
public List<SimpleGrantedAuthority> getAuthorities() {
var authorities = getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}

View File

@ -13,7 +13,8 @@ spring:
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
application:
security:
jwt:
@ -22,6 +23,6 @@ application:
refresh-token:
expiration: 604800000 # 7 days
server:
port: 8080
port: 8082
servlet:
context-path: /auth-service
context-path: /crm-rest-api