[Auth] Changed JWT into Cookie
This commit is contained in:
parent
78d836dfa3
commit
68233402fc
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.goi.erp.auth;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AuthCookieService {
|
||||||
|
|
||||||
|
public static final String AUTH_COOKIE_NAME = "AUTH_TOKEN";
|
||||||
|
public static final String REFRESH_COOKIE_NAME = "REFRESH_TOKEN";
|
||||||
|
|
||||||
|
@Value("${auth.cookie.secure:true}")
|
||||||
|
private boolean secure;
|
||||||
|
|
||||||
|
@Value("${auth.cookie.same-site:Lax}")
|
||||||
|
private String sameSite;
|
||||||
|
|
||||||
|
@Value("${auth.cookie.path:/}")
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
// ===================== Access Token =====================
|
||||||
|
|
||||||
|
public void addAuthCookie(HttpServletResponse response, String jwt) {
|
||||||
|
addCookie(response, AUTH_COOKIE_NAME, jwt, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAuthCookie(HttpServletResponse response) {
|
||||||
|
addCookie(response, AUTH_COOKIE_NAME, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> extractJwt(HttpServletRequest request) {
|
||||||
|
return extractCookie(request, AUTH_COOKIE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Refresh Token =====================
|
||||||
|
|
||||||
|
public void addRefreshCookie(HttpServletResponse response, String refreshToken) {
|
||||||
|
addCookie(response, REFRESH_COOKIE_NAME, refreshToken, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearRefreshCookie(HttpServletResponse response) {
|
||||||
|
addCookie(response, REFRESH_COOKIE_NAME, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> extractRefreshToken(HttpServletRequest request) {
|
||||||
|
return extractCookie(request, REFRESH_COOKIE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 내부 공통 =====================
|
||||||
|
|
||||||
|
private void addCookie(
|
||||||
|
HttpServletResponse response,
|
||||||
|
String name,
|
||||||
|
String value,
|
||||||
|
int maxAge
|
||||||
|
) {
|
||||||
|
Cookie cookie = new Cookie(name, value);
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
cookie.setSecure(secure);
|
||||||
|
cookie.setPath(path);
|
||||||
|
cookie.setMaxAge(maxAge);
|
||||||
|
|
||||||
|
response.addCookie(cookie);
|
||||||
|
addSameSiteAttribute(response, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> extractCookie(HttpServletRequest request, String name) {
|
||||||
|
if (request.getCookies() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.stream(request.getCookies())
|
||||||
|
.filter(c -> name.equals(c.getName()))
|
||||||
|
.map(Cookie::getValue)
|
||||||
|
.filter(v -> v != null && !v.isBlank())
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SameSite 수동 세팅 (Servlet Cookie API 한계)
|
||||||
|
*/
|
||||||
|
private void addSameSiteAttribute(HttpServletResponse response, Cookie cookie) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(cookie.getName()).append("=");
|
||||||
|
sb.append(cookie.getValue() == null ? "" : cookie.getValue());
|
||||||
|
sb.append("; Path=").append(cookie.getPath());
|
||||||
|
sb.append("; HttpOnly");
|
||||||
|
|
||||||
|
if (cookie.getSecure()) {
|
||||||
|
sb.append("; Secure");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sameSite != null && !sameSite.isBlank()) {
|
||||||
|
sb.append("; SameSite=").append(sameSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookie.getMaxAge() == 0) {
|
||||||
|
sb.append("; Max-Age=0");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addHeader("Set-Cookie", sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
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;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
@ -16,27 +13,37 @@ import java.io.IOException;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthenticationController {
|
public class AuthenticationController {
|
||||||
|
|
||||||
private final AuthenticationService service;
|
private final AuthenticationService authenticationService;
|
||||||
|
|
||||||
// @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")
|
@PostMapping("/login")
|
||||||
public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
public ResponseEntity<AuthenticationResponse> authenticate(
|
||||||
service.refreshToken(request, response);
|
@RequestBody AuthenticationRequest request,
|
||||||
}
|
HttpServletResponse response
|
||||||
|
) {
|
||||||
|
AuthenticationResponse authResponse = authenticationService.authenticate(request, response);
|
||||||
|
return ResponseEntity.ok(authResponse);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/authenticate/system")
|
// ===================== Refresh Token =====================
|
||||||
public AuthenticationResponse authenticateSystem(@RequestBody SystemAuthenticationRequestDto request) {
|
|
||||||
return service.authenticateSystem(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@PostMapping("/token/refresh")
|
||||||
|
public void refreshToken(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws IOException {
|
||||||
|
authenticationService.refreshToken(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== System Token =====================
|
||||||
|
|
||||||
|
@PostMapping("/login/system")
|
||||||
|
public ResponseEntity<SystemAuthenticationResponseDto> authenticateSystem(
|
||||||
|
@RequestBody SystemAuthenticationRequestDto request
|
||||||
|
) {
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
authenticationService.authenticateSystem(request)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
package com.goi.erp.auth;
|
package com.goi.erp.auth;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class AuthenticationResponse {
|
public class AuthenticationResponse {
|
||||||
|
|
||||||
@JsonProperty("access_token")
|
private boolean authenticated;
|
||||||
private String accessToken;
|
|
||||||
@JsonProperty("refresh_token")
|
private String loginId;
|
||||||
private String refreshToken;
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
|
||||||
|
private List<String> roles;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,190 +1,204 @@
|
||||||
package com.goi.erp.auth;
|
package com.goi.erp.auth;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.goi.erp.common.exception.InvalidPasswordException;
|
import com.goi.erp.common.exception.InvalidPasswordException;
|
||||||
import com.goi.erp.common.exception.UserNotFoundException;
|
import com.goi.erp.common.exception.UserNotFoundException;
|
||||||
import com.goi.erp.config.JwtService;
|
import com.goi.erp.config.JwtService;
|
||||||
import com.goi.erp.config.SecuritySystemClientsProperties;
|
import com.goi.erp.config.SecuritySystemClientsProperties;
|
||||||
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.Employee;
|
||||||
import com.goi.erp.employee.EmployeeRepository;
|
import com.goi.erp.employee.EmployeeRepository;
|
||||||
import com.goi.erp.employee.EmployeeRole;
|
import com.goi.erp.employee.EmployeeRole;
|
||||||
import com.goi.erp.employee.EmployeeRoleRepository;
|
import com.goi.erp.employee.EmployeeRoleRepository;
|
||||||
import com.goi.erp.role.RolePermissionRepository;
|
import com.goi.erp.role.RolePermissionRepository;
|
||||||
|
import com.goi.erp.token.Token;
|
||||||
|
import com.goi.erp.token.TokenRepository;
|
||||||
|
import com.goi.erp.token.TokenType;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Transactional
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthenticationService {
|
public class AuthenticationService {
|
||||||
private final EmployeeRepository employeeRepository;
|
|
||||||
|
private final EmployeeRepository employeeRepository;
|
||||||
private final EmployeeRoleRepository employeeRoleRepository;
|
private final EmployeeRoleRepository employeeRoleRepository;
|
||||||
|
private final RolePermissionRepository rolePermissionRepository;
|
||||||
private final TokenRepository tokenRepository;
|
private final TokenRepository tokenRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final RolePermissionRepository rolePermissionRepository;
|
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final SecuritySystemClientsProperties systemClientsProperties;
|
private final SecuritySystemClientsProperties systemClientsProperties;
|
||||||
// private final AuthenticationManager authenticationManager;
|
private final AuthCookieService authCookieService;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
// 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(
|
||||||
public AuthenticationResponse authenticate(AuthenticationRequest request) {
|
AuthenticationRequest request,
|
||||||
// 1. Employee 조회
|
HttpServletResponse response
|
||||||
|
) {
|
||||||
Employee employee = employeeRepository.findByEmpLoginId(request.getEmpLoginId())
|
Employee employee = employeeRepository.findByEmpLoginId(request.getEmpLoginId())
|
||||||
.orElseThrow(() -> new UserNotFoundException("Employee not found"));
|
.orElseThrow(() -> new UserNotFoundException("Employee not found"));
|
||||||
|
|
||||||
// 2. 비밀번호 검증
|
|
||||||
if (!passwordEncoder.matches(request.getEmpLoginPassword(), employee.getEmpLoginPassword())) {
|
if (!passwordEncoder.matches(request.getEmpLoginPassword(), employee.getEmpLoginPassword())) {
|
||||||
throw new InvalidPasswordException("Invalid password");
|
throw new InvalidPasswordException("Invalid password");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. EmployeeRole 조회 → Role 이름 리스트 생성
|
List<EmployeeRole> activeRoles =
|
||||||
List<EmployeeRole> activeRoles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
|
employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
|
||||||
|
|
||||||
List<String> roles = activeRoles.stream()
|
List<String> roles = extractRoles(activeRoles);
|
||||||
.map(er -> er.getRoleInfo().getRoleName())
|
List<String> permissions = extractPermissions(activeRoles);
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
|
String accessToken = jwtService.generateToken(employee, roles, permissions);
|
||||||
// 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);
|
String refreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
|
||||||
|
|
||||||
// 기존 토큰 회수 및 새 토큰 저장
|
|
||||||
revokeAllEmployeeTokens(employee);
|
revokeAllEmployeeTokens(employee);
|
||||||
saveEmployeeToken(employee, jwtToken);
|
// refresh 도 저장
|
||||||
|
saveEmployeeToken(employee, accessToken, TokenType.ACCESS);
|
||||||
|
saveEmployeeToken(employee, refreshToken, TokenType.REFRESH);
|
||||||
|
|
||||||
|
|
||||||
|
// Cookie로 내려줌
|
||||||
|
authCookieService.addAuthCookie(response, accessToken);
|
||||||
|
authCookieService.addRefreshCookie(response, refreshToken);
|
||||||
|
|
||||||
return AuthenticationResponse.builder()
|
return AuthenticationResponse.builder()
|
||||||
.accessToken(jwtToken)
|
.authenticated(true)
|
||||||
.refreshToken(refreshToken)
|
.loginId(employee.getEmpLoginId())
|
||||||
|
.firstName(employee.getEmpFirstName())
|
||||||
|
.lastName(employee.getEmpLastName())
|
||||||
|
.roles(roles)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT 토큰 저장
|
// ===================== Refresh Token =====================
|
||||||
private void saveEmployeeToken(Employee employee, String jwtToken) {
|
|
||||||
|
public void refreshToken(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
|
||||||
|
String refreshToken = authCookieService
|
||||||
|
.extractRefreshToken(request)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (refreshToken == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String loginId = jwtService.extractUsername(refreshToken);
|
||||||
|
if (loginId == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails userDetails = userDetailsService.loadUserByUsername(loginId);
|
||||||
|
|
||||||
|
if (!jwtService.isTokenValid(refreshToken, userDetails)) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Employee employee = employeeRepository.findByEmpLoginId(loginId)
|
||||||
|
.orElseThrow(() -> new UserNotFoundException("Employee not found"));
|
||||||
|
|
||||||
|
List<EmployeeRole> activeRoles =
|
||||||
|
employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
|
||||||
|
|
||||||
|
List<String> roles = extractRoles(activeRoles);
|
||||||
|
List<String> permissions = extractPermissions(activeRoles);
|
||||||
|
|
||||||
|
|
||||||
|
String newAccessToken = jwtService.generateToken(employee, roles, permissions);
|
||||||
|
String newRefreshToken = jwtService.generateRefreshToken(employee, roles, permissions);
|
||||||
|
|
||||||
|
// 기존 토큰 전부 revoke
|
||||||
|
revokeAllEmployeeTokens(employee);
|
||||||
|
|
||||||
|
// 새 토큰 저장
|
||||||
|
saveEmployeeToken(employee, newAccessToken, TokenType.ACCESS);
|
||||||
|
saveEmployeeToken(employee, newRefreshToken, TokenType.REFRESH);
|
||||||
|
|
||||||
|
// 쿠키 갱신
|
||||||
|
authCookieService.addAuthCookie(response, newAccessToken);
|
||||||
|
authCookieService.addRefreshCookie(response, newRefreshToken);
|
||||||
|
|
||||||
|
// body 없음
|
||||||
|
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===================== System Token =====================
|
||||||
|
// OPR / CRM 등 서버간 호출용 (Header 기반)
|
||||||
|
|
||||||
|
public SystemAuthenticationResponseDto authenticateSystem(SystemAuthenticationRequestDto request) {
|
||||||
|
|
||||||
|
if (request.getClientId() == null || request.getClientSecret() == null) {
|
||||||
|
throw new InvalidPasswordException("Missing client credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientConfig = systemClientsProperties.getClients().get(request.getClientId());
|
||||||
|
if (clientConfig == null) {
|
||||||
|
throw new InvalidPasswordException("Invalid system client");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientConfig.getSecret().equals(request.getClientSecret())) {
|
||||||
|
throw new InvalidPasswordException("Invalid system secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
String jwtToken = jwtService.generateSystemToken(
|
||||||
|
request.getClientId(),
|
||||||
|
clientConfig.getPermissions()
|
||||||
|
);
|
||||||
|
|
||||||
|
return SystemAuthenticationResponseDto.builder()
|
||||||
|
.accessToken(jwtToken)
|
||||||
|
.refreshToken(null)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===================== 내부 헬퍼 =====================
|
||||||
|
|
||||||
|
private List<String> extractRoles(List<EmployeeRole> roles) {
|
||||||
|
return roles.stream()
|
||||||
|
.map(er -> er.getRoleInfo().getRoleName())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractPermissions(List<EmployeeRole> roles) {
|
||||||
|
return roles.stream()
|
||||||
|
.flatMap(er -> rolePermissionRepository
|
||||||
|
.findByRoleId(er.getRoleInfo().getRoleId()).stream())
|
||||||
|
.map(rp -> rp.getPermissionInfo().getPermModule()
|
||||||
|
+ ":" + rp.getPermissionInfo().getPermAction()
|
||||||
|
+ ":" + rp.getPermissionInfo().getPermScope())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveEmployeeToken(Employee employee, String jwtToken, TokenType tokenType) {
|
||||||
Token token = Token.builder()
|
Token token = Token.builder()
|
||||||
.employee(employee)
|
.employee(employee)
|
||||||
.token(jwtToken)
|
.token(jwtToken)
|
||||||
.tokenType(TokenType.BEARER)
|
.tokenType(tokenType)
|
||||||
.expired(false)
|
.expired(false)
|
||||||
.revoked(false)
|
.revoked(false)
|
||||||
.build();
|
.build();
|
||||||
tokenRepository.save(token);
|
tokenRepository.save(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기존 토큰 회수
|
|
||||||
private void revokeAllEmployeeTokens(Employee employee) {
|
private void revokeAllEmployeeTokens(Employee employee) {
|
||||||
List<Token> validTokens = tokenRepository.findAllValidTokenByEmployee(employee.getEmpId());
|
tokenRepository.revokeAllValidTokensByEmployee(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 시스템 토큰 발급 (OPR/CRM 등 서버간 호출용)
|
|
||||||
public AuthenticationResponse authenticateSystem(SystemAuthenticationRequestDto request) {
|
|
||||||
|
|
||||||
if (request.getClientId() == null || request.getClientSecret() == null) {
|
|
||||||
throw new InvalidPasswordException("Missing client credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
var clientConfig = systemClientsProperties.getClients().get(request.getClientId());
|
|
||||||
if (clientConfig == null) {
|
|
||||||
throw new InvalidPasswordException("Invalid system client");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clientConfig.getSecret().equals(request.getClientSecret())) {
|
|
||||||
throw new InvalidPasswordException("Invalid system secret");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> permissions = clientConfig.getPermissions(); // 예: ["H:R:A"]
|
|
||||||
|
|
||||||
String jwtToken = jwtService.generateSystemToken(request.getClientId(), permissions);
|
|
||||||
|
|
||||||
// system token은 보통 DB(TokenRepository)에 저장/폐기(revoke) 안 함 (짧게 발급 + 캐싱)
|
|
||||||
return AuthenticationResponse.builder()
|
|
||||||
.accessToken(jwtToken)
|
|
||||||
.refreshToken(null)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
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 SystemAuthenticationResponseDto {
|
||||||
|
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
}
|
||||||
|
|
@ -1,91 +1,93 @@
|
||||||
package com.goi.erp.config;
|
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 lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import com.goi.erp.auth.AuthCookieService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final EmployeeRepository employeeRepository;
|
private final UserDetailsService userDetailsService;
|
||||||
private final EmployeeRoleRepository employeeRoleRepository;
|
private final AuthCookieService authCookieService;
|
||||||
private final TokenRepository tokenRepository;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@NonNull HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
@NonNull HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
@NonNull FilterChain filterChain
|
FilterChain filterChain
|
||||||
) throws ServletException, IOException {
|
) throws ServletException, IOException {
|
||||||
|
|
||||||
// 인증 API는 필터링 제외
|
if (SecurityContextHolder.getContext().getAuthentication() != null) {
|
||||||
if (request.getServletPath().contains("/api/v1/auth")) {
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String authHeader = request.getHeader("Authorization");
|
String jwt = resolveToken(request);
|
||||||
final String jwt;
|
|
||||||
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (jwt == null) {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt = authHeader.substring(7);
|
String username;
|
||||||
final String empUuidStr = jwtService.extractUsername(jwt);
|
try {
|
||||||
|
username = jwtService.extractUsername(jwt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (empUuidStr != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
UUID empUuid = UUID.fromString(empUuidStr);
|
|
||||||
|
|
||||||
// Employee 조회
|
if (!jwtService.isTokenValid(jwt, userDetails)) {
|
||||||
Employee employee = employeeRepository.findByEmpUuid(empUuid)
|
filterChain.doFilter(request, response);
|
||||||
.orElseThrow(() -> new RuntimeException("Employee not found"));
|
return;
|
||||||
// Role 조회
|
}
|
||||||
List<EmployeeRole> roles = employeeRoleRepository.findActiveRolesByEmployeeId(employee.getEmpId());
|
|
||||||
EmployeeDetails employeeDetails = new EmployeeDetails(employee, roles);
|
|
||||||
|
|
||||||
// DB 토큰 검증
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
boolean isTokenValid = tokenRepository.findByToken(jwt)
|
new UsernamePasswordAuthenticationToken(
|
||||||
.map(t -> !t.isExpired() && !t.isRevoked())
|
userDetails,
|
||||||
.orElse(false);
|
null,
|
||||||
|
userDetails.getAuthorities()
|
||||||
// JWT 유효성 검증 + DB 토큰 검증
|
|
||||||
if (jwtService.isTokenValid(jwt, employee) && isTokenValid) {
|
|
||||||
UsernamePasswordAuthenticationToken authToken =
|
|
||||||
new UsernamePasswordAuthenticationToken(
|
|
||||||
employee.getEmpLoginId(), // principal → loginId
|
|
||||||
null,
|
|
||||||
employeeDetails.getAuthorities()
|
|
||||||
);
|
|
||||||
authToken.setDetails(
|
|
||||||
new WebAuthenticationDetailsSource().buildDetails(request)
|
|
||||||
);
|
);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
authentication.setDetails(
|
||||||
|
new WebAuthenticationDetailsSource().buildDetails(request)
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 추출 우선순위:
|
||||||
|
* 1. Authorization Header
|
||||||
|
* 2. HttpOnly Cookie
|
||||||
|
*/
|
||||||
|
private String resolveToken(HttpServletRequest request) {
|
||||||
|
|
||||||
|
String authHeader = request.getHeader("Authorization");
|
||||||
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||||
|
return authHeader.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> cookieToken = authCookieService.extractJwt(request);
|
||||||
|
return cookieToken.orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.io.Decoders;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -13,6 +14,7 @@ import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.goi.erp.employee.Employee;
|
import com.goi.erp.employee.Employee;
|
||||||
|
|
@ -20,7 +22,7 @@ import com.goi.erp.employee.Employee;
|
||||||
@Service
|
@Service
|
||||||
public class JwtService {
|
public class JwtService {
|
||||||
|
|
||||||
@Value("${application.security.jwt.secret-key}")
|
@Value("${application.security.jwt.secret-key}")
|
||||||
private String secretKey;
|
private String secretKey;
|
||||||
|
|
||||||
@Value("${application.security.jwt.expiration}")
|
@Value("${application.security.jwt.expiration}")
|
||||||
|
|
@ -29,107 +31,84 @@ public class JwtService {
|
||||||
@Value("${application.security.jwt.refresh-token.expiration}")
|
@Value("${application.security.jwt.refresh-token.expiration}")
|
||||||
private long refreshExpiration;
|
private long refreshExpiration;
|
||||||
|
|
||||||
// =================== 기존 UserDetails용 ===================
|
// =================== 공통 ===================
|
||||||
public String extractUsername(String token) {
|
|
||||||
return extractClaim(token, Claims::getSubject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
public String extractUsername(String token) {
|
||||||
final Claims claims = extractAllClaims(token);
|
return extractClaim(token, Claims::getSubject);
|
||||||
return claimsResolver.apply(claims);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String generateToken(Employee employee, List<String> roles, List<String> permissions) {
|
public <T> T extractClaim(String token, Function<Claims, T> resolver) {
|
||||||
Map<String, Object> extraClaims = new HashMap<>();
|
return resolver.apply(extractAllClaims(token));
|
||||||
extraClaims.put("roles", roles);
|
}
|
||||||
|
|
||||||
// Admin 계정 여부 확인
|
// =================== Token 생성 ===================
|
||||||
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
|
|
||||||
|
|
||||||
if (isAdmin) {
|
public String generateToken(Employee employee, List<String> roles, List<String> permissions) {
|
||||||
// Admin이면 permissions를 ALL로 단순화
|
return buildEmployeeToken(employee, roles, permissions, jwtExpiration);
|
||||||
extraClaims.put("permissions", List.of("ALL"));
|
}
|
||||||
} else {
|
|
||||||
// 일반 계정이면 상세 권한 넣기
|
|
||||||
extraClaims.put("permissions", permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 직원 이름 추가
|
public String generateRefreshToken(Employee employee, List<String> roles, List<String> permissions) {
|
||||||
extraClaims.put("firstName", employee.getEmpFirstName());
|
return buildEmployeeToken(employee, roles, permissions, refreshExpiration);
|
||||||
extraClaims.put("lastName", employee.getEmpLastName());
|
}
|
||||||
extraClaims.put("loginId", employee.getEmpLoginId());
|
|
||||||
|
|
||||||
return buildToken(extraClaims, employee.getEmpUuid().toString(), jwtExpiration);
|
private String buildEmployeeToken(
|
||||||
}
|
Employee employee,
|
||||||
|
List<String> roles,
|
||||||
|
List<String> permissions,
|
||||||
|
long expiration
|
||||||
|
) {
|
||||||
|
Map<String, Object> claims = new HashMap<>();
|
||||||
|
claims.put("roles", roles);
|
||||||
|
|
||||||
public String generateSystemToken(String clientId, java.util.List<String> permissions) {
|
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
|
||||||
Map<String, Object> extraClaims = new HashMap<>();
|
claims.put("permissions", isAdmin ? List.of("ALL") : permissions);
|
||||||
extraClaims.put("permissions", permissions);
|
|
||||||
extraClaims.put("loginId", clientId);
|
|
||||||
return buildToken(extraClaims, clientId, jwtExpiration);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
claims.put("empUuid", employee.getEmpUuid().toString());
|
||||||
|
claims.put("firstName", employee.getEmpFirstName());
|
||||||
|
claims.put("lastName", employee.getEmpLastName());
|
||||||
|
|
||||||
public String generateRefreshToken(Employee employee, List<String> roles, List<String> permissions) {
|
// 🔥 subject는 loginId
|
||||||
Map<String, Object> extraClaims = new HashMap<>();
|
return buildToken(claims, employee.getEmpLoginId(), expiration);
|
||||||
extraClaims.put("roles", roles);
|
}
|
||||||
// Admin 계정 여부 확인
|
|
||||||
boolean isAdmin = roles.stream().anyMatch(r -> r.equalsIgnoreCase("Admin"));
|
|
||||||
|
|
||||||
if (isAdmin) {
|
public String generateSystemToken(String clientId, List<String> permissions) {
|
||||||
// Admin이면 permissions를 ALL로 단순화
|
Map<String, Object> claims = new HashMap<>();
|
||||||
extraClaims.put("permissions", List.of("ALL"));
|
claims.put("permissions", permissions);
|
||||||
} else {
|
return buildToken(claims, clientId, jwtExpiration);
|
||||||
// 일반 계정이면 상세 권한 넣기
|
}
|
||||||
extraClaims.put("permissions", permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 직원 이름 추가
|
// =================== Validation ===================
|
||||||
extraClaims.put("firstName", employee.getEmpFirstName());
|
|
||||||
extraClaims.put("lastName", employee.getEmpLastName());
|
|
||||||
extraClaims.put("loginId", employee.getEmpLoginId());
|
|
||||||
|
|
||||||
return buildToken(extraClaims, employee.getEmpUuid().toString(), refreshExpiration);
|
public boolean isTokenValid(String token, UserDetails userDetails) {
|
||||||
}
|
final String username = extractUsername(token);
|
||||||
|
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
private String buildToken(Map<String, Object> claims, String subject, long expiration) {
|
||||||
final String username = extractUsername(token);
|
return Jwts.builder()
|
||||||
return (username.equals(employee.getEmpUuid().toString())) && !isTokenExpired(token);
|
.setClaims(claims)
|
||||||
}
|
.setSubject(subject)
|
||||||
|
.setIssuedAt(new Date())
|
||||||
|
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
||||||
|
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTokenExpired(String token) {
|
private boolean isTokenExpired(String token) {
|
||||||
return extractExpiration(token).before(new Date());
|
return extractClaim(token, Claims::getExpiration).before(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Date extractExpiration(String token) {
|
private Claims extractAllClaims(String token) {
|
||||||
return extractClaim(token, Claims::getExpiration);
|
return Jwts.parserBuilder()
|
||||||
}
|
.setSigningKey(getSignInKey())
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
private Claims extractAllClaims(String token) {
|
private Key getSignInKey() {
|
||||||
return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
|
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = "";
|
|
||||||
|
|
||||||
String token = "";
|
|
||||||
|
|
||||||
// user 정보
|
|
||||||
Claims claims = jwtService.extractAllClaims(token);
|
|
||||||
|
|
||||||
System.out.println("Claims: " + claims);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,34 +7,42 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import com.goi.erp.token.TokenRepository;
|
import com.goi.erp.token.TokenRepository;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Transactional
|
||||||
public class LogoutService implements LogoutHandler {
|
public class LogoutService implements LogoutHandler {
|
||||||
|
|
||||||
private final TokenRepository tokenRepository;
|
private final TokenRepository tokenRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logout(
|
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
// 로그아웃 한 번에 ACCESS + REFRESH 전부 revoke
|
||||||
Authentication authentication
|
String tokenValue = null;
|
||||||
) {
|
|
||||||
final String authHeader = request.getHeader("Authorization");
|
if (request.getCookies() != null) {
|
||||||
final String jwt;
|
for (var cookie : request.getCookies()) {
|
||||||
if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
|
if ("AUTH_TOKEN".equals(cookie.getName())
|
||||||
return;
|
|| "REFRESH_TOKEN".equals(cookie.getName())) {
|
||||||
|
tokenValue = cookie.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenValue == null) return;
|
||||||
|
|
||||||
|
var tokenEntity = tokenRepository.findByToken(tokenValue).orElse(null);
|
||||||
|
if (tokenEntity == null) return;
|
||||||
|
|
||||||
|
tokenRepository.revokeAllValidTokensByEmployee(
|
||||||
|
tokenEntity.getEmployee().getEmpId()
|
||||||
|
);
|
||||||
|
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
}
|
}
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package com.goi.erp.config;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
|
@ -12,23 +14,28 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
import static com.goi.erp.user.Permission.ADMIN_CREATE;
|
//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_DELETE;
|
||||||
import static com.goi.erp.user.Permission.ADMIN_READ;
|
//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.ADMIN_UPDATE;
|
||||||
import static com.goi.erp.user.Permission.MANAGER_CREATE;
|
//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_DELETE;
|
||||||
import static com.goi.erp.user.Permission.MANAGER_READ;
|
//import static com.goi.erp.user.Permission.MANAGER_READ;
|
||||||
import static com.goi.erp.user.Permission.MANAGER_UPDATE;
|
//import static com.goi.erp.user.Permission.MANAGER_UPDATE;
|
||||||
import static com.goi.erp.user.Role.ADMIN;
|
//import static com.goi.erp.user.Role.ADMIN;
|
||||||
import static com.goi.erp.user.Role.MANAGER;
|
//import static com.goi.erp.user.Role.MANAGER;
|
||||||
import static org.springframework.http.HttpMethod.DELETE;
|
//import static org.springframework.http.HttpMethod.DELETE;
|
||||||
import static org.springframework.http.HttpMethod.GET;
|
//import static org.springframework.http.HttpMethod.GET;
|
||||||
import static org.springframework.http.HttpMethod.POST;
|
//import static org.springframework.http.HttpMethod.POST;
|
||||||
import static org.springframework.http.HttpMethod.PUT;
|
//import static org.springframework.http.HttpMethod.PUT;
|
||||||
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
|
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|
@ -51,18 +58,37 @@ public class SecurityConfiguration {
|
||||||
private final AuthenticationProvider authenticationProvider;
|
private final AuthenticationProvider authenticationProvider;
|
||||||
private final LogoutHandler logoutHandler;
|
private final LogoutHandler logoutHandler;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration config = new CorsConfiguration();
|
||||||
|
|
||||||
|
config.setAllowedOrigins(List.of("http://localhost:5173"));
|
||||||
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-CSRF-TOKEN"));
|
||||||
|
config.setAllowCredentials(true);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", config);
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
|
.cors(Customizer.withDefaults())
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.authorizeHttpRequests(req ->
|
.authorizeHttpRequests(req ->
|
||||||
req.requestMatchers(WHITE_LIST_URL)
|
req.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
|
.requestMatchers("/auth/login", "/auth/token/refresh", "/auth/login/system").permitAll()
|
||||||
|
.requestMatchers("/auth/logout").authenticated()
|
||||||
|
.requestMatchers(WHITE_LIST_URL)
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
|
// .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
|
||||||
.requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.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(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())
|
||||||
.requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.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())
|
// .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated()
|
.authenticated()
|
||||||
)
|
)
|
||||||
|
|
@ -70,7 +96,7 @@ public class SecurityConfiguration {
|
||||||
.authenticationProvider(authenticationProvider)
|
.authenticationProvider(authenticationProvider)
|
||||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
.logout(logout ->
|
.logout(logout ->
|
||||||
logout.logoutUrl("/api/v1/auth/logout")
|
logout.logoutUrl("/auth/logout")
|
||||||
.addLogoutHandler(logoutHandler)
|
.addLogoutHandler(logoutHandler)
|
||||||
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
|
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ import java.util.List;
|
||||||
public interface EmployeeRoleRepository extends JpaRepository<EmployeeRole, Integer> {
|
public interface EmployeeRoleRepository extends JpaRepository<EmployeeRole, Integer> {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT er
|
SELECT er
|
||||||
FROM EmployeeRole er
|
FROM EmployeeRole er
|
||||||
WHERE er.employee.id = :empId
|
JOIN FETCH er.roleInfo
|
||||||
AND er.emrRevokedAt IS NULL
|
WHERE er.employee.id = :empId
|
||||||
""")
|
AND er.emrRevokedAt IS NULL
|
||||||
List<EmployeeRole> findActiveRolesByEmployeeId(Long empId);
|
""")
|
||||||
|
List<EmployeeRole> findActiveRolesByEmployeeId(Long empId);
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.goi.erp.token;
|
||||||
|
|
||||||
import com.goi.erp.employee.Employee;
|
import com.goi.erp.employee.Employee;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -9,14 +10,26 @@ import java.util.Optional;
|
||||||
|
|
||||||
public interface TokenRepository extends JpaRepository<Token, Integer> {
|
public interface TokenRepository extends JpaRepository<Token, Integer> {
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
select t from Token t
|
select t from Token t
|
||||||
where t.employee.id = :employeeId
|
where t.employee.id = :employeeId
|
||||||
and (t.expired = false or t.revoked = false)
|
and t.expired = false
|
||||||
""")
|
and t.revoked = false
|
||||||
List<Token> findAllValidTokenByEmployee(Long employeeId);
|
""")
|
||||||
|
List<Token> findAllValidTokenByEmployee(Long employeeId);
|
||||||
|
|
||||||
Optional<Token> findByToken(String token);
|
Optional<Token> findByToken(String token);
|
||||||
|
|
||||||
List<Token> findByEmployee(Employee employee);
|
List<Token> findByEmployee(Employee employee);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("""
|
||||||
|
update Token t
|
||||||
|
set t.expired = true, t.revoked = true
|
||||||
|
where t.employee.id = :employeeId
|
||||||
|
and t.tokenType in ('ACCESS','REFRESH')
|
||||||
|
and t.expired = false
|
||||||
|
and t.revoked = false
|
||||||
|
""")
|
||||||
|
int revokeAllValidTokensByEmployee(Long employeeId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.goi.erp.token;
|
package com.goi.erp.token;
|
||||||
|
|
||||||
public enum TokenType {
|
public enum TokenType {
|
||||||
|
ACCESS,
|
||||||
|
REFRESH,
|
||||||
BEARER
|
BEARER
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,11 @@ application:
|
||||||
secret: ${OPR_SYSTEM_CLIENT_SECRET}
|
secret: ${OPR_SYSTEM_CLIENT_SECRET}
|
||||||
permissions:
|
permissions:
|
||||||
- "H:R:A"
|
- "H:R:A"
|
||||||
|
auth:
|
||||||
|
cookie:
|
||||||
|
secure: false # prod: true / local: false
|
||||||
|
same-site: Lax # 필요 시 None (→ Secure 필수)
|
||||||
|
path: /
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
servlet:
|
servlet:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue