chore: Remove obsolete .classpath file
- Deleted the .classpath file as it is no longer needed in the project structure. - This cleanup helps maintain a tidy codebase by removing unnecessary files that may cause confusion or clutter.
This commit is contained in:
@@ -1,248 +1,126 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 보안 가이드
|
||||
|
||||
## 인증 및 인가
|
||||
|
||||
### 세션 기반 인증
|
||||
현재 시스템은 세션 기반 인증을 사용합니다:
|
||||
### JWT 기반 인증
|
||||
현재 시스템은 JWT 토큰 기반 인증을 사용합니다:
|
||||
|
||||
```java
|
||||
// 사용자 인증 정보 저장
|
||||
PersonBean person = new PersonBean();
|
||||
person.setUserId(userId);
|
||||
person.setUserName(userName);
|
||||
request.getSession().setAttribute(Constants.PERSON_BEAN, person);
|
||||
```typescript
|
||||
// 토큰 생성 (로그인 시)
|
||||
const token = jwt.sign(
|
||||
{ userId, companyCode, role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
|
||||
// 인증 정보 조회
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
if (person == null) {
|
||||
// 로그인 페이지로 리다이렉트
|
||||
}
|
||||
// 토큰 검증 (미들웨어)
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
```
|
||||
|
||||
### 권한 관리 구조
|
||||
- **AUTH_GROUP**: 권한 그룹 정의
|
||||
- **MENU_AUTH_GROUP**: 메뉴별 권한 그룹 매핑
|
||||
- **USER_AUTH**: 사용자별 권한 할당
|
||||
- **auth_group**: 권한 그룹 정의
|
||||
- **menu_auth_group**: 메뉴별 권한 그룹 매핑
|
||||
- **user_auth**: 사용자별 권한 할당
|
||||
|
||||
### 메뉴 접근 권한 체크
|
||||
```java
|
||||
public HashMap<String, Object> checkUserMenuAuth(HttpServletRequest request, Map<String, Object> paramMap) {
|
||||
PersonBean person = (PersonBean)request.getSession().getAttribute(Constants.PERSON_BEAN);
|
||||
String userId = person.getUserId();
|
||||
|
||||
paramMap.put("userId", userId);
|
||||
paramMap.put("menuUrl", request.getRequestURI());
|
||||
|
||||
// 권한 체크 쿼리 실행
|
||||
return sqlSession.selectOne("admin.checkUserMenuAuth", paramMap);
|
||||
}
|
||||
```
|
||||
### 인증 미들웨어
|
||||
`backend-node/src/middleware/authMiddleware.ts` 참조
|
||||
|
||||
## 데이터 보안
|
||||
|
||||
### SQL 인젝션 방지
|
||||
**올바른 방법** - 파라미터 바인딩 사용:
|
||||
```xml
|
||||
<select id="selectUser" parameterType="map" resultType="map">
|
||||
SELECT * FROM user_info
|
||||
WHERE user_id = #{userId} <!-- 안전한 파라미터 바인딩 -->
|
||||
</select>
|
||||
```typescript
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM user_info WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
```
|
||||
|
||||
**위험한 방법** - 직접 문자열 치환:
|
||||
```xml
|
||||
<select id="selectUser" parameterType="map" resultType="map">
|
||||
SELECT * FROM user_info
|
||||
WHERE user_id = '${userId}' <!-- SQL 인젝션 위험 -->
|
||||
</select>
|
||||
**위험한 방법** - 직접 문자열 삽입:
|
||||
```typescript
|
||||
const result = await pool.query(
|
||||
`SELECT * FROM user_info WHERE user_id = '${userId}'`
|
||||
);
|
||||
```
|
||||
|
||||
### 패스워드 보안
|
||||
```java
|
||||
// 패스워드 암호화 (EncryptUtil 사용)
|
||||
String encryptedPassword = EncryptUtil.encrypt(plainPassword);
|
||||
```typescript
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// 패스워드 검증
|
||||
boolean isValid = EncryptUtil.matches(plainPassword, encryptedPassword);
|
||||
// 해싱
|
||||
const hashedPassword = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
// 검증
|
||||
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
|
||||
```
|
||||
|
||||
### 입력값 검증
|
||||
```java
|
||||
// CommonUtils를 사용한 입력값 정제
|
||||
String safeInput = CommonUtils.checkNull(request.getParameter("input"));
|
||||
if (StringUtils.isEmpty(safeInput)) {
|
||||
throw new IllegalArgumentException("필수 입력값이 누락되었습니다.");
|
||||
```typescript
|
||||
import Joi from 'joi';
|
||||
|
||||
const schema = Joi.object({
|
||||
userId: Joi.string().required().max(50),
|
||||
email: Joi.string().email().required(),
|
||||
});
|
||||
|
||||
const { error, value } = schema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({ success: false, message: error.message });
|
||||
}
|
||||
```
|
||||
|
||||
## 세션 보안
|
||||
## 보안 미들웨어
|
||||
|
||||
### 세션 설정
|
||||
[web.xml](mdc:WebContent/WEB-INF/web.xml)에서 세션 타임아웃 설정:
|
||||
```xml
|
||||
<session-config>
|
||||
<session-timeout>1440</session-timeout> <!-- 24시간 -->
|
||||
</session-config>
|
||||
### Helmet (보안 헤더)
|
||||
```typescript
|
||||
import helmet from 'helmet';
|
||||
app.use(helmet());
|
||||
```
|
||||
|
||||
### 세션 무효화
|
||||
```java
|
||||
// 로그아웃 시 세션 무효화
|
||||
@RequestMapping("/logout.do")
|
||||
public String logout(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.invalidate();
|
||||
}
|
||||
return "redirect:/login.do";
|
||||
}
|
||||
### Rate Limiting
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
```
|
||||
|
||||
## 파일 업로드 보안
|
||||
|
||||
### 파일 확장자 검증
|
||||
```java
|
||||
public boolean isAllowedFileType(String fileName) {
|
||||
String[] allowedExtensions = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx"};
|
||||
String extension = fileName.toLowerCase().substring(fileName.lastIndexOf("."));
|
||||
return Arrays.asList(allowedExtensions).contains(extension);
|
||||
}
|
||||
```
|
||||
|
||||
### 파일 크기 제한
|
||||
```java
|
||||
// web.xml에서 파일 업로드 크기 제한
|
||||
<multipart-config>
|
||||
<max-file-size>10485760</max-file-size> <!-- 10MB -->
|
||||
<max-request-size>52428800</max-request-size> <!-- 50MB -->
|
||||
</multipart-config>
|
||||
```
|
||||
|
||||
### 안전한 파일명 생성
|
||||
```java
|
||||
// FileRenameClass 사용하여 안전한 파일명 생성
|
||||
String safeFileName = FileRenameClass.rename(originalFileName);
|
||||
```
|
||||
|
||||
## XSS 방지
|
||||
|
||||
### 출력값 이스케이프
|
||||
JSP에서 사용자 입력값 출력 시:
|
||||
```jsp
|
||||
<!-- 안전한 출력 -->
|
||||
<c:out value="${userInput}" escapeXml="true"/>
|
||||
|
||||
<!-- 위험한 출력 -->
|
||||
${userInput} <!-- XSS 공격 가능 -->
|
||||
```
|
||||
|
||||
### JavaScript에서 데이터 처리
|
||||
```javascript
|
||||
// 안전한 방법
|
||||
var safeData = $('<div>').text(userInput).html();
|
||||
|
||||
// 위험한 방법
|
||||
var dangerousData = userInput; // XSS 공격 가능
|
||||
```
|
||||
|
||||
## CSRF 방지
|
||||
|
||||
### 토큰 기반 CSRF 방지
|
||||
```jsp
|
||||
<!-- 폼에 CSRF 토큰 포함 -->
|
||||
<form method="post" action="save.do">
|
||||
<input type="hidden" name="csrfToken" value="${csrfToken}"/>
|
||||
<!-- 기타 입력 필드 -->
|
||||
</form>
|
||||
```
|
||||
|
||||
```java
|
||||
// 컨트롤러에서 CSRF 토큰 검증
|
||||
@RequestMapping("/save.do")
|
||||
public String save(HttpServletRequest request, @RequestParam Map<String, Object> paramMap) {
|
||||
String sessionToken = (String)request.getSession().getAttribute("csrfToken");
|
||||
String requestToken = (String)paramMap.get("csrfToken");
|
||||
|
||||
if (!sessionToken.equals(requestToken)) {
|
||||
throw new SecurityException("CSRF 토큰이 일치하지 않습니다.");
|
||||
}
|
||||
|
||||
// 정상 처리
|
||||
}
|
||||
```
|
||||
|
||||
## 로깅 및 감사
|
||||
|
||||
### 보안 이벤트 로깅
|
||||
```java
|
||||
private static final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
|
||||
|
||||
public void logSecurityEvent(String event, String userId, String details) {
|
||||
securityLogger.info("Security Event: {} | User: {} | Details: {}", event, userId, details);
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
logSecurityEvent("LOGIN_SUCCESS", userId, request.getRemoteAddr());
|
||||
logSecurityEvent("ACCESS_DENIED", userId, request.getRequestURI());
|
||||
```
|
||||
|
||||
### 민감 정보 마스킹
|
||||
```java
|
||||
public String maskSensitiveData(String data) {
|
||||
if (data == null || data.length() < 4) {
|
||||
return "****";
|
||||
}
|
||||
return data.substring(0, 2) + "****" + data.substring(data.length() - 2);
|
||||
}
|
||||
```
|
||||
|
||||
## 환경별 보안 설정
|
||||
|
||||
### 개발 환경
|
||||
- 상세한 오류 메시지 표시
|
||||
- 디버그 모드 활성화
|
||||
- 보안 제약 완화
|
||||
|
||||
### 운영 환경
|
||||
- 일반적인 오류 메시지만 표시
|
||||
- 디버그 모드 비활성화
|
||||
- 엄격한 보안 정책 적용
|
||||
|
||||
```java
|
||||
// 환경별 설정 예시
|
||||
if (Constants.IS_PRODUCTION) {
|
||||
// 운영 환경 설정
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "접근이 거부되었습니다.");
|
||||
} else {
|
||||
// 개발 환경 설정
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "권한 부족: " + detailedMessage);
|
||||
}
|
||||
### CORS 설정
|
||||
```typescript
|
||||
import cors from 'cors';
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN,
|
||||
credentials: true,
|
||||
}));
|
||||
```
|
||||
|
||||
## 보안 체크리스트
|
||||
|
||||
### 코드 레벨
|
||||
- [ ] SQL 인젝션 방지 (#{} 파라미터 바인딩 사용)
|
||||
- [ ] XSS 방지 (출력값 이스케이프)
|
||||
- [ ] CSRF 방지 (토큰 검증)
|
||||
- [ ] 입력값 검증 및 정제
|
||||
- [ ] 패스워드 암호화 저장
|
||||
- [ ] SQL 인젝션 방지 ($1 파라미터 바인딩 사용)
|
||||
- [ ] XSS 방지 (React 자동 이스케이프 활용)
|
||||
- [ ] 입력값 검증 (Joi 스키마)
|
||||
- [ ] 패스워드 bcrypt 해싱
|
||||
- [ ] JWT 토큰 만료 설정
|
||||
|
||||
### 설정 레벨
|
||||
- [ ] 세션 타임아웃 설정
|
||||
- [ ] 파일 업로드 제한
|
||||
- [ ] 오류 페이지 설정
|
||||
- [ ] Helmet 보안 헤더
|
||||
- [ ] Rate Limiting 적용
|
||||
- [ ] CORS 적절한 설정
|
||||
- [ ] HTTPS 사용 (운영 환경)
|
||||
- [ ] 보안 헤더 설정
|
||||
- [ ] 환경 변수로 시크릿 관리
|
||||
|
||||
### 운영 레벨
|
||||
- [ ] 정기적인 보안 점검
|
||||
- [ ] 로그 모니터링
|
||||
- [ ] winston 로깅 모니터링
|
||||
- [ ] 권한 정기 검토
|
||||
- [ ] 패스워드 정책 적용
|
||||
- [ ] 백업 데이터 암호화
|
||||
- [ ] 백업 데이터 암호화
|
||||
|
||||
Reference in New Issue
Block a user