https://docs.google.com/spreadsheets/d/1-jm3A68VoKrOF2snpPbtCp1s3VI_Zyy08h_lnXnc9XE/edit?usp=sharing
- 프로필 이미지 추가
- 게시판 multipart/form-data 파일 업로드 부분 추가 -> XSS Filter
- Controller에서 요청방식(requestMapping, GetMapping, ...) 검토 및 수정
- Controller 부분 user_id로 getPrincipal() 가져오는 부분 수정
- jsp 페이지 내 _csrf token input -> header
- successHandler 추가, failureHandler 수정
- AccessDeniedHandler, AuthenticationEntryPointHandler 다른 방향으로 생각 -> 수정
- null 처리
- 아이디 찾기 버튼 클릭 시 "null" 로 받아오는 부분 수정
- SecurityConfig 로그아웃 부분 추가
- SecurityConfig sessionManagement 에서 동시 세션 관리(중복 로그인) -> 테스트하고 수정
- mapper.xml 에서 *로 select 해오는 부분 구체적으로 변경, parameterType, resultType 추가
- mybatis-config.xml 사용 X
- 불필요한 주석, import 제거
- header 추가
- 회원가입 시 메일 인증 추가
충전 페이지
- 결제 후 회원 cash 충전
- 충전 완료 시 충전 확인 안내 메일
마이페이지
- 수정하기 버튼 클릭 시 본인 인증 후 edit 페이지로 넘어가게 수정
- 닉네임 변경 기능 추가
- 비밀번호 변경 페이지 추가
- 프로필 이미지 변경 기능 추가
- 충전/사용 내역 추가
회원게시판
- 게시글 수정
- 게시글 삭제
- 회원 게시판 댓글 수정
- 회원 게시판 댓글 삭제
자료게시판
- 게시글 수정
- 게시글 삭제
- 조회 수 기능 추가
- 자료게시판 댓글 기능 추가
- 자료게시판 댓글 수정
- 자료게시판 댓글 삭제
- 자료게시판 리스트에서 댓글 수 표시
장바구니
- 좋아요 기능 추가
- 좋아요한 파일 장바구니에서 확인 가능
- 장바구니 페이지 추가
관리자 페이지
- 회원 리스트
- 회원 상태 변경 기능 추가
- 등록 게시글 없을 경우 "등록 게시글이 없습니다." 표시
- 등록 게시글 삭제하기 버튼 기능 추가
- 관리자 리스트
- 등록 게시글 없을 경우 "등록 게시글이 없습니다." 표시
- 게시글 승인 대기 리스트
- 비공개/공개로 전환 기능 수정
- 회원 댓글 리스트
- 회원 댓글 리스트 관리 페이지 추가
공지사항
- 공지사항 게시판 추가 (수정, 삭제, 등록)
- 공지사항 게시글 관리자 관리 추가
- 공지사항 게시글 댓글 기능 추가
- 게시글 관리에서 선택된 게시글 메인화면에 표시
- 공지사항 리스트에서 댓글 수 표시
- Filter삽입
- multipart/form-data
- json
- AOP를 활용한 URL별 권한 체크
- security 로그인 성공 핸들러
1. spring framework 사용
- 리스트/상세 페이지
2. db 연동
3. 로그인 / 로그아웃 / 회원가입 구현
4. Filter 삽입
- xssFilter 적용 ( 만들어도 되고, library 사용해도 됨 )
- XSS공격 케이스. 1-3번째 케이스 모두 Blocking 하거나 Replace 해야합니다. 구길섭 추가
- 첫번째, form 또는 input과 같은 html 태그에 스크립트 공격이 들어오는 경우
- 두번째, "multipart/form-data" 스크립트 공격이 들어오는 경우
- 세번째, json 형식의 데이터 전송에 스크립트 공격을 하는 경우
- 네번째, 웹소켓에서 스크립트 공격이 들어오는 경우 <- 제외. 이런케이스도 있다는 것만 알아두셈>
5. spring security 적용
- 로그인 / 로그아웃 기능 적용
- url 패턴별 권한 적용 예시)
-> 권한 : NOR_ADMIN, SYS_ADMIN
1번 URL : NOR_ADMIN/SYS_ADMIN,
2번 URL : SYS_ADMIN 권한 적용
A계정이 로그인 시 NOR_ADMIN 권한 부여 => 1번 URL에 접속 가능 / 2번 URL은 접속 안되도록 처리
6. security에 session 관리 적용
- 중복 로그인 가능으로 구현
- 중복 로그인 방지로 구현
7. security 로그인 성공/실패 핸들러 커스텀
8. AOP를 활용한 URL별 권한 체크 ( 5번의 URL 패턴별 권한 적용은 주석 후 무조건 인증으로 변경 )
9. resources 파일 환경 분리
- local / dev 2개의 환경으로 분리할 것
- maven / gradle 둘중 하나 사용
- 배포파일(.war) 생성 시 해당 profile에 대한 resource 파일만 포함 할 것
CREATE TABLE kahyun.board
(seq int(10) NOT NULL AUTO_INCREMENT comment 'PK',
title varchar(50) NOT NULL comment '제목',
content varchar(100) NOT NULL comment '내용',
nickname varchar(50) NOT NULL,
create_dt datetime NOT NULL comment '등록일',
update_dt datetime NULL comment '수정일',
status enum('N','D') NOT NULL DEFAULT 'N' comment '상태',
`view` int(10) NOT NULL DEFAULT 0 comment '조회 수',
user_seq int(10) NOT NULL comment '작성자 번호',
boardOption enum('boardOption1', 'boardOption2','boardOption3) NOT NULL DEFAULT 'boardOption1',
PRIMARY KEY (seq)
) ENGINE=INNODB DEFAULT charset=utf8
user_seq, boardOption 추가
CREATE TABLE kahyun.users
(seq int(10) NOT NULL AUTO_INCREMENT comment 'PK',
username varchar(50) NOT NULL comment '이름',
user_id varchar(50) NOT NULL comment '아이디',
nickname varchar(50) NOT NULL,
password varchar(100) NOT NULL comment '비밀번호',
reg_date datetime NULL comment '수정일',
status enum('N','D') NOT NULL DEFAULT 'N' comment '상태',
auth enum('USER','ADMIN') NOT NULL DEFAULT 'USER' comment '권한',
email varchar(50) NOT NULL comment '이메일',
PRIMARY KEY (seq)
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.comment
(seq int(10) NOT NULL AUTO_INCREMENT comment 'PK',
nickname varchar(50) NOT NULL comment 'nickname',
user_seq int(10) NOT NULL comment 'user seq',
content varchar(100) NOT NULL comment '댓글 내용',
create_dt datetime NULL comment '등록일',
status enum('N','D') NOT NULL DEFAULT 'N' comment '상태',
board_seq int(10) NOT NULL comment '게시글 seq',
PRIMARY KEY (seq)
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.charge_cash (
seq int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment 'PK',
buyer_seq int(10) NOT NULL comment '충전 유저 seq',
buyer_name varchar(10) NOT NULL comment '충전 유저 이름',
price varchar(100) NOT NULL comment '충전 금액',
buyer_email varchar(100) comment '충전 유저 이메일',
buyer_phone varchar(50) comment '충전 유저 전화 번호',
card_name varchar(50) comment '카드사',
embPgProvider varchar(50) comment '결제 경로'
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.use_cash (
seq int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment 'PK',
buyer_seq int(10) NOT NULL comment '구매 유저 seq',
buyer_name varchar(10) NOT NULL comment '구매 유저 이름',
price varchar(100) NOT NULL comment '구매 금액',
file_board_seq int(10) NOT NULL comment '구매 게시글 번호',
file_seq int(10) NOT NULL comment '구매 파일 번호',
file varchar(100) NOT NULL comment '구매 파일명'
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.refund_cash (
seq int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment 'PK',
buyer_seq int(10) NOT NULL comment '환불 유저 seq',
price varchar(100) NOT NULL comment '환불 금액',
buyer_email varchar(100) comment '환불 유저 이메일',
buyer_phone varchar(50) comment '환불 유저 전화 번호',
card_name varchar(50) comment '카드사',
embPgProvider varchar(50) comment '환불 경로',
refund_dt datetime comment '환불일자'
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.file (
seq int(10) PRIMARY KEY,
orgName varchar(50) NOT NULL,
savedName varchar(50) NOT NULL,
savedPath varchar(100),
s_board_seq varchar(100) NOT NULL comment '게시글 번호',
size varchar(100) comment '파일 크기',
delete_yn enum('N','Y') NOT NULL default 'N' comment '삭제 여부',
create_dt datetime NOT NULL comment '생성 일시',
) ENGINE=INNODB DEFAULT charset=utf8
CREATE TABLE kahyun.s_board
(seq int(10) NOT NULL AUTO_INCREMENT comment 'PK',
title varchar(50) NOT NULL comment '제목',
content varchar(100) NOT NULL comment '내용',
nickname varchar(50) NOT NULL,
create_dt datetime NOT NULL comment '등록일',
update_dt datetime NULL comment '수정일',
status enum('N','D') NOT NULL DEFAULT 'N' comment '상태',
`view` int(10) NOT NULL DEFAULT 0 comment '조회 수',
user_seq int(10) NOT NULL comment '등록자 번호',
files varchar(100) NULL,
PRIMARY KEY (seq)
) ENGINE=INNODB DEFAULT charset=utf8
INSERT INTO kahyun.board
(title, nickname, create_dt)
values("제목2", "nickname2", current_time())
INSERT INTO kahyun.comment
(nickname, user_seq, content, create_dt, board_seq)
values ('test', 6, '1번글의 댓글 2', CURRENT_TIME(), 1)
- 인증 (Authenticate)
- 보호된 리소스에 접근한 대상에 대해 이 유저가 누구인지, 애플리케이션의 작업을 수행해도 되는 주체인지 확인하는 과정 (예. 로그인)
- 권한 (Authorize)
- 인증된 사용자가 어떤 것을 할 수 있는지를 의미한다.
- 인가 (Authorization)
- 인증된 사용자가 요청된 자원에 접근 가능한지를 결정하는 절차
Authentication은 접근하는 주체의 정보와 권한을 담은 인터페이스로 SecurityContext에 저장된다.
SecurityContext : Authentication의 보관을 담당하며 해당 객체를 꺼내올 수 있다.
SecurityContextHolder : 보안 주체의 세부 정보를 포함하며, 현재 SecurityContext에 대한 세부 정보가 저장된다.
- username과 password를 통해 UsernamePasswordAuthenticationToken을 생성
- 생성된 토큰의 검증을 위해 AuthenticationManager의 인스턴스로 전달
- 인증에 성공하면 Authentication 인스턴스를 리턴하고 SecurityContextHolder에 저장
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
/*
정적 자원에 대해서 인증을 하지 않도록 설정 (resources에 접근)
permitAll은 필터를 거치지만 ignore()은 필터를 거치지 않는다. (permitAll 대신 ignore 사용 이유)
*/
return(web) -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
...
...
return http.build();
}
- 웹 사이트 취약점 공격 방지를 위해 사용, 스프링 시큐리티가 CSRF 토큰 값을 세션을 통해 발행하고 웹 페이지에서는 폼 전송 시에 해당 토큰을 함께 전송하여 실제 웹 페이지에서 작성된 데이터가 전달되는지를 검증하는 기술이다.
- CSRF 토큰을 설정해주지 않으면 jsp에서 보내는 POST 요청을 전부 막기 때문에 로그인, 회원가입 기능이 동작하지 않으며 인증 과정 진행이 불가하다.
JSP에서 POST 요청을 보낼 때 CSRF Token에 값을 넣어 함께 보내고 Spring security가 token 값을 확인하여 CSRF 공격 판단
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
로컬에서 확인을 위해 csrf 비활성화
.csrf(csrf -> csrf.disable())
403 -> alert 표시 후 메인으로 이동
- exceptionHandling(handler->handler.accessDeniedHandler(accessDeniedHandler))
implements AccessDeniedHandler
401 -> alert 표시 후 메인으로 이동
- .exceptionHandling(handler->handler.authenticationEntryPoint(authenticationEntryPoint))
implements AuthenticationEntryPoint
.formLogin(login -> login
.loginPage("/user/login")
.loginProcessingUrl("/user/loginForm")
// username : user_id, password : password
.usernameParameter("user_id")
.passwordParameter("password")
// 로그인 성공 시 defaultSuccessUrl 페이지로 이동
.defaultSuccessUrl("/")
/* 로그인 성공 핸들러 successHandler */
/*.successHandler(successHandler)*/
/*
로그인 실패 핸들러 failureHandler
: 실패한 인증 시도를 처리하는데 사용, 일반적으로 사용자를 인증 페이지로 리디렉션한다.
예외 유형에 따라 구현
*/
.failureHandler(failureHandler)
)
.sessionManagement(session -> session
.sessionFixation().changeSessionId()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/")
)
- 세션 고정 보호 sessionFixation()
- changeSessionId()
- 사용자가 인증을 시도하게 되면 사용자 세션은 그대로 두고 세션 아이디만 변경 -> 세션 아이디가 변경되어서 공격자가 갖고있는 세션 아이디는 무용지물이 된다.
- migrateSession()
- 새로운 세션을 생성하고 세션 아이디도 새로 발급하며 migration한다.
- newSession()
- 새로운 세션과 아이디를 생성하며 이전의 설정 값들을 사용 불가
- none()
- 아무런 보호를 하지 않는다.
- changeSessionId()
changeSessionId(), migrateSession()은 이전 세션의 설정 값들을 그대로 사용할 수 있지만 newsession()의 경우 새로운 세션을 생성하여 이전 설정 값을 사용할 수 없다.
-
세션 정책 설정 코드 sessionCreationPolicy
- SessionCreationPolicy.ALWAYS : 항상 세션을 생성
- SessionCreationPolicy.IF_REQUIRED : 필요 시 세션 생성
- SessionCreationPolicy.NEVER : 생성하진 않지만 존재 시 사용
- SessionCreationPolicy.STATELESS : 생성하지 않고 존재해도 사용하지 않는다.(예. JWT)
-
동시 세션 관리
- maximumSessions() : 최대 허용 가능 세션 수 (-1을 넣으면 무제한으로 세션 생성 허용됨)
- maxSessionsPreventsLogin()
: maximumSessions 에서 설정한 최대 허용 세션의 수가 되었을 때 추가적인 인증 요청(세션 생성)이 있을 경우 어떻게 처리 할지를 정함
true : 현재 사용자 인증 실패
false(default) : 기존 세션 만료 - expiredUrl
- 기존 세션 만료 시 이동할 페이지
- invalidSessionUrl()
- 세션이 유효하지 않을 때 이동할 페이지
메소드 | 동작 |
---|---|
authenticated() | 인증된 사용자의 접근을 허용 |
fullyAuthenticated() | 인증된 사용자의 접근을 허용, remeberMe 인증 제외 |
permitAll() | 무조건 접근을 허용 |
denyAll() | 무조건 접을을 허용하지 않음 |
anonymous() | 익명사용자의 접근을 허용 |
rememberMe() | 기억하기를 통해 인증된 사용자의 접근을 허용 |
access(String) | 주어진 SpEL 표현식의 평가 결과가 true면 접근을 허용 |
hasRole(String) | 사용자가 주어진 역할이 있다면 접근을 허용 |
hasAuthority(String) | 사용자가 주어진 권한이 있다면 접근을 허용 |
hasAnyRole(String...) | 사용자가 권한을 하나라도 가지고 있는 경우 접근을 허용 |
hasAnyAuthority(String...) | 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용 |
hasIpAddress(String) | 주어진 IP로부터 요청이 왔다면 접근을 허용 |
spring security의 인증 방식 : credential 기반 인증 (사용자명과 비밀번호를 이용한 방식)
- principal : 아이디
- credential : 비밀번호
AuthenticationProvider를 상속한 AuthProvider 클래스
1. UserDetailsService의 loadByUsername을 이용하지 않고 DB의 user 정보를 가져온다.
2. login.jsp 페이지의 로그인 버튼을 눌렀을 때 POST로 전송되는 user_id, password를 이용하여 사용자 인증
3. 입력받은 user_id로 회원정보 select (loginService.selectUser(user_id)
4. 조회된 회원정보와 (암호화된) 비밀번호가 일치하는 경우 users 권한에 따라 SYS_ADMIN, NOR_ADMIN 권한을 부여한다.
5. 인증된 user인 경우 정보를 담아 SecurityContextHolder에 저장되는 token을 생성한다.
- 필터로 구현할 수 있는 기능
서블릿이 호출되기 전에 서블릿 요청을 가로채는 기능
서블릿이 호출되기 전에 요청 내용을 점검하는 기능
요청 헤더의 수정과 조정 기능
서블릿이 호출된 후에 서블릿 응답을 가로채는 기능
응답 헤더의 수정과 조정 기능
- init
필터 객체가 생성될 때 호출되는 메소드로 초기화 기능을 구현한다.
웹 서버가 시작될 때 한 번만 생성된 다음 계속 사용되므로 init() 메소드는 서버가 시작될 때 한 번만 호출된다.FilterConfig : 필터 객체 init() 메소드의 인자값으로 전달되는 객체로서 필터에 대한 정보값을 추출하는 메소드를 가지고 있다.
- doFilter
필터링을 설정한 서블릿을 실행할 때마다 호출되는 메소드로서 실제 필터링 기능을 구현하는 메소드
- destroy
필터 객체가 삭제될 때 호출되는 메소드로 자원 해제 기능을 구현한다.
- Model 객체
- Controller에서 생성한 데이터를 담아서 View로 전달할 때 사용하는 객체 (메소드에 파라미터로 넣어주고 String 형태로 반환한다.)
- addAttribute("변수 이름", "값") 메소드를 사용하여 전달할 데이터 세팅
- ModelAndView
- ModelAndView 객체를 생성해서 객체 형태로 변환한다.
- 화면에 출력할 데이터(Model)을 설정할 수 있고, 어떤 화면(View)으로 넘어갈지 경로를 설정할 수 있다.
- Model + View 값을 넣을 때는 addObject를 사용하고, 값을 보낼 View를 세팅할 때는 setViewName()을 사용한다.
- 단방향 암호화를 위해 만들어진 해시 함수
- salt를 부여하여 여러 번 해싱하므로 같은 비밀번호를 암호화하더라도 해시 값은 매번 다른 값이 도출된다.
Hash (해싱) : 각 사용자의 일반 텍스트 암호를 가져와 단방향 수학 함수를 통해 암호화를 실행하는 것
Salt (솔트) : 해시 함수를 돌리기 전에 원문에 임의의 문자열을 덧붙이는 것
사용자 비밀번호 -> salt 생성 -> hashing -> 암호화된 비밀번호 DB에 저장
- 모듈화
- 서비스를 만들어두면 컨트롤러에서는 해당 서비스를 호출하여 사용
- 서비스를 만들지 않고 컨트롤러에서 구현한 경우 해당 기능을 필요로 하는 모든 컨트롤러가 동일한 기능을 중복으로 구현해야 한다.
- 보안
- 서비스를 통해서만 데이터베이스에 접근하도록 구현하는 것이 보안 상 안전하다.
- 해킹을 통해 컨트롤러를 제어할 수 있게 되더라도 리포지토리에 직접 접근할 수는 없게 된다.
: HTTP에서 request는 header와 body 부분으로 나누어져 있다. header의 content-type은 body에 대한 데이터를 정의한다. 서버는 content-type의 값을 보고 body를 알맞은 형태로 해석한다. multipart란 이 요청 헤더 content-type의 한 종류로서 웹 클라이언트가 요청을 보낼 때, HTTP 요청의 바디 부분을 여러 부분으로 나누어서 보내는 방식이다.
두 종류의 데이터가 하나의 HTTP Request Body에 들어가야 하는 경우 하나의 Body에서 두 종류의 데이터를 구분해서 넣어주는 방법이 multipart 타입이다.
Body에서 데이터를 구분해야 하기 때문에 요청파라미터를 url 뒤에 문자열로 추가하는 GET 방식으로는 파일을 보낼 수 없다.POST 방식에서만 사용 가능