-vm C:\Program Files\Java\... // 자신의 컴퓨터에 설치된 JDK 경로를 입력
스프링 버전 변경하기
<properties> ... <org.springframework-version>변경할 스프링 버전</org.springframework-version> ... </properties>
Java Version 변경하기
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>변경할 자바 버전</source> <target>변경할 자바 버전</target> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin>
Lombok이란?
Lombok을 이용한 자바 코드와 일반 자바 코드 차이
public class 클래스명{ private String 변수1; private int 변수2; ... public void set변수1(String str){ ... } public String get변수1(){ ... } public void set변수2(String str){ ... } public String get변수2(){ ... } ... }
@Getter // Getter 메소드를 자동으로 생성 @Setter // Setter 메소드를 자동으로 생성 @ToString // ToString 메소드를 자동으로 생성 @NoArgsConstructor // 파라미터가 없는 생성자를 생성 @AllArgsConstructor // 클래스에 존재하는 모든 필드에 대한 생성자를 자동으로 생성 // @RequiredArgsConstructor는 초기화되지 않은 모든 final 필드, @NonNull로 마크되어 있는 필드에 대한 생성자를 자동으로 생성 // @Data를 사용하면 위 5개의 어노테이션을 포함함. public class 클래스명{ private String 변수1; private int 변수2; ... }
Lombok 적용 방법
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>Lombok 버전</version> <scope>provided</scope> </dependency>
Lombok에서 자주 사용하는 어노테이션
@Getter(AccessLevel.PRIVATE)
<properties> <java-version>자바버전</java-version> <org.springframework-version>스프링 버전</org.springframework-version> ... <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>스프링 버전</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> ...
package 패키지 경로 import ... @Configuration public class 클래스명{ ... }
POJO(Plain Old Java Object) 기반의 구성
의존성 주입(DI)을 통한 객체 간의 관계 구성
AOP(Aspect-Oriented-Programming) 지원
편리한 MVC 구조
트랜잭션의 지원
WAS의 종속적이지 않은 개발 환경
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework-version}//스프링버전</version> <scope>test</scope> </dependency>
... @Component @Data public class DITest1{ }
... @Component @Data public class DITest2{ @Setter(onMethod_ = @Autowired) private DITest1 diTest; }
... <context:component-scan base-package="패키지경로"></context:component-scan>
... @Configuration // 설정 파일임을 나타냄 @ComponentScan(basePackages= {"패키지명"}) public class RootConfig{ ... }
테스트 코드 중요 부분
패키지 관련 코드... @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("file:root-context.xml 파일 경로") @Log4j2 public class testClass{ @Setter(onMethod_ = { @Autowired }) private B b; @Test public void testMethod(){ assertNotNull(b); log.info(b); ... } }
// 사용자 계정 생성 CREATE USER 계정명 IDENTIFIED BY 비밀번호 DEFAULT TABLESPACE USERS TEMPORARY TABLESPACE TEMP; // 사용자 계정에 권한 부여하기 GRANT CONNECT, DBA TO 계정명;
<dependency> <groupId>com.oracle.ojdbc</groupId> <artifactId>ojdbc8</artifactId> <version>JDBC 버전</version> </dependency>
... @Log4j2 public class JDBCTests{ static{ try{ Class.forName("JDBC 패키지 경로"); } catch(Exception e){ ... } } @Test public void testConnection(){ try(Connection conn = DriverManager.getConnection(JDBCURL, ID, PWD)){ ... } }
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>HikariCP 버전</version> </dependency>
... <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"></property> <property name="jdbcUrl" value="jdbc:oracle:thin:@localhost:1521:XE"></property> <property name="username" value="DB 계정명"></property> <property name="password" value="DB 계정 비밀번호"></property> </bean> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close"> <constructor-arg ref="hikariConfig" /> </bean> ...
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>mybatis 버전</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>mybatis-spring 버전</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>spring-tx 버전</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>spring-jdbc 버전</version> </dependency>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> </bean>
... public interface Mapper{ // java로 mapper 설정 @Select("SELECT문") // getMapper1 메소드에 SELECT문 추가 public String getMapper1(); // xml로 mapper설정 public String getMapper2(); } ...
<mybatis-spring:scan base-package="Mapper 패키지 경로" />
... <mapper namespace="mapper 패키지 경로"> // insert, update, delete, select 태그가 존재 <select id="메소드명" resultType="반환타입"> SELECT 문 </select> </mapper>
... @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("file:root-context.xml 경로" | classes = { RootConfig 패키지 경로 }) @Log4j2 public class mapperTests{ @Setter(onMethod_ = @Autowired) private Mapper mapper; @Test public void getMapperTest1(){ log.info(mapper.getClass().getName()); log.info(mapper.getMapper1()); } @Test public void getMapperTest2(){ log.info("getMapper2"); log.info(mapper.getMapper2()); } }
<dependency> <groupId>org.bgee.log4jdbc-log4j2</groupId> <artifactId>log4jdbc-log4j2-jdbc4</artifactId> <version>log4jdbc-log4j2-jdbc4 버전</version> </dependency>
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
... <bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig"> <property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property> <property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property> ...
<!-- Appender, Layout 설정 --> <Appenders> <Console name="console" target="SYSTEM_OUT"> <PatternLayout pattern=" %-5level %c(%M:%L) - %m%n" /> </Console> </Appenders> <!-- Logger 설정 --> <Loggers> <Root level="INFO"> <AppenderRef ref="console" /> </Root> <Logger name="org.zerock" level="INFO" additivity="false"> <AppenderRef ref="console" /> </Logger> <Logger name="org.springframework" level="DEBUG" additivity="false"> <AppenderRef ref="console" /> </Logger> </Loggers>
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="경로" /> <beans:property name="suffix" value=".확장자" /> </beans:bean>
... @Controller @RequestMapping("/경로/*") public class Controller{ ... }
<context:component-scan base-package="패키지 경로" />
// 기본 형태 @RequestMapping(value="/경로") // value=를 생략한 경우 @RequestMapping("/경로") // URL이 여러개인 경우 @RequestMapping({"/경로1", "/경로2"}) @RequestMapping(value = {"/경로1", "/경로2"})
// GET/POST 메소드를 설정한 경우 @RequestMapping(value="/경로" method=RequestMethod.GET | POST) public static 메소드명(... // static import로 RequestMethod 라이브러리를 추가 import static org.springframework.web.bind.annotation.RequestMethod.*; @RequestMapping(value="/경로" method=GET | POST) // Pathvariable로 파라미터 전송 @RequestMapping("/경로/{ 속성명 }") public ModelAndView 메소드명(@PathVariable int 파라미터1){ ModelAndView view = new ModelAndView(); view.setViewname("View명"); view.addObject("속성명", 파라미터1) return view; }
// 파라미터명=파라미터값 : 파라미터명이 파라미터값일 때 호출 // 파라미터명!=파라미터값 : 파라미터명이 파라미터값이 아닐 때 호출 // 파라미터명 : 파라미터에 존재할 때 호출 // !파라미터명 : 파라미터에 존재하지 않을 때 호출 @RequestMapping(value="/경로", params="파라미터명=파라미터값") // 파라미터가 여러개인 경우 @RequestMapping(value="/경로", params={ "파라미터명1=파라미터값1", "파라미터명2", ...})
@RequsetMapping(value="/경로", headers="해더")
@GetMapping("경로") public String 메소드명(@RequestParam("파라미터명1") String 파라미터명1, @RequestParam("파라미터명2") int 파라미터명2){ log.info("파라미터1 : " + 파라미터1); log.info("파라미터2 : " + 파라미터2); return "이동할경로"; }
// 리스트 @GetMapping("경로") public String 메소드명(@RequestParam("파라미터명") ArrayList<String> 파라미터명){ log.info("파라미터 : " + 파라미터); return "이동할경로"; } // 배열 @GetMapping("경로") public String 메소드명(@RequestParam("파라미터명") String[] 파라미터명){ log.info("파라미터 : " + Arrays.toString(파라미터)); return "이동할경로"; }
@GetMapping("경로") public String 메소드명(Bean클래스명 파라미터명){ log.info("파라미터 : " + 파라미터); return "이동할경로"; }
// Controller의 일부분 @InitBinder public void initBinder(WebDataBinder binder){ // 예시로 Date타입 변환 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-mm-dd"); binder.registerCustomEditor(java.util.Date.class, new CustomDateEditer(dateFormat, false)); }
@Data public class 클래스명{ private String title; @DateTimeFormat(pattern="yyyy-mm-dd") private Date date; }
// Controller의 일부 public String 메소드명(Model model){ model.addAttribute("속성명", 속성값); return "경로"; }
// Controller의 일부 // 데이터를 웹 페이지에 전달할 때 빈객체 파라미터는 전달됨 // 하지만 다른 파라미터는 해당 어노테이션을 사용하지 않으면 웹 페이지에 전달되지 않음. public String 메소드명(빈객체 파라미터1,@ModelAttribute("속성명") 파라미터2){ log.info("파라미터1 : " + 파라미터1); log.info("파라미터2 : " + 파라미터2); return "경로"; }
// Controller의 일부 public String 메소드명( ... ){ ... // addFlashAttribute를 사용해 화면에 한 번만 사용하고 다음에 // 사용되지 않는 데이터를 전달하기 위해 사용 rttr.addFlashAttribute("데이터명1", 데이터명1) rttr.addFlashAttribute("데이터명3", 데이터명2) ... return "redirect:/"; }
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="/WEB-INF/views/" /> <beans:property name="suffix" value=".jsp" /> </beans:bean>
// pom.xml <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>commons-fileupload 버전</version> </dependency>
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <bean:property name="defaultEncoding" value="utf-8"></beans:property> <bean:property name="maxUploadSize" value="request로 전달될 수 있는 최대 크기"></beans:property> <bean:property name="maxUploadSizePerFile" value="하나의 파일 최대 크기"></beans:property> <bean:property name="uploadTimpDir" value="임시파일 저장 경로"></beans:property> <bean:property name="maxInMemorySize" value="메모리상에서 유지하는 최대크기"></beans:property> </beans:bean>
@ControllerAdvice @log4j2 public class CommonExceptionAdvice { @ExceptionHandler(예외처리 클래스) public String except(Exception ex, Model model){ log.error("Exception........" + ex.getMessage()); model.addAttribute("exception", ex); log.error(model); return "error_page"; } }
<filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-class>UTF-8</param-class> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <servlet-name>appServlet</servlet-name> </filter-mapping>
order by의 문제
실행 계획
인덱스를 이용한 데이터 정렬
힌트를 이용한 데이터 정렬
select /*+ FULL(테이블명) */ * from 테이블 order by 기준칼럼 desc;
select /*+ INDEX_ASC(테이블명 인덱스명) */ * from 테이블 where 조건;
ROWNUM
<if test="조건"> SQL문 </if>
<choose> <when test="조건"> SQL문 </when> <when test="조건"> SQL문 </when> ... <otherwise> SQL문 </otherwise> </choose>
select * from 테이블명 <where> where문 </where>
select * from 테이블명 <foreach item="변수명" index="키" collection="콜랙션 타입 종류"> where문 </foreach>
@RestController @RequestMapping("/경로") @Log4j2 public class Controller{ @GetMapping(value="/세부경로", produces="text/plain; charset=UTF-8") public String Method(){ log.info("MIME TYPE : " + MediaType.TEXT_PLAIN_VALUE); return "데이터"; } }
@GetMapping(value="/경로", produces={MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE}) public 빈객체 method(){ return new 빈객체(...); }
@GetMapping(value="/경로") public List<SampleVO> method(){ // 내부적으로 1~10까지 루프를 처리, 빈객체를 만들어 리스트로 만듬 return IntSteam.range(1,10).mapToObj(i -> new 빈객체(...)).collect(Collectors.toList()); // MAP 타입을 리턴 Map<String, 빈객체> map = new HashMap<>(); map.put("First", new 빈객체(...)); return map; }
@GetMapping(value="/경로" params={"height", "weight"}) public ResponseEntity<sampleVO> check(Double height, Double weight){ 빈객체 vo = new 빈객체(...); ResponseEntity<빈객체>result = null if(height < 150){ result = ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(vo); } else{ result = ResponseEntity.status(HTTPStatus.OK).body(vo); } }
@GetMapping("/경로/{세부경로1}/{세부경로2}") public String[] method(@PathVariable("세부경로1") String 세부경로1, @PathVariable("세부경로2") Integer 세부경로2){ return new String[] {"속성1:"+세부경로1, "속성2:"+세부경로2}; }
@PostMapping("/ticket") public 빈객체 method(@RequestBody 빈객체 파라미터명){ log.info("convert...........ticket" + 파라미터명); return 파라미터명; }
@within(어노테이션 타입)
@target(어노테이션 타입)
@annotation(어노테이션 타입)
@AOP어노테이션("execution([접근제어자] 반환타입 패키지경로.클래스.메소드(인자))")
@AOP어노테이션("whitin(패키지경로.클래스)")
<servlet> ... <multipart-config> <location>파일 저장 경로</location> <max-file-size>최대 파일 크기</max-file-size> <max-request-size>최대 요청 크기</max-request-size> <file-size-threshold>파일이 메모리에 기록되는 임계값</file-size-threshold> </multipart-config> </servlet>
... <beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"> </beans:bean>
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$"); var maxSize = 5242880; function checkExtension(fileName, fileSize){ if(fileSize >= maxSize){ alert("파일 사이즈 초과"); return false; } ... if(regex.test(fileName)){ alert("해당 종류의 파일은 업로드할 수 없습니다."); return false; } return true; } ... 이후 해당 함수를 이용해 체크
@PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile) { ... // 파일 이름 설정 String uploadFileName = multipartFile.getOriginalFilename(); uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\")+1); log.info("only file name : " + uploadFileName); UUID uuid = UUID.randomUUID(); uploadFileName = uuid.toString() + "_" + uploadFileName; File saveFile = new File(uploadPath, uploadFileName); ... }
private String getFolder() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Date date = new Date(); String str = sdf.format(date); return str.replace("-", File.separator); } @PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile) { ... // '년/월/일' 폴더 생성 File uploadPath = new File(uploadFolder,getFolder()); log.info("upload path : " + uploadPath); if(uploadPath.exists() == false) { uploadPath.mkdirs(); } ... }
<dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.8</version> </dependency>
// private boolean checkImageType(File file) { try { String contentType = Files.probeContentType(file.toPath()); return contentType.startsWith("image"); } catch (IOException e) { e.printStackTrace(); } return false; } @PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile) { ... try { // 파일 저장 File saveFile = new File(uploadPath, uploadFileName); multipartFile.transferTo(saveFile); if(checkImageType(saveFile)) { FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName)); Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100); thumbnail.close(); } } catch (Exception e) { e.printStackTrace(); } }
<!-- jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.5</version> </dependency> <!-- jackson-dataformat-xml --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.5</version> </dependency>
// input태그 복사 var cloneObj = $(".uploadDiv").clone(); $("#uploadBtn").on("click", function(e){ ... $.ajax({ url:'/controller/uploadAjaxAction', processData: false, contentType: false, data: formData, type:'post', dataType:'json', success: function(result){ console.log(result); // input태그 위치에 다시 추가 $(".uploadDiv").html(cloneObj.html()); } }); });
화면에 선택한 파일 표시
... <style type="text/css"> .uploadResult{ width : 100%; background-color : gray; } .uploadResult ul{ display: flex; flex-flow: row; justify-content: center; align-items: center; } .uploadResult ul li { list-style: none; padding: 10px; } .uploadResult ul li img{ width: 20px; } </style> ... <script type="text/javascript"> ... var uploadResult = $(".uploadResult ul"); function showUploadedFile(uploadResultArr){ var str = ""; $(uploadResultArr).each(function(i, obj){ if(!obj.image){ // 이미지일 때 str += "<li><img src='/resources/img/attach.png'" + obj.fileName + "</li>"; }else{ // 일반 파일 일때 str += "<li>" + obj.fileName + "</li>"; } }); uploadResult.append(str); } ... </script>
컨트롤러에서 섬네일 데이터 전송
// Controller // 문자열로 파일의 경로가 포함된 fileName을 파라미터로 받고 byte 배열을 전송 @GetMapping("/display") @ResponseBody public ResponseEntity<byte[]> getFile(String fileName){ log.info("fileName : " + fileName); File file = new File("c:\\upload\\"+fileName); log.info("file : " + file); ResponseEntity<byte[]> result = null; try { HttpHeaders header = new HttpHeaders(); // byte 배열로 이미지 파일을 전송할 때 브라우저에 보내주는 MIME 타입이 파일의 종류에 따라 달라짐. // probeContentType 메소드를 이용해 적절한 MIME타입 데이터를 Http 헤더 메시지에 포함할 수 있도록 처리 header.add("Content-Type", Files.probeContentType(file.toPath())); result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK); } catch (IOException e) { e.printStackTrace(); } return result; }
function showUploadedFile(uploadResultArr){ var str = ""; $(uploadResultArr).each(function(i, obj){ if(!obj.image){ // 이미지일 때 str += "<li><img src='/resources/img/attach.png'" + obj.fileName + "</li>"; }else{ // 일반 파일 일때 // str += "<li>" + obj.fileName + "</li>"; // 파일 경로 인코딩 var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid + "_" + obj.fileName); str += "<li><img src='/controller/display?fileName="+fileCallPath+"'></li>"; } }); uploadResult.append(str); }
첨부파일 다운로드
컨트롤러
// 파일을 다운 받기 위해 MIME 타입을 application/octet-stream으로 설정 // 사용자가 사용중인 브라우저를 @RequestHeader를 통해 HTTP 헤더 메시지의 User-Agent 데이터를 가져옴 @GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @ResponseBody public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName){ // 업로드한 파일 경로 가져오기 Resource resource = new FileSystemResource("C:\\upload\\" + fileName); // 업로드한 파일이 있는지 없는지 체크 if(resource.exists() == false) { return new ResponseEntity<>(HttpStatus.NOT_FOUND);// 없으면 not_found 상태 리턴 } // 파일이름 가져오기 String resourceName = resource.getFilename(); // UUID 제거 String resourceOriginalName = resourceName.substring(resourceName.indexOf("_")+1); HttpHeaders headers = new HttpHeaders(); try { String downloadName = null; // 브라우저별 인코딩 처리 if(userAgent.contains("Trident")) { log.info("IE browser"); downloadName = URLEncoder.encode(resourceOriginalName,"UTF-8").replaceAll("\\", " "); } else if(userAgent.contains("Edge browser")) { log.info("Edge browser"); downloadName = URLEncoder.encode(resourceOriginalName,"UTF-8"); } else { log.info("Chrome browser"); downloadName = new String(resourceOriginalName.getBytes("UTF-8"),"ISO-8859-1"); } log.info("downloadName" + downloadName); // 헤더 설정 // Content-Disposition을 통해 파일 이름 문자열 처리할 때 한글 깨지는 문제 막기 위해 사용 // filename에 다운로드할 파일 설정 headers.add("Content-Disposition", "attachment; filename=" + downloadName); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK); }
View 처리
<script type="text/javascript"> function showUploadedFile(uploadResultArr){ var str = ""; $(uploadResultArr).each(function(i, obj){ if(!obj.image){ var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName); // a태그를 이용해 download 컨트롤러로 접근하여 // 클릭했을 때 다운로드 진행 str += "<li><div><a href='/controller/download?fileName="+fileCallPath+"'>" + "<img src='/controller/resources/img/attach.png'>"+obj.fileName+"</a>"+ "<span data-file=\'"+fileCallPath+"\' data-type='file'>x</span></div></li>"; }else{ ... } }); uploadResult.append(str); } </script>
원본 이미지 보여주기
// 원본 파일 보여주기 // $(document).ready() 바깥에 작성한 이유는 a태그에서 직접 호출할 수 있는 방식으로 작성하기 위해 function showImage(fileCallPath){ $(".bigPictureWrapper").css("display","flex").show(); $(".bigPicture") .html("<img src='/controller/display?fileName="+encodeURI(fileCallPath)+"'>") .animate({width:'100%', height:'100%'},1000); } $(document).ready(function(){ function showUploadedFile(uploadResultArr){ ... $(uploadResultArr).each(function(i, obj){ if(!obj.image){ // 기존에 "/s_"로 파일 경로를 잡아준 부분을 "/"로 변경 var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid + "_" + obj.fileName); ... }else{ ... } }); ... } ... // 원본 이미지 숨기기 $(".bigPictureWrapper").on("click",function(e){ $(".bigPicture").animate({width:'0%',height:'0%'},1000); setTimeout(()=>{ $(".bigPictureWrapper").hide(); },1000) }); });
첨부파일 삭제
@PostMapping("/deleteFile") @ResponseBody public ResponseEntity<String> deleteFile(String fileName, String type){ File file; try { // 파일 경로 설정 file = new File("C:\\upload\\" + URLDecoder.decode(fileName,"UTF-8")); file.delete(); // 이미지일 때 if(type.equals("image")) { String largeFileName = file.getAbsolutePath().replace("s_",""); log.info("largeFileName" + largeFileName); file = new File(largeFileName); file.delete(); } } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return new ResponseEntity<>("deleted", HttpStatus.OK); }
// 첨부파일 삭제 $(".uploadResult").on("click","span", function(e){ var targetFile = $(this).data("file"); var type = $(this).data("type"); console.log(targetFile); $.ajax({ url:'/controller/deleteFile', data:{fileName:targetFile, type:type}, dataType:'text', type:'POST', success:function(result){ alert(result); } }); });
public interface Filter{ // 필터 객체 초기화 및 서비스 추가하기 위한 메소드 // 웹 컨테이너가 처음에 init 메소드를 호출, 이후부터는 doFilter를 통해 처리 public default void init(FilterConfig filterConfig) throws ServletException; // url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기전 웹 컨테이너에 의해 실행되는 메소드 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; // 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드 public default void destroy(); }
public interface HandlerInterceptor{ // 컨트롤러가 호출되기 전에 실행 // 컨트롤러 이전에 처리해야하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우 사용 // handler 파리미터는 핸들링 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 객체, // @RequestMapping이 붙은 메소드의 정보를 추상화한 객체 default boolean preHandle(ServletRequest request, ServletResponse response, Object handler) throws Exception; // 컨트롤러를 호출된 후에 실행 // 컨트롤러 이후 처리해야하는 후처리 작업이 있을 때 사용 // 하위 컨트롤러에서 작업을 진행하다 예외가 발생하면 실행안됨 default void postHandle(ServletRequest request, ServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception; // 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행 // 요청 처리 중에 사용한 리소스를 반환할 때 사용하기 적합 // 하위 컨트롤러에서 작업을 진행하다 예외가 발생하더라고 실행됨 default void afterCompletion((ServletRequest request, ServletResponse response, Object handler, @Nullable Exception ex) throws Exception; }
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>Spring Security 버전</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>Spring Security 버전</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>Spring Security 버전</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>Spring Security 버전</version> </dependency>
... <security:http> <security:form-login/> </security:http> <!-- 스프링 시큐리티가 동작하기 위해서 Authentication Manager가 필요 <security:authentication-manager> 부분이 스프링 시큐리티가 스프링 MVC에서 작동하는 시작 지점 --> <security:authentication-manager> </security:authentication-manager> ...
... <!-- 스프링 시큐리티가 스프링 MVC에서 사용되기 위해 필터를 이용해 스프링 동작에 관여하도록 설정 --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 위 필터만 작성하면 NoSuchBeanDefinitionException이 발생하는데 이 예외는 springSecurityFilterChain라는 빈이 설정되지 않아서 발생하는 문제 -> 스프링 시큐리티 설정파일을 찾을 수 없어서 발생한 문제로 생각하면됨. context-param을 통해 security-context.xml 파일 로딩과 해당 파일에 최소한의 설정이 필요 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/root-context.xml /WEB-INF/spring/security-context.xml </param-value> </context-param> ...
security-context.xml
<!-- 직접 작성한 Handler 클래스를 이용하기 위해 Bean에 저장 --> <bean id="customAccessDenied" class="org.zerock.security.CustomAccessDeniedHandler"></bean> <bean id="customLoginSuccess" class="org.zerock.security.CustomLoginSuccessHandler"></bean> <security:http> <!-- security:intercept-url - pattern : URI 패턴 - access : 권한 체크 --> <security:intercept-url pattern="/sample/all" access="permitAll"/> <security:intercept-url pattern="/sample/member" access="hasRole('ROLE_MEMBER')"/> <security:intercept-url pattern="/sample/admin" access="hasRole('ROLE_ADMIN')"/> <!-- 에러 페이지 --> <security:access-denied-handler ref="customAccessDenied"/> <!-- 로그인 페이지 - 로그인을 성공했을 때 authentication-success-handler-ref에 설정된 핸들러로 진입 --> <security:form-login login-page="/customLogin" authentication-success-handler-ref="customLoginSuccess"/> <!-- 로그아웃 페이지 - 로그아웃 시 logout-url에 설정된 페이지로 요청 - invalidate-session을 통해 세션정보를 제거 --> <security:logout logout-url="/customLogout" invalidate-session="true"/> </security:http> <security:authentication-manager> <security:authentication-provider> <security:user-service> <!-- 계정 설정 및 권한 부여 --> <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/> <security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/> </security:user-service> </security:authentication-provider> </security:authentication-manager>
컨트롤러
// 에러 페이지 @GetMapping("/accessError") public void accessDenied(Authentication auth, Model model) { log.info("access Denied : " + auth); model.addAttribute("msg", "Access Denied"); } // 로그인 페이지 @GetMapping("/customLogin") public void loginInput(String error, String logout, Model model) { log.info("error : " + error); log.info("logout : " + logout); if(error != null) { model.addAttribute("error", "Login Error Check Your Account"); } if(logout != null) { model.addAttribute("logout", "Logout!!"); } } // 로그아웃 페이지 @GetMapping("/customLogout") public void logoutGet() { log.info("custom logout"); }
핸들러
public class CustomAccessDeniedHandler implements AccessDeniedHandler{ // 에러 페이지 핸들러 // 로그인 실패시 에러 페이지로 리다이렉트 @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { log.error("Access Denied Handler"); log.error("Redirect............."); response.sendRedirect("/controller/accessError"); } }
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler{ // 로그인 성공 페이지 // security-context.xml에서 계정 설정 때 부여한 권한을 가져와 roleNames 리스트에 추가 // if문을 통해 ROLE NAME에 따른 처리 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException { log.warn("Login Success"); List<String> roleNames = new ArrayList<>(); auth.getAuthorities().forEach(authority -> { roleNames.add(authority.getAuthority()); }); log.warn("ROLE NAMES : " + roleNames); if(roleNames.contains("ROLE_ADMIN")) { response.sendRedirect("/controller/sample/admin"); return; } if(roleNames.contains("ROLE_MEMBER")) { response.sendRedirect("/controller/sample/member"); return; } response.sendRedirect("/"); } }
CSRF(Cross-site request forgery) 공격과 토큰
CSRF 공격 방지 방법
스프링 시큐리티에서 CSRF 설정
<security:csrf disabled="false"/>
... <!-- 커스텀 패스워드 인코더 설정 --> <bean id="customPasswordEncoder" class="org.zerock.security.CustomNoOpPasswordEncoder"></bean> ... <security:authentication-manager> <security:authentication-provider> <!-- 계정 설정 및 권한 부여 부분 이전 코드 <security:user-service> <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/> <security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/> </security:user-service> JDBC 연결 data-source-ref : JDBC 연결 정보 --> <security:jdbc-user-service data-source-ref="dataSource" /> <security:password-encoder ref="customPasswordEncoder"/> </security:authentication-provider> </security:authentication-manager>
// 패스워드 인코딩 관련 // 패스워드는 무조건 단방향으로 암호화 // PasswordEncoder 구현 클래스에 다양한 암호화 메소드를 지원 // - BcryptPasswordEncoder // - Argon2PasswordEncoder // - Pbkdf2PasswordEncoder // - ScryptPasswordEncoder // PasswordEncoder 구현 클래스에서 지원하는 메소드를 사용하면 해당 패스워드 인코더 핸들러를 안만들어도됨. @Log4j2 public class CustomNoOpPasswordEncoder implements PasswordEncoder{ // 암호화할 때 사용 @Override public String encode(CharSequence rawPassword) { log.warn("before encode : " + rawPassword); return rawPassword.toString(); } // 사용자에게서 입력받은 패스워드를 비교 @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { log.warn("matches : " + rawPassword + " : " + encodedPassword); return rawPassword.toString().equals(encodedPassword); } }
... <!-- 스프링 시큐리티에서 지원하는 BcryptPasswordEncoder 사용 --> <bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean> ... <security:authentication-manager> <security:authentication-provider> <!-- 계정 설정 및 권한 부여 부분 이전 코드 <security:user-service> <security:user name="member" password="{noop}member" authorities="ROLE_MEMBER"/> <security:user name="admin" password="{noop}admin" authorities="ROLE_MEMBER, ROLE_ADMIN"/> </security:user-service> JDBC 연결 data-source-ref : JDBC 연결 정보 users-by-username-query속성과 authorities-by-username-query속성을 이용하면 기존 테이블을 이용해 사용자 인증 및 권한 확인할 수 있음. --> <security:jdbc-user-service data-source-ref="dataSource" users-by-username-query="select userid, userpw, enabled from tb1_member where userid = ? " authorities-by-username-query="select userid,auth from tb1_member_auth where userid = ? " /> <security:password-encoder ref="bcryptPasswordEncoder"/> </security:authentication-provider> </security:authentication-manager>
// 여러 개의 사용자 권한을 가질 수 있게 설계 // 사용자 정보 VO @Data public class MemberVO { private String userid; private String userpw; private String userName; private String enabled; private Date regDate; private Date updateDate; private List<AuthVO> authList; }
@Data public class AuthVO { private String userid; private String auth; }
public interface MemberMapper { public MemberVO read(String userid); }
<mapper namespace="org.zerock.mapper.MemberMapper"> <resultMap type="org.zerock.domain.MemberVO" id="memberMap"> <id property="userid" column="userid"/> <result property="userid" column="userid"/> <result property="userpw" column="userpw"/> <result property="userName" column="username"/> <result property="regDate" column="regdate"/> <result property="updateDate" column="updatedate"/> <collection property="authList" resultMap="authMap"></collection> </resultMap> <resultMap type="org.zerock.domain.AuthVO" id="authMap"> <result property="userid" column="userid"/> <result property="auth" column="auth"/> </resultMap> <select id="read" resultMap="memberMap"> SELECT mem.userid, userpw, username, enabled, regdate, updatedate, auth FROM tb1_member mem LEFT OUTER JOIN tb1_member_auth auth on mem.userid = auth.userid WHERE mem.userid = #{userid} </select> </mapper>
@Log4j2 public class CustomUserDetailsService implements UserDetailsService{ @Setter(onMethod_ = @Autowired) private MemberMapper memberMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.warn("Load User By UserName : " + username); // username을 통해 해당 유저 정보 가져오기 MemberVO vo = memberMapper.read(username); log.warn("queried by member mapper : " + vo); return vo == null ? null : new CustomUser(vo); } }
<security:authentication-manager> <!-- userDetailsService 클래스를 빈으로 생성하여 user-service-ref에 할당 --> <security:authentication-provider user-service-ref="customUserDetailsService"> <security:password-encoder ref="bcryptPasswordEncoder"/> </security:authentication-provider> </security:authentication-manager>
// 스프링 시큐리티의 User 클래스를 상속 // @Getter public class CustomUser extends User{ private static final long serialVersionUID = 1L; private MemberVO member; public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } public CustomUser(MemberVO vo) { // AuthVO 인스턴스를 GrantedAuthority 객체로 변환 // stream과 map을 이용해서 처리 super(vo.getUserid(), vo.getUserpw(), vo.getAuthList().stream().map(auth -> new SimpleGrantedAuthority(auth.getAuth())).collect(Collectors.toList())); } }
... <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %> ... <body> <h1>/sample/admin page</h1> <!-- <sec:authentication property="principal"/> : UserDetailsService에서 반환된 객체 CustomUserDetailsService를 이용했으면 CustomUser를 반환 --> <p>principal : <sec:authentication property="principal"/></p> <p>MemberVO : <sec:authentication property="principal.member"/></p> <p>사용자이름 : <sec:authentication property="principal.member.userName"/></p> <p>사용자아이디 : <sec:authentication property="principal.username"/></p> <p>사용자 권한 리스트 : <sec:authentication property="principal.member.authList"/></p> <a href="/controller/customLogout">Logout</a> </body> </html>
// 익명 사용자인경우 <sec:authorize access="isAnonymous()"> <a href="/controller/customLogin">로그인</a> </sec:authorize> // 인증된 사용자인 경우 <sec:authorize access="isAuthenticated()"> <a href="/controller/customLogout">로그아웃</a> </sec:authorize>
<security:http> ... <!-- 로그아웃 페이지 - 로그아웃 시 logout-url에 설정된 페이지로 요청 - invalidate-session을 통해 세션정보를 제거 - delete-cookies를 통해 자동로그인 쿠키 삭제 --> <security:logout logout-url="/customLogout" invalidate-session="true" delete-cookies="remember-me, JSESSION_ID"/> <!-- 자동 로그인 - key : 쿠키에 사용되는 값을 암호화하기 위한 키 값 - data-source-ref : DataSource를 지정하고 테이블을 이용해서 기존 로그인 정보를 기록(옵션) - remember-me-cookie : 브라우저에 보관되는 쿠키의 이름을 지정, 기본값은 remember-me - remember-me-parameter : 웹 화면에서 로그인할 때 'remember-me'는 대부분 체크박스를 이용해 처리, 체크박스 태그는 name 속성을 의미 - token-validity-seconds : 쿠키 유효시간 지정 --> <security:remember-me data-source-ref="dataSource" token-validity-seconds="604800"/> </security:http>
... // pre-post-annotations와 secured-annotations 등이 enabled로 설정되어 있어야 // 해당 어노테이션을 사용할 수 있음 <security:global-method-security pre-post-annotations="enabled" secured-annotations="enabled"></security:global-method-security> ...
참고한 책
참고 사이트