728x90
300x250

[JSP] 19. MyBatis-3.5.5 와 Maven / Servlet 연동하기 (Oracle 19g) - Java 방식


MyBatis 관련 이전의 글을 읽어보고 실습을 해본 다음에 경험해봐도 무방할 것으로 보인다.

이번에 소개할 내용은 조금 세련된 방식으로 MyBatis를 적용하는 방법에 대해서 소개하려고 한다.


이 글을 작성하게 된 계기는 MyBatis 공식 사이트에서 제시하는 방법론에 대해서 실질적으로 어떻게 구현하는지 소개해보고 싶어서 그렇다.


이전 스타일에 익숙하다면, 새로운 스타일을 경험하시어 훨씬 생산성이 있는 방법으로 개발을 시도해보면 좋지 않겠냐는 것이다.


1. [JSP] 8. 영속프레임워크 MyBatis를 활용한 CRUD 구현 - JSP와 Oracle (XML 방식), 2020-9-19
https://yyman.tistory.com/1390 

-> XML 방식으로 구현됨.


- IDE: Spring-Tool-Suite 4-4.7.2 Releases (2020-06 최신)

- DB: Oracle Databases 19g (2020-09 최신)

- Maven 3.6.3/1.16.0.20200610-1735 (2020-09 최신)

- JAR: javax.servlet-api.4.0.1.jar (2020-09 최신)

         ojdbc8-19.7.0.0.jar (2020-09 최신)

         MyBatis 3.5.5 (2020-09 최신)



1. 결과


결과를 바탕으로 살펴보면, 이렇게 출력되면 잘 된 것이다.



그림 1. 결과 - DB 연동 모습



그림 2. 결과 - 프로젝트 구성도


작업을 할 내용이다. 매우 간결한 프로젝트 구성이다.



2. 데이터베이스 설계


Spring Security 5의 사용자 계정 테이블을 인용하여 설계하였다.

테이블의 구조는 간단한 형태이다.



그림 3. Oracle SQL Developer - 데이터베이스 설계





그림 4. Oracle SQL Developer - (데이터 내용)



그림 5. Oracle SQL Developer - COMP_USERS (Model)


CREATE TABLE comp_users (

username VARCHAR(50) NOT NULL,

password VARCHAR(300) NOT NULL,

enabled INT NOT NULL,

PRIMARY KEY (username)

);


-- 계정

INSERT INTO comp_users (username, password, enabled) VALUES ('user', '$2a$10$x04djNV2e9rpcPPRyXoLk.rMm6iZe2/vYdzpqHQcLeNSYdt7kc30O', 1);

INSERT INTO comp_users (username, password, enabled) VALUES ('admin', '$2a$10$QUddY3O/6ZgkYCR6MFlv9.nqA501Fm0cc/ZxQHX5pwb1o0CYCTiIS', 1);


파일명: comp_users.sql


[첨부(Attachments)]

comp_users.zip





3. 프로젝트 생성하기


프로젝트 생성에 대해서 소개하겠다.



그림 6. Maven Project 생성하기(1)


File -> New -> Maven Project를 클릭한다.




그림 7. Maven Project 생성하기(2)


프로젝트를 생성할 때 "org.apache.maven.archetype", "maven-archetype-webapp"을 선택한다.

Next를 클릭한다.



그림 8. Maven Project 생성하기(3)


Group Id, Artifact Id를 입력해준다.

Finish를 누른다.




4. 자바 버전 - Build Path, Project Facets 설정하기


프로젝트를 선택한다. 그리고 Properties에 들어가서 설정할 것이다.



그림 9. 프로젝트의 마우스 오른쪽 버튼의 메뉴 모습


프로젝트를 선택한 후 마우스 오른쪽 버튼을 누른다.

Properties를 클릭한다.



그림 10. 프로젝트의 Build Path


Java Build Path를 클릭한다.

JRE System Library를 14버전으로 변경해준다.

Apply를 누른다.




그림 11. 프로젝트의 Project Factes


Project Factes를 클릭한다.

Java의 버전을 14로 변경한다.

Apply를 누른다.



4. MyBatis - 공식 사이트 메뉴얼 읽어보기


공식 사이트에서 지원하는 방식에 대해서 간단하게 소개되어 있다.

아래의 공식 링크를 참고해서 적용할 것이다.


https://mybatis.org/mybatis-3/ko/getting-started.html



그림 12. MyBatis - 시작하기 (Official Site)





5. pom.xml - 설정하기


http://mvnrepository.com에서 Java Servlet API 4.0.1과 MyBatis를 찾아서 추가한다.

Oracle은 Oracle 공식 사이트나 또는 Oracle Databases 19g가 설치되어 있다면, 간단하게 Add Dependency를 통해서 pom.xml에 추가할 수 있다.



그림 13. Servlet API 4.0.1 - Mvnrepository






그림 14. MyBatis 3.5.5 - Mvnrepository



그림 15. Pom.xml 마우스 오른쪽 버튼 메뉴 모습


pom.xml을 마우스 오른쪽 버튼으로 클릭한다.

Maven->Add Dependency를 클릭한다.


(참고로 Oracle Databases 19g가 설치된 경우에서만 가능한 작업이다.)

(설치가 되지 않은 경우라면, Oracle 공식 사이트에서 Oracle JDBC를 내려받기 바란다.

그리고 lib 폴더에 넣어주고 프로젝트에서 셋팅해줘야 한다.)



그림 16. Add-Dependency


Oracle을 검색한다.

com.oracle.database.jdbc  "ojdbc8"을 선택한다.

OK를 누른다.



그림 17. STS 4.4 - pom.xml 모습


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>


  <groupId>com.examplebatis</groupId>

  <artifactId>web</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>war</packaging>


  <name>web Maven Webapp</name>

  <!-- FIXME change it to the project's website -->

  <url>http://www.example.com</url>


  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.7</maven.compiler.source>

    <maven.compiler.target>1.7</maven.compiler.target>

  </properties>


  <dependencies>

    <dependency>

      <groupId>junit</groupId>

      <artifactId>junit</artifactId>

      <version>4.11</version>

      <scope>test</scope>

    </dependency>

    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->

<dependency>

    <groupId>org.mybatis</groupId>

    <artifactId>mybatis</artifactId>

    <version>3.5.5</version>

</dependency>


<dependency>

<groupId>com.oracle.database.jdbc</groupId>

<artifactId>ojdbc8</artifactId>

<version>19.7.0.0</version>

</dependency>

  </dependencies>


  <build>

    <finalName>web</finalName>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->

      <plugins>

        <plugin>

          <artifactId>maven-clean-plugin</artifactId>

          <version>3.1.0</version>

        </plugin>

        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->

        <plugin>

          <artifactId>maven-resources-plugin</artifactId>

          <version>3.0.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-compiler-plugin</artifactId>

          <version>3.8.0</version>

        </plugin>

        <plugin>

          <artifactId>maven-surefire-plugin</artifactId>

          <version>2.22.1</version>

        </plugin>

        <plugin>

          <artifactId>maven-war-plugin</artifactId>

          <version>3.2.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-install-plugin</artifactId>

          <version>2.5.2</version>

        </plugin>

        <plugin>

          <artifactId>maven-deploy-plugin</artifactId>

          <version>2.8.2</version>

        </plugin>

      </plugins>

    </pluginManagement>

  </build>

</project>



파일명: pom.xml


[첨부(Attachments)]

pom.zip




6. SqlMapSessionFactory.java - 자바 방식 연결부


자바 방식 연결부를 작성하도록 하겠다.



그림 18. Java Resources의 마우스 오른쪽 클릭 메뉴 모습


Java Resources를 마우스 오른쪽 버튼으로 클릭한다.

New->Class를 클릭한다.




그림 19. SqlMapSessionFactory.java 만들기


Package명과 Name을 입력한다. 

(예: package: com.exmple.web.db)    //    탈자(exmple로 만들었으니 알아서 참고할 것.)

(예: Name: SqlMapSessionFactory)


Finish를 누른다.



그림 20. SqlMapSessionFactory.java


코드를 수정해준다.


package com.exmple.web.db;


import java.io.IOException;

import java.io.InputStream;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;


import javax.sql.DataSource;


import org.apache.ibatis.io.Resources;

import org.apache.ibatis.mapping.Environment;

import org.apache.ibatis.session.Configuration;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.apache.ibatis.transaction.TransactionFactory;

import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;


import com.exmple.web.mapper.CompUsersMapper;


import oracle.jdbc.pool.OracleDataSource;


public class SqlMapSessionFactory {


private static SqlMapSessionFactory factory = new SqlMapSessionFactory();


private SqlMapSessionFactory() {}


private final static String driverName = "oracle.jdbc.driver.OracleDriver";

private final static String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";

private final static String userName = "사용자계정명";

private final static String userPassword = "비밀번호";


public static SqlMapSessionFactory getInstance() {

return factory;

}


public static SqlSessionFactory ssf;


    static{


    DataSource dataSource = getOracleDataSource();

    TransactionFactory transactionFactory = new JdbcTransactionFactory();

   

    Environment environment = new Environment("development", transactionFactory, dataSource);

    Configuration configuration = new Configuration(environment);

   

    configuration.addMapper(CompUsersMapper.class); // Mapper 클래스

   

        ssf = new SqlSessionFactoryBuilder().build(configuration);

        

    }

    

// iBatis(MyBatis 반환)

public static SqlSessionFactory getSqlSessionFactory(){

        return ssf;

    }


/*


*     public static DataSource getMySQLDataSource() {

        Properties props = new Properties();


        FileInputStream fis = null;

        MysqlDataSource mysqlDS = null;

        

        try {

            fis = new FileInputStream("db.properties");


            props.load(fis);

            mysqlDS = new MysqlDataSource();

            mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));

            mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));

            mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));

            

        } catch (IOException e) {

            e.printStackTrace();

        }

        return mysqlDS;

        

    }

    */

/*

* Description: 순정 오라클 데이터소스

*/

    private static DataSource getOracleDataSource(){

        

    OracleDataSource oracleDS = null;

        

    try {

            oracleDS = new OracleDataSource();

            oracleDS.setURL(dbUrl);

            oracleDS.setUser(userName);

            oracleDS.setPassword(userPassword);

            

        } catch (SQLException e) {

            e.printStackTrace();

        }

        return oracleDS;


    }


public Connection connect() {


Connection conn = null;


try {

Class.forName(driverName);

conn = DriverManager.getConnection(dbUrl, userName, userPassword);

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

return conn;

}


public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

close(conn, ps); // Recursive 구조 응용(재귀 함수)

} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {

try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {


try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


} // end of if


}


}


파일명: SqlMapSessionFactory.java


[첨부(Attachments)]

SqlMapSessionFactory.zip


비고: MySQL의 지원에 대해서 코드를 적어놓았다.




7. Servlet - 생성하기(Controller)


서블릿을 생성해줄 것이다.


그림 21. Java Resources의 오른쪽 버튼 메뉴 모습


Java Resources를 마우스 오른쪽 버튼으로 클릭한다.

New->Servlet을 클릭한다.



그림 22. Servlet 만들기


Package명은 "com.exmple.web.controller"로 한다.

Class Name은 "FrontController"로 한다.


Finish를 누른다.


(Exmple 오타 -> Example이 맞지만, 이미 생성했으니 그냥 따르는 걸로 하겠음.)



8. web.xml - 수정 작업


Servlet 경로를 살짝 수정해주겠다.



그림 23. web.xml - 수정하기



<!DOCTYPE web-app PUBLIC

 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

 "http://java.sun.com/dtd/web-app_2_3.dtd" >


<web-app>

  <display-name>Archetype Created Web Application</display-name>

  <servlet>

  <servlet-name>FrontController</servlet-name>

  <display-name>FrontController</display-name>

  <description></description>

  <servlet-class>com.exmple.web.controller.FrontController</servlet-class>

  </servlet>

  <servlet-mapping>

  <servlet-name>FrontController</servlet-name>

  <url-pattern>/sample</url-pattern>

  </servlet-mapping>

</web-app>



파일명: web.xml


[첨부(Attachments)]

web.zip





9. CompUsers.java - Model(모델)


앞서 설계한 DB Model을 코드로 구현할 것이다.



그림 24. CompUsers.java


package com.exmple.web.model;


public class CompUsers {


private String username;

private String password;

private int enabled;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public int getEnabled() {

return enabled;

}

public void setEnabled(int enabled) {

this.enabled = enabled;

}

}



파일명: CompUsers.java


[첨부(Attachments)]

CompUsers.zip




10. CompUsersMapper.java - Mapper(DAO)


이전 글과는 달리 공식 메뉴얼에서 제시하는 방법을 적용하도록 하겠다.



그림 25. Mapper 클래스 가이드



그림 26. Mapper 클래스 가이드 - 현실의 문제에 맞게 적용하기


package com.exmple.web.mapper;


import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;


import com.exmple.web.model.CompUsers;


@Mapper

public interface CompUsersMapper {

  @Select("SELECT * FROM comp_users WHERE username = #{username}")

  public CompUsers findByUsername(String username);

  

}



파일명: CompUsersMapper.java


[첨부(Attachments)]

CompUsersMapper.zip


대응하는 방식에는 XML Mapper를 적용하는 방법도 있다.
(참고로 자바방식으로 구현했다면, XML Mapper는 사용할 수 없다.)

(DB를 설계하면서 View도 만들 수도 있고 각종 조합을 할 수도 있는데, 엔터티를 잘 파악해서 구현하면 될 것 같다.)


[Spring Boot 버전의 MyBatis 핵심 사용법]


@Mapper

public interface StudentMyBatisRepository {


@Select("select * from student")

public List<Student> findAll();


@Select("SELECT * FROM student WHERE id = #{id}")

public Student findById(long id);


@Delete("DELETE FROM student WHERE id = #{id}")

public int deleteById(long id);


@Insert("INSERT INTO student(id, name, passport) VALUES (#{id}, #{name}, #{passport})")

public int insert(Student student);


@Update("Update student set name=#{name}, passport=#{passport} where id=#{id}")

public int update(Student student);


}




11. FrontController.java - Controller


FrontController의 내용을 수정하도록 하겠다.



그림 27. FrontController.java - 수정하기


package com.exmple.web.controller;


import java.io.IOException;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;


import com.exmple.web.db.SqlMapSessionFactory;

import com.exmple.web.mapper.CompUsersMapper;

import com.exmple.web.model.CompUsers;


public class FrontController extends HttpServlet {

private static final long serialVersionUID = 1L;

    SqlSessionFactory factory = SqlMapSessionFactory.getSqlSessionFactory();


/**

* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)

*/

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


/**

* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)

*/

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

doAction(request, response);

}


protected void doAction(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

try (SqlSession session = factory.openSession()) {

  CompUsersMapper mapper = session.getMapper(CompUsersMapper.class);

  CompUsers user = mapper.findByUsername("user");

  

  System.out.println("계정명:" + user.getUsername());

  

}

}


}



파일명: FrontController.java


[첨부(Attachments)]

FrontController.zip




12. 맺음글(Conclusion)


MyBatis 3.5.5와 Maven, JSP/Servlet 그리고 Oracle Databases를 쉽고 빠르게 연동하는 방법에 대해서 소개하였다.


1. [JSP] 20. MyBatis-3.5.5, HikariCP 3.4.2 연동 - Maven(Servlet) Spring 제거버전 (Oracle 19g) - Java 방식, 2020-10-01

https://yyman.tistory.com/1435





* 참고 자료(References)


1. MyBatis - 마이바티스 3 | 시작하기, https://mybatis.org/mybatis-3/ko/getting-started.html, Accessed by 2020-10-01, Last Modified 2020-06-05.

- 비고: 사용 방법이 담겨있음.


2. [JSP] 8. 영속프레임워크 MyBatis를 활용한 CRUD 구현 - JSP와 Oracle (XML 방식), https://yyman.tistory.com/1390, Accessed by 2020-10-01, Last Modified 2020-09-19.

- 비고: MyBatis 셋팅 방법을 참고하였으며, XML 방식을 시도하였으나 안 되었다. (Spring Beans가 안 되는 것을 알게 되었음.)


3. [Spring-Framework] 22(번외). STS 4.4 - Spring Boot Starter - MyBatis(Boot용), HikariCP 3.4 사용하기, https://yyman.tistory.com/1432, Accessed by 2020-10-01, Last Modified 2020-10-01.

- 비고: HikariCP의 properties 방식에 대해서 다시 살펴보았다.

반응형
728x90
300x250

[Spring-Framework] 23(번외). STS 4.4 - Spring Boot Starter - 배포(Jar, War방식)


이전의 글을 참고하여 진행하면 될 것 같다.

Spring Boot Starter의 배포 방식에는 크게 "Jar", "War"로 구성된다.

Spring Boot Starter의 Jar파일 배포는 maven Build를 통해서 이뤄진다.

내장 톰캣 웹서버까지 함께 배포해버리는 단점이 있다.

차이점이 Spring Legacy Project로 생성한 Spring Framework는 일반적인 배포에서는 간단하게 War 배포를 할 수 있다.


- IDE: Spring Tool-Suite 4-4.7.2 Releases (2020-06-29 기준 최신 버전) // 대략



1. Spring Boot Starter - 배포 방법 JAR


Jar 방식으로 배포하는 방법에 대해서 소개하겠다.



그림 1. Run에서 Maven Build하기


(작업하고 있는) Spring Boot 프로젝트를 선택한다.

Run 메뉴를 클릭한다.

Run As에서 5 Maven Build...를 클릭한다. (은근히 햇갈리게 되어 있음.)



그림 2. Run에서 Maven Build하기

Goals에 package

Profiles은 비워둔다.

Run 버튼을 누른다.




그림 3. 빌드 기다리는 모습(1)


시간이 걸린다. 다운로드 과정 등이 있다. 알아서 배포 작업이 잘 되고 있는지 태스트 작업도 하고 그렇다.



그림 4. 빌드 기다리는 모습(2)



그림 5. 빌드 완료된 모습(1)


BUILD SUCCESS라고 뜨면 잘 된 것이다. 빨강색 뜨면 안 좋은 것이다. (오류도 잡아야 하고 그렇다. ㅠㅠ)



그림 6. 빌드 완료된 모습(2)


console 대화창 내용을 클릭한다.

Ctrl + F를 누른다.

jar를 검색한다.


무슨 파일명으로 배포되었는지 알기 위해서이다.



그림 7. 빌드 완료된 모습(3)


프로젝트 창을 F5키로 새로고침한다.

target 폴더를 클릭해보면, 배포된 파일을 살펴볼 수 있다.



그림 8. 빌드 완료된 모습(4)


쉬워보여도 쉬운 과정이 아니다. 또 실제 서버에 빌드해서 태스트를 해야 한다.

또 안 되면, 다시 Eclipse 작업창에서 삽질을 해야 한다.



2. Spring Boot Starter - 배포 방법 War


war 배포 방법은 다소 어렵다. 코드 몇 줄이긴 하지만, 작성해줘야 한다.

코드 부분도 소개하도록 하겠다.



2-1. war 배포 - pom.xml


pom.xml 수정에 대한 사항이다.



그림 9. pom.xml 파일 수정(1) - 변경해야 할 내용



그림 10. pom.xml 파일 수정(2) - 변경해야 할 내용



그림 11. pom.xml 파일 수정(2) - 변경해야 할 내용


아래의 내용을 열어보면, 확인할 수 있다. (굵기 표시로 변경 작업을 한 내역을 표시하였음.)





2-2. War 배포 - SpringBootMvcDemoApplication.java


사소해보이지만, 코드 변경이 중요하다.

이거 안 해주면, pom.xml 설정 잘 되어 있어도 오류가 뜬다.



그림 12. pom.xml 파일 수정(2) - 변경해야 할 내용


package com.springmvc.home;


import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.boot.builder.SpringApplicationBuilder;

import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;


@SpringBootApplication

public class SpringBootMvcDemoApplication extends SpringBootServletInitializer {


public static void main(String[] args) {

//SpringApplication.run(SpringBootMvcDemoApplication.class, args);

SpringApplication app = new SpringApplication(SpringBootMvcDemoApplication.class);

app.run(args);

}


@Override

protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {

return builder.sources(SpringBootMvcDemoApplication.class);

}


}



파일명: SpringBootMvcDemoApplication.java


[첨부(Attachments)]

SpringBootMvcDemoApplication.zip





2-3. Run As로 Maven Build하기 - War 배포


지금부터 작업은 배포를 하는 것이다.



그림 13. Run의 Maven build 메뉴 모습


Run 메뉴를 클릭한다.

Run As의 Maven build를 클릭한다.




그림 14. Select Configuration


SpringBootMVCDemo (1) : package를 선택하고 OK를 누른다.




그림 15. 빌드가 완료된 모습


빌드하는 데 시간이 다소 소요된다.

기다리면 된다.


BUILD SUCCESS라고 녹색 대화 글이 나오면 잘 된 것이다.



2-4. 톰캣 서버에 올리기 - war 배포


웹 서버에 실제로 배포하는 방법에 대해서 소개하겠다.








그림 16. 빌드가 완료된 파일 (1)


demo-0.0.1-SNAPSHOT.war 파일을 선택한다.

복사한다. (Ctrl + C)




그림 17. 톰캣 서버의 webapps 폴더에 넣어주기 (2)


톰캣 서버 설치 경로의 webapps에 들어간다.

붙여넣기를 한다. (Ctrl + V)




그림 18. 톰캣 서버의 bin - startup 실행해주기


bin폴더에서 startup.bat을 더블 클릭한다.

서버를 실행해준다.




그림 19. 서버가 실행되고 나면, 압축해제가 된 모습


정상적으로 폴더가 생성되었다면, 배포가 잘 된 것이다.




그림 20. 서버 실행 모습


Tomcat 서버에 보면, 익숙한 모습으로 Spring Security 생성 암호가 있다.

Using generated security password:의 내용을 복사한다.


Username: User

Password: 복사한 내용의 비밀번호(암호화된 랜덤 형태의 암호이다)




그림 21. 로그인 후 배포된 모습


정상적으로 화면이 보인다면, 배포가 성공적으로 이뤄진 것이다.



3. 결론(Conclusion)


서버 배포 작업도 개발자라면 경험해봐야 할 중요한 부분이라고 생각된다.

다소 스프링 부트는 배포 부분에 있어서는 쉽지만 않다.


서버 배포에 대해서 살펴보았고, Jar파일로 배포하는 방법에 대해서도 소개하였다.



* 참고자료(References)


1. [스프링 부트] war 파일과 외부 톰캣을 이용한 서버 배포, https://steady-hello.tistory.com/53, Accessed by 2020-09-30, Last Modified 2019-10-30.

반응형
728x90
300x250

[JSP] 18. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (2)


2부에서는 페이징, DB구현부, 모델, 서비스 영역에 대해서 소개하겠다.


1. [JSP] 18. JSP/Servlet MVC2 - 페이징네이션과 검색 그리고 오라클 프로젝트 (1)

https://yyman.tistory.com/1428




14. 페이징네이션 


실제경로: /target/generated-sources/annotations/com/smile/web/model

패키지명: com.smile.web.logic

파일명: Paging.java


아마 이거 하나면, 현존에 있는 페이지 기법은 충분히 소화되지 않겠냐는 생각이다.

물론 정답은 아니다. 오류가 있을 수도 있다.


이러한 복잡한 문제에 직면했을 때, 수학을 공부 많이 해야 한다고 생각한다.

머리가 좋으신 분들이 많으시니깐 더 좋은 방법이 있다면, 개선해봐도 좋을 듯 싶다.


package com.smile.web.logic;


public class Paging {

    private long pageSize; // 게시 글 수

    private long firstPageNo; // 첫 번째 페이지 번호

    private long prevPageNo; // 이전 페이지 번호

    private long startPageNo; // 시작 페이지 (페이징 네비 기준)

    private long pageNo; // 페이지 번호

    private long endPageNo; // 끝 페이지 (페이징 네비 기준)

    private long nextPageNo; // 다음 페이지 번호

    private long finalPageNo; // 마지막 페이지 번호

    private long totalCount; // 게시 글 전체 수

    private long dbStartNum; // db 조회 (시작번호)

    private long dbEndNum; // db 조회 (종료번호)


    /**

     * @return the pageSize

     */

    public long getPageSize() {

        return pageSize;

    }


    /**

     * @param pageSize the pageSize to set

     */

    public void setPageSize(long pageSize) {

        this.pageSize = pageSize;

    }


    /**

     * @return the firstPageNo

     */

    public long getFirstPageNo() {

        return firstPageNo;

    }


    /**

     * @param firstPageNo the firstPageNo to set

     */

    public void setFirstPageNo(long firstPageNo) {

        this.firstPageNo = firstPageNo;

    }


    /**

     * @return the prevPageNo

     */

    public long getPrevPageNo() {

        return prevPageNo;

    }


    /**

     * @param prevPageNo the prevPageNo to set

     */

    public void setPrevPageNo(long prevPageNo) {

        this.prevPageNo = prevPageNo;

    }


    /**

     * @return the startPageNo

     */

    public long getStartPageNo() {

        return startPageNo;

    }


    /**

     * @param startPageNo the startPageNo to set

     */

    public void setStartPageNo(long startPageNo) {

        this.startPageNo = startPageNo;

    }


    /**

     * @return the pageNo

     */

    public long getPageNo() {

        return pageNo;

    }


    /**

     * @param pageNo the pageNo to set

     */

    public void setPageNo(long pageNo) {

        this.pageNo = pageNo;

    }


    /**

     * @return the endPageNo

     */

    public long getEndPageNo() {

        return endPageNo;

    }


    /**

     * @param endPageNo the endPageNo to set

     */

    public void setEndPageNo(long endPageNo) {

        this.endPageNo = endPageNo;

    }


    /**

     * @return the nextPageNo

     */

    public long getNextPageNo() {

        return nextPageNo;

    }


    /**

     * @param nextPageNo the nextPageNo to set

     */

    public void setNextPageNo(long nextPageNo) {

        this.nextPageNo = nextPageNo;

    }


    /**

     * @return the finalPageNo

     */

    public long getFinalPageNo() {

        return finalPageNo;

    }


    /**

     * @param finalPageNo the finalPageNo to set

     */

    public void setFinalPageNo(long finalPageNo) {

        this.finalPageNo = finalPageNo;

    }


    /**

     * @return the totalCount

     */

    public long getTotalCount() {

        return totalCount;

    }


    /**

     * @param totalCount the totalCount to set

     */

    public void setTotalCount(long totalCount) {

        this.totalCount = totalCount;

        this.makePaging();

        this.makeDbPaging();

    }


    /**

     * 페이징 생성 (setTotalCount에서 호출함)

     */

    private void makePaging() {

   

        if (this.totalCount == 0) 

        return; 

        

        // 게시 글 전체 수가 없는 경우

        

        if (this.pageNo == 0) 

        this.setPageNo(1); // 기본 값 설정

        

        if (this.pageSize == 0) 

        this.setPageSize(10); // 기본 값 설정


        long finalPage = (totalCount + (pageSize - 1)) / pageSize; // 마지막 페이지

        

        if (this.pageNo > finalPage) 

        this.setPageNo(finalPage); // 기본 값 설정


        if (this.pageNo < 0 || this.pageNo > finalPage) 

        this.pageNo = 1; // 현재 페이지 유효성 체크


        boolean isNowFirst = pageNo == 1 ? true : false; // 시작 페이지 (전체)

        boolean isNowFinal = pageNo == finalPage ? true : false; // 마지막 페이지 (전체)


        long startPage = ((pageNo - 1) / 10) * 10 + 1; // 시작 페이지 (페이징 네비 기준)

        long endPage = startPage + 10 - 1; // 끝 페이지 (페이징 네비 기준)


        if (endPage > finalPage) { // [마지막 페이지 (페이징 네비 기준) > 마지막 페이지] 보다 큰 경우

            endPage = finalPage;

        }


        this.setFirstPageNo(1); // 첫 번째 페이지 번호


        if (isNowFirst) {

            this.setPrevPageNo(1);    // 이전 페이지 번호

        } else {

            this.setPrevPageNo(((pageNo - 1) < 1 ? 1 : (pageNo - 1))); // 이전 페이지 번호

        }


        this.setStartPageNo(startPage); // 시작 페이지 (페이징 네비 기준)

        this.setEndPageNo(endPage); // 끝 페이지 (페이징 네비 기준)


        if (isNowFinal) {

            this.setNextPageNo(finalPage); // 다음 페이지 번호

        } else {

            this.setNextPageNo(((pageNo + 1) > finalPage ? finalPage : (pageNo + 1))); // 다음 페이지 번호

        }


        this.setFinalPageNo(finalPage); // 마지막 페이지 번호

        

    }

    

    private void makeDbPaging() {

   

    long current = this.getPageNo();

    long weight = this.getPageSize();

    long endNum = 0;

   

    this.setDbEndNum( current * weight );

   

    endNum = this.getDbEndNum();

    this.setDbStartNum( (endNum - weight) + 1 );

   

    }


    /**

     * @return the dbStartNum

     */

public long getDbStartNum() {

return dbStartNum;

}



    /**

     * @param dbStartNum the dbStartNum to set

     */

public void setDbStartNum(long dbStartNum) {

this.dbStartNum = dbStartNum;

}


    /**

     * @return dbEndNum

     */


public long getDbEndNum() {

return dbEndNum;

}



    /**

     * @param dbEndNum the dbEndNum to set

     */

public void setDbEndNum(long dbEndNum) {

this.dbEndNum = dbEndNum;

}

    

}


파일명: Paging.java


[첨부(Attachments)]

Paging.zip




15. Board.java - 모델(Model)


실제경로: /target/generated-sources/annotations/com/smile/web/model

패키지명: com.smile.web.model

파일명: Board.java


게시판 모델에 대한 설계이다.


package com.smile.web.model;


import java.sql.Timestamp;


public class Board {


private long id;

private String name;

private String subject;

private String memo;

private long count;

private Timestamp regidate;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getSubject() {

return subject;

}

public void setSubject(String subject) {

this.subject = subject;

}

public String getMemo() {

return memo;

}

public void setMemo(String memo) {

this.memo = memo;

}


public long getCount() {

return count;

}


public void setCount(long count) {

this.count = count;

}


public Timestamp getRegidate() {

return regidate;

}


public void setRegidate(Timestamp regidate) {

this.regidate = regidate;

}

}



파일명: Board.java


[첨부(Attachments)]

Board.zip




16. BoardService.java - Service 영역


실제경로: /target/generated-sources/annotations/com/smile/web/service

패키지명: com.smile.web.service

파일명: BoardService.java


게시판 서비스에 필요한 영역을 구현한 것이다.


package com.smile.web.service;


import java.util.List;


import com.smile.web.model.Board;


public class BoardService {

private static BoardService service = null;

private static BoardServiceImpl dao = null;

private BoardService() {}

public static BoardService getInstance() {


        if(service == null){

        service = new BoardService();

    dao = BoardServiceImpl.getInstatnce();

        }


        return service;

}

public List<Board> getBoardList(long startNum, long endNum){

return dao.getBoardList(startNum, endNum);

}

public long getTotalCount() {

return dao.getTotalCount();

}

public List<Board> getBoardKeywordList(String keyword, long startNum, long endNum){

return dao.getBoardKeywordList(keyword, startNum, endNum);

}


public long getTotalKeywordCount(String keyword) {

return dao.getTotalKeywordCount(keyword);

}

}



파일명: BoardService.java


[첨부(Attachments)]

BoardService.zip





16. BoardServiceImpl.java - Service 영역(DAO)


실제경로: /target/generated-sources/annotations/com/smile/web/service

패키지명: com.smile.web.service

파일명: BoardServiceImpl.java


DAO 영역에 관한 것이다. (실제 DB를 처리하는 영역)


package com.smile.web.service;


import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.ArrayList;

import java.util.List;


import com.smile.web.db.DBFactory;

import com.smile.web.model.Board;


public class BoardServiceImpl {

private static BoardServiceImpl boardDAO;

private static DBFactory dbPool;

private BoardServiceImpl() {}

public static BoardServiceImpl getInstatnce() {

if ( boardDAO == null ) {

boardDAO = new BoardServiceImpl();

dbPool = new DBFactory();

}

return boardDAO;

}

public List<Board> getBoardList(long startNum, long endNum){

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    List<Board> boardList = new ArrayList<Board>();


    String sql = "select * from (select /*+INDEX_DESC(tbl_board pk_board) */ rownum rn, A.*" + 

    " from board A order by id desc) " + 

    "where rn >= ? and rn <= ?";


    //System.out.println(sql);

    //System.out.println(startNum + "/" + endNum);

   

    try {

    conn = dbPool.getConnection();

    pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1, startNum);

    pstmt.setLong(2, endNum);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

    // 데이터가 존재할 때, 노드 생성

    node = new Board();

   

    node.setId(rs.getLong(2));

    node.setSubject(rs.getNString(3));

    node.setMemo(rs.getNString(4));

    node.setName(rs.getNString(5));

   

    boardList.add(node);

   

    System.out.println("rs:" + rs.getLong(1));

   

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return boardList;

}

public List<Board> getBoardKeywordList(String keyword, long startNum, long endNum){

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    String allKeyword = "%" + keyword + "%";

   

    List<Board> boardList = new ArrayList<Board>();


    String sql = "select * " + 

    "from (select /*+INDEX_DESC(tbl_board pk_board) */ rownum rn, A.* " + 

    "from board A where subject like ? order by id desc) " + 

    "where rn >= ? and rn <= ?";

   

    //System.out.println(sql);

   

    try {

    conn = dbPool.getConnection();

    pstmt = conn.prepareStatement(sql);

    pstmt.setNString(1, allKeyword);

    pstmt.setLong(2, startNum);

    pstmt.setLong(3, endNum);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

    // 데이터가 존재할 때, 노드 생성

    node = new Board();

   

    node.setId(rs.getLong(2));

    node.setSubject(rs.getNString(3));

    node.setMemo(rs.getNString(4));

    node.setName(rs.getNString(5));

   

    boardList.add(node);

   

    //System.out.println("rs:" + rs.getLong(1));

   

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return boardList;

}

public long getTotalCount() {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    long cnt = 0;


    String sql = "select count(*) from board";

   

    System.out.println("sql:" + sql);


    try {

    conn = dbPool.getConnection();

    //System.out.println( conn.getSchema() );

    pstmt = conn.prepareStatement(sql);

   

    rs = pstmt.executeQuery();

    //System.out.println( rs.getFetchSize() );


    if ( rs.next() ) {

    cnt = rs.getLong(1);

        System.out.println("전체갯수:" + cnt + "/" + rs.getLong(1));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return cnt;

}

public long getTotalKeywordCount(String keyword) {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    Board node = null;

   

    String allKeyword = "%" + keyword + "%";

   

    long cnt = 0;


    String sql = "select count(*) from board where subject like ?";

   

    //System.out.println("sql:" + sql + "/키:" + keyword);


    try {

    conn = dbPool.getConnection();

    /// System.out.println( conn.getSchema() );

    pstmt = conn.prepareStatement(sql);

    pstmt.setNString(1, allKeyword);

   

    rs = pstmt.executeQuery();

    /// System.out.println( rs.getFetchSize() );


    if ( rs.next() ) {

    cnt = rs.getLong(1);

        System.out.println("특정 갯수:" + cnt + "/" + rs.getLong(1));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    dbPool.close(conn, pstmt, rs);

    }

return cnt;

}


}


파일명: BoardServiceImpl.java


[첨부(Attachments)]

BoardServiceImpl.zip



17. 데이터베이스 - 응용 영역


이 부분이 의외로 어려울 수 있다고 본다. 머리를 조금 써야 한다.


[개선 후 SQL 파일 내용]


-- Oracle 11 - 자동번호 생성 테이블 정의
-- Table 생성 (BOARD)
-- NEW.ID (Table의 id를 가리킴)
CREATE TABLE board
(
    id NUMBER PRIMARY KEY,
    name VARCHAR2(30),
    subject VARCHAR2(30),
    memo NCLOB,
    count NUMBER,
    regidate DATE
);

-- Sequence 정의
CREATE SEQUENCE board_sequence
START WITH 1
INCREMENT BY 1;

-- Trigger 생성
-- BEFORE INSERT on '테이블명'
CREATE OR REPLACE TRIGGER board_trigger
BEFORE INSERT
    ON board
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
SELECT board_sequence.nextval INTO :NEW.ID FROM dual;
END;


/* 데이터 추가 */
INSERT INTO board (name, subject, memo, count, regidate) VALUES ('홍길동', '안녕하세요.', '메모메모', '0', '2020-09-29 11:11:00');

/* 데이터 등록 후 커밋할 것(대량 정보 처리 후) */
COMMIT;


-- 싱글 쿼리 (페이징)
SELECT * FROM (
SELECT /*+ INDEX_DESC(Z OP_SAMPLE_PK) */ ROWNUM AS RNUM, Z.* FROM (
SELECT * from board order by id desc
) Z WHERE ROWNUM <= 10
) WHERE RNUM >= 1

-- 특정 싱글 쿼리 SQL
SELECT * FROM (
SELECT /*+ INDEX_DESC(Z OP_SAMPLE_PK) */ ROWNUM AS RNUM, Z.* FROM (
SELECT * from board where subject like '%야해해%' order by id desc
) Z WHERE ROWNUM <= 10
) WHERE RNUM >= 1


파일명: board-tbl-oracle_개선후.sql (변경해도 되는 쿼리)  /  board-tbl-oracle_개선전.sql (이 글의 16번 소스코드에 적용된 페이징 쿼리)


(16번 파일의 페이징 쿼리를 수정해서 사용해도 됨.)

-> 개선 전 쿼리라고 해서 파일을 두 개로 두었음.


[첨부(Attachments)]

board-tbl-oracle.zip (업데이트: 2020-10-11)


페이징 로직 설계가 되었다면, 다음 계획해야 할 작업이 실질적으로는 이 작업이다.

의외로 작업을 하다보면, 골치가 아픈 문제들이 많이 생긴다.


[Oracle Databases 코너에 쿼리 관련 작업에 대해서 추가로 보충하여 소개하도록 하겠다.]

- 단일 테이블 게시물 쿼리 방법, 뷰 테이블 쿼리 방법, 테이블 조인(2개 이상) 사용 방법


-> 태스트 결과로는 "Java 페이징 로직"은 크게 문제가 없다. 
    다만, 나머지 부분은 쿼리라고 본다.



[쿼리 관련 - 보충 글]
1. [Oracle Databases] 번외글 - 게시판 페이징 관련 로직 쿼리, 2020-10-11.
- https://yyman.tistory.com/1466




18. 뷰 - css


css 하나 만들어준다. 보안 영역에 만들지 말고 외부 영역에 만들어준다.



그림 23. style.css 프로젝트 위치


생성할 폴더: 

/src/main/webapp/board

/src/main/webapp/board/css


생성할 파일

/src/main/web/app/board/css/style.css


사실 style.css을 먼저 만들고 나서 작업하는 건 절대적으로 아니다.

웹 페이지를 보고 하나 하나 왕복 화면 전환 등 복합적으로 하면서 수많은 태스트과정을 거쳐서 작성하게 된다.

css 작업을 하는데 있어서도 어떻게 하면 깔끔하면서 표준을 지킬지 고민을 많이 해봐야 한다고 본다.


@charset "utf-8";


/* 1. 제목 */

h3{

font-size:20px;

font-family:'Nanum Gothic';

text-align:center;

}


/* 2. 게시판 */

/* 게시판 목록 출력 */

.board_list{

border-top:1px solid #e2e2e2;

border-bottom:1px solid #e2e2e2;

font-size:12px;

font-family:'Nanum Gothic';

width:900px;

margin:auto;

text-align:center;

}


/* 제목 */

.board_list th{


border-right:1px solid #666;

border-bottom:1px solid #666;

font-size:15px;

font-family:'Nanum Gothic';

height:20px;

text-align:center;

background-color:#eeeeee;

}


/* 내용 */

.board_list td{


border-right:1px solid #e2e2e2;

font-size:15px;

font-family:'Nanum Gothic';

height:20px;

text-align:center;

}


/* 3. 페이징 네이션 */

.paginate{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

}


.paginate .first{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .prev{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .next{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .last{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


.paginate .choice{


font-size:20px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

margin-right:10px;

}


/* 4. 링크 */

a{

text-decoration:none;

color:#666;

}


/* 5. 검색 영역 */

.searchArea{


font-size:15px;

font-family:'Nanum Gothic';

font-color:#666;

text-align: center;

}


파일명: style.css


[첨부(Attachments)]

style.zip




19. 뷰(view) - jsp파일 작성하기


이번에 소개할 것은 include를 실제 적용한 코드이다.



그림 24. board 폴더 내 파일들 (작업할 부분)


생성할 폴더: 

/src/main/webapp/WEB-INF/views/

/src/main/webapp/WEB-INF/views/board


생성할 파일

/src/main/webapp/WEB-INF/views/board/list.jsp

/src/main/webapp/WEB-INF/views/board/search.jsp

/src/main/webapp/WEB-INF/views/board/paging.jsp


list.jsp를 호출하면, search.jsp, paging.jsp도 함께 호출이 된다.

기능을 전문적으로 분리하여 설계한 것이다.




20. 뷰(view) - list.jsp


list.jsp에 관한 것이다.

코드를 보고 한눈에 알 수 없는 사람들을 위해서 실제 동작 결과화면을 소개하겠다.



그림 25. list.jsp 화면 영역 구성도



<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page import = "java.util.*" %>

<%@ page import = "com.smile.web.model.*" %>

<%@ page session="false" %>

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>게시물 목록</title>

<style>

@import url('https://fonts.googleapis.com/css?family=Nanum+Gothic:400,700,800');


</style>

<link href="css/style.css" rel="stylesheet" type="text/css" />

</head>

<body>

<%

List<Board> boardList = (List<Board>)request.getAttribute("list");

%>


<h3>게시물 목록</h3>


<!-- 목록 출력 -->

<table class="board_list">

<tr>

<th style="width:15%;">

번호(Num)

</th>

<th>

제목(Subject)

</th>

<th style="width:13%;">

글쓴이(Author)

</th>

<th style="width:13%; border-right:none;">

조회수(Count)

</th>

</tr>

<%

for(Board board:boardList){

%>

<tr>

<td style="width:15%;">

<%= board.getId() %>

</td>

<td>

<%= board.getSubject() %>

</td>

<td style="width:13%;">

<%= board.getName() %>

</td>

<td style="width:13%; border-right:none;">

<%= board.getCount() %>

</td>

</tr>

<%

}

%>

</table>


<!-- 페이징 -->

<jsp:include page="/WEB-INF/views/board/paging.jsp">

<jsp:param name="customURL" value="${pagingUrl}" />

    <jsp:param name="firstPageNo" value="${paging.firstPageNo}" />

    <jsp:param name="prevPageNo" value="${paging.prevPageNo}" />

    <jsp:param name="startPageNo" value="${paging.startPageNo}" />

    <jsp:param name="pageNo" value="${paging.pageNo}" />

    <jsp:param name="endPageNo" value="${paging.endPageNo}" />

    <jsp:param name="nextPageNo" value="${paging.nextPageNo}" />

    <jsp:param name="finalPageNo" value="${paging.finalPageNo}" />

</jsp:include>


<!-- 검색 -->

<jsp:include page="/WEB-INF/views/board/search.jsp" />


</body>

</html>


파일명: list.jsp


[첨부(Attachments)]

list.zip



21. 뷰(view) - paging.jsp


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>



<div class="paginate">

    <a href="${param.customURL}page=${param.firstPageNo}" class="first">처음 페이지</a>

    <a href="${param.customURL}page=${param.prevPageNo}" class="prev">이전 페이지</a>

    <span>

        <c:forEach var="i" begin="${param.startPageNo}" end="${param.endPageNo}" step="1">

            <c:choose>

                <c:when test="${i eq param.pageNo}"><a href="${param.customURL}page=${i}" class="choice">${i}</a></c:when>

                <c:otherwise><a href="${param.customURL}page=${i}">${i}</a></c:otherwise>

            </c:choose>

        </c:forEach>

    </span>

    <a href="${param.customURL}page=${param.nextPageNo}" class="next">다음 페이지</a>

    <a href="${param.customURL}page=${param.finalPageNo}" class="last">마지막 페이지</a>

</div>



파일명: paging.jsp


[첨부(Attachments)]

paging.zip




22. 뷰(view) - search.jsp


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>


<div class="searchArea">

<form id="searchForm" action="list.do" method='get'>

<select name="type">

<option value="T">제목</option>

<option value="C">내용</option>

<option value="W">작성자</option>

</select>

<input type="text" name="keyword">

<button class="">검색</button>

</form>

</div>


파일명: search.jsp


[첨부(Attachments)]

search.zip




23. 데이터베이스 작업시 성능 - 꼭 파악하면서 작업해보기


오늘 날 게시판 뿐만 아니라 DB의 비중이 매우 중요한 시대에 직면해 있다.

성능 측정 꼭 해보기 바란다.

동작도 물론 중요한데, 결과는 정확한지 등 많이 고민하고 수 십번, 수 백법 이상 찍어봐야 한다.



그림 26. 데이터베이스 작업 모습 - 오라클 SQL Developer




* 맺음글(Conclusion)


페이징네이션 기반의 게시판 프로젝트에 대해서 살펴보았다.




* 참고자료(References)


1. [Oracle] 오라클 데이터타입(DataType) 총정리, https://coding-factory.tistory.com/416, Accessed by 2020-09-29, Last Modified 2019-11-03.


2. [JSP]include 와 forward 의 페이지 이동, https://jerryjerryjerry.tistory.com/31, Accessed by 2020-09-29, Last Modified 2018-04-13.


3. 오라클 페이징 쿼리, 오라클 paging 방법 - 개발자 삽질 일기, https://programmer93.tistory.com/4, Accessed by 2020-09-29, Last Modified 2019.


4. [OracleDB] 페이징(Paging) 처리하는 법, https://m.blog.naver.com/wideeyed/221796538283, Accessed by 2020-09-29, Last Modified 2020-02-04.

반응형
728x90
300x250

[Spring-Framework] 20. Spring MVC - Spring Framework 5 REST, Jackson, Commons-FileUpload - (2)


2부에서는 Controller, Model, View, Util(한글 문제)을 집중적으로 소개하겠다.


1. [Spring-Framework] 19. Spring MVC - Spring Framework 5 REST, Jackson, Commons-FileUpload - (1), 2020-09-28

https://yyman.tistory.com/1425



12. Controller - HomeController.java



초기 프로젝트를 생성하면 자동으로 만들어지는 HomeController.java이다.

REST 프로젝트 작업에 필요한 형태로 추가 작성 및 변형하였다.


- REST Client에 대해서 자세히 소개하였음. (CRUD - GET, POST, PUT, DELETE 클라이언트)


package com.example.restexample2.controller;


import java.io.UnsupportedEncodingException;

import java.net.URI;

import java.net.URLDecoder;

import java.nio.charset.Charset;

import java.text.DateFormat;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Date;

import java.util.HashMap;

import java.util.List;

import java.util.Locale;

import java.util.Map;

import java.util.concurrent.atomic.AtomicLong;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

import org.springframework.http.converter.FormHttpMessageConverter;

import org.springframework.http.converter.HttpMessageConverter;

import org.springframework.http.converter.StringHttpMessageConverter;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.util.LinkedMultiValueMap;

import org.springframework.util.MultiValueMap;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.client.HttpClientErrorException;

import org.springframework.web.client.HttpServerErrorException;

import org.springframework.web.client.RestTemplate;

import org.springframework.web.util.UriComponents;

import org.springframework.web.util.UriComponentsBuilder;


import com.example.restexample2.model.Board;

import com.example.restexample2.model.Greeting;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.fasterxml.jackson.databind.deser.impl.CreatorCandidate.Param;


@Controller

public class HomeController {

private static final Logger logger = LoggerFactory.getLogger(HomeController.class);;

@RequestMapping(value = "/", method = RequestMethod.GET)

public String home(Locale locale, Model model) {

logger.info("Welcome home! The client locale is {}.", locale);

Date date = new Date();

DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "home";

}


    /**

     * 파일 업로드 입력 화면

     */

@RequestMapping(value = "/fileUploadView", method = RequestMethod.GET)

public String fileUploadView(Locale locale, Model model) {

logger.info("Welcome FileUpload! The client locale is {}.", locale);

Date date = new Date();

DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate );

return "file/upload";

}



    /**

     * GET 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapGet", method = RequestMethod.GET)

public String helloClient(String regId, String time) throws UnsupportedEncodingException{

// Get 응답 방법론

String url = "http://localhost:8080/restexample2/testValue2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .queryParam("regId", regId)

                .queryParam("tmFc", time)

                .queryParam("_type", "json")

                .build(false);    //자동으로 encode해주는 것을 막기 위해 false

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        Object response = restTemplate.getForObject(builder.toUriString(), List.class);

        

        if ( response != null) {


            List<Integer> map = (List<Integer>) response;

        System.out.println("map" + map);

        }

        

        //return response;


return "home";

}


    /**

     * POST 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapPost")

public String helloClient2(String regId, String time) throws UnsupportedEncodingException{

// POST 응답 방법

//String url = "http://..............";

String url = "http://localhost:8080/restexample2/testValue2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        /*

        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .queryParam("regId", regId)

                .queryParam("tmFc", time)

                .queryParam("_type", "json")

                .build(false);    //자동으로 encode해주는 것을 막기 위해 false

        */

        

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        Object response = restTemplate.postForObject(url, parameters, List.class);

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Integer> map = (List<Integer>) response;

        System.out.println("map" + map.get(0));

        }

        

        //return response;


return "home";

}

    /**

     * PUT 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapPut/{boardIdx}")

public String helloClient3(String regId, 

String time,

@PathVariable(name="boardIdx", required=true) int boardIdx)

throws  UnsupportedEncodingException{

// PUT 방법

String url = "http://localhost:8080/restexample2/v1/api/board/2";

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        // 파라메터

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        Board updatedBoard = new Board();

        updatedBoard.setId(boardIdx);

        updatedBoard.setSubject("앙하하");

        updatedBoard.setName("수정했다.");

        updatedBoard.setMemo("메모지");

        

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        //Object response = restTemplate.postForObject(url, parameters, List.class);

        restTemplate.put ( url, updatedBoard, parameters );

        

        // 데이터 확인하기

        url = "http://localhost:8080/restexample2/v1/api/board";


        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .build(false);


        Board[] response = restTemplate.getForObject(builder.toUriString(), Board[].class );

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Board> listData = Arrays.asList(response);

        System.out.println("list(Size):" + listData.size());

       

        try {

        if ( listData.size() != 0 && (boardIdx - 1) >= 0) {

            System.out.println("boardIdx:" + (boardIdx - 1));

        Board boardTmp = listData.get(boardIdx - 1);

        System.out.println("board:" + boardTmp.getId() + "/" + boardTmp.getSubject());

        }

        }catch(Exception e) {

        e.printStackTrace();

        }

       

        }

        

        //return response;

return "home";

}


    /**

     * PUT 방식 - 클라이언트

     */

@SuppressWarnings("unchecked")

@RequestMapping(value = "/client/listMapDelete/{boardIdx}")

public String helloClient4(String regId, 

String time,

@PathVariable(name="boardIdx", required=true) int boardIdx)

throws  UnsupportedEncodingException{

   // PUT 방법

  String url = "http://localhost:8080/restexample2/v1/api/board/" + boardIdx;

        String serviceKey = "서비스키";

        String decodeServiceKey = URLDecoder.decode(serviceKey, "UTF-8");

        

        RestTemplate restTemplate = new RestTemplate();

        HttpHeaders headers = new HttpHeaders();

        headers.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));    //Response Header to UTF-8  

        

        // 파라메터

        MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();

        parameters.add("servicekey", decodeServiceKey);

        parameters.add("regId", regId);

        parameters.add("tmFc", time);

        parameters.add("_type", "json");

        

        //Object response = restTemplate.exchange(builder.toUriString(), HttpMethod.GET, new HttpEntity<String>(headers), String.class);

        

        // Object response = restTemplate.getForEntity(builder.toUriString(), List.class);

        //Object response = restTemplate.postForObject(url, parameters, List.class);

        restTemplate.delete ( url, parameters );

        

        // 데이터 확인하기

        url = "http://localhost:8080/restexample2/v1/api/board";


        UriComponents builder = UriComponentsBuilder.fromHttpUrl(url)

                .queryParam("serviceKey", decodeServiceKey)

                .build(false);


        Board[] response = restTemplate.getForObject(builder.toUriString(), Board[].class );

        // Object response = restTemplate.postForEntity(url, parameters, List.class);

        

        if ( response != null) {


            List<Board> listData = Arrays.asList(response);

        System.out.println("list(Size):" + listData.size());

       

        try {

        if ( listData.size() != 0 && (boardIdx - 1) >= 0) {

            System.out.println("boardIdx:" + (boardIdx - 1));

        Board boardTmp = listData.get(boardIdx - 1);

        System.out.println("board:" + boardTmp.getId() + "/" + boardTmp.getSubject());

        }

        }catch(Exception e) {

        e.printStackTrace();

        }

       

        }

        

        //return response;

return "home";

}

}



파일명: HomeController.java


[첨부(Attachments)]

HomeController.zip





13. Controller - JSONController.java



REST를 간단하게 입문하는 용도로 작성하였다.


package com.example.restexample2.controller;


import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

import java.util.concurrent.atomic.AtomicLong;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;


import com.example.restexample2.model.Greeting;


/**

 * Handles requests for the application home page.

 */

@RestController

public class JSONController {

private static final Logger logger = LoggerFactory.getLogger(JSONController.class);


private static final String template = "Hello, %s!";

private final AtomicLong counter = new AtomicLong();

@GetMapping("/greeting")

public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {

return new Greeting(counter.incrementAndGet(), String.format(template, name));

}

@GetMapping(value="/testValue") 

public String getTestValue(){

String TestValue = "레스트컨트롤러 테스트";

return TestValue;

}


@GetMapping(value="/testValue2") 

public List<Integer> getTestValue2(){

List<Integer> mList = new ArrayList<Integer>();

mList.add(1);

mList.add(2);

mList.add(3);

mList.add(4);

return mList;

/* 

* Error: pom.xml의 jackson-databind, jackson-core 추가할 것

* onverter found for return value of type: class java.util.ArrayList]

*/

}

@PostMapping(value="/testValue2") 

public List<Integer> getTestValue3(){

List<Integer> mList = new ArrayList<Integer>();

mList.add(1);

mList.add(2);

mList.add(3);

mList.add(4);

return mList;

/* 

* Error: pom.xml의 jackson-databind, jackson-core 추가할 것

* onverter found for return value of type: class java.util.ArrayList]

*/

}

@GetMapping(value="/getMap")

public Map<String, Greeting> getMap(){

Map<String, Greeting> map = new HashMap<>();

map.put("First", new Greeting(1, "Hello"));

map.put("Second", new Greeting(2, "Rest"));

return map;

}

}



파일명: JSONController.java


[첨부(Attachments)]

JSONController.zip




14. Model - Board.java



매우 간단한 게시판 구조에 대한 모델이다. 방명록도 어찌보면, 게시판의 한 종류가 될 수 있다.


package com.example.restexample2.model;


public class Board {

private long id;

private String subject;

private String name;

private String memo;

public long getId() {

return id;

}

public void setId(long id) {

this.id = id;

}

public String getSubject() {

return subject;

}

public void setSubject(String subject) {

this.subject = subject;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getMemo() {

return memo;

}

public void setMemo(String memo) {

this.memo = memo;

}

}



파일명: Board.java


[첨부(Attachments)]

Board.zip



15. Model - Greeting.java



사실 쉽고 간단한 코드가 더 어려운 코드라고 본다.

꼭 반드시 게시판 형태만 생각할 필요가 없다고 본다.

공부하는 데 있어서는 간단한 것이 사실 더 어려운 부분이라고 본다.


package com.example.restexample2.model;


public class Greeting {


private final long id;

private final String content;


public Greeting(long id, String content) {

this.id = id;

this.content = content;

}


public long getId() {

return id;

}


public String getContent() {

return content;

}

}


파일명: Greeting.java


[첨부(Attachments)]

Greeting.zip




16. Model - FileInfo.java


경로: /src/main/java/com/example/restexample2/model/FileInfo.java


파일에 관한 명세이다.


package com.example.restexample2.model;


import org.springframework.web.multipart.MultipartFile;


public class FileInfo {


private long num;

private String filename;

private long filesize;

private MultipartFile mediaFile;


public long getNum() {

return num;

}

public void setNum(long num) {

this.num = num;

}

public String getFilename() {

return filename;

}

public void setFilename(String filename) {

this.filename = filename;

}

public long getFilesize() {

return filesize;

}

public void setFilesize(long filesize) {

this.filesize = filesize;

}

public MultipartFile getMediaFile() {

return mediaFile;

}


public void setMediaFile(MultipartFile mediaFile) {

this.mediaFile = mediaFile;

}

}



파일명: FileInfo.java


[첨부(Attachments)]

FileInfo.zip





17. Controller - FileController.java


경로: /src/main/java/com/example/restexample2/controller/FileController.java


파일 업로드에 대한 기능이다.


package com.example.restexample2.controller;


import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileOutputStream;

import java.io.IOException;


import javax.servlet.ServletContext;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;


import org.apache.commons.fileupload.disk.DiskFileItemFactory;

import org.apache.commons.fileupload.servlet.ServletFileUpload;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.multipart.MultipartFile;


import com.example.restexample2.model.Board;

import com.example.restexample2.model.FileInfo;

import com.example.restexample2.util.HttpUtil;


@RestController

@RequestMapping ("/file")

public class FileController {

private static final Logger logger = LoggerFactory.getLogger(FileController.class);

     

     /**

      * 파일 멀티파트 업로드 Rest

      * {/

      * @param inputFile

      * @return 

      *  (주석 스타일 참고)

      */

     @RequestMapping(value = "/uploadFileModelAttribute/new", 

    method = {RequestMethod.POST },

    produces="text/plain;charset=UTF-8")

     public String multiFileUpload(@ModelAttribute Board boardVO, 

    @RequestParam("mediaFile")MultipartFile[] files, 

    Model model,

    HttpServletRequest req,

    HttpServletResponse res) throws IOException {

     

    boolean filechk = false;

     

         //디스크상의 프로젝트 실제 경로얻기

         //String contextRootPath = "c:" + File.separator + "upload";

    // String charset = "UTF-8";


    // req.setAttribute("charset", charset);

    // req.setCharacterEncoding(charset);

    // res.setContentType("text/html; charset=" + charset);

     

    String dirName = "upload" ; 

String contextRootPath = req.getSession().getServletContext().getRealPath("/") + dirName;

 

         System.out.println("실제경로:" + contextRootPath);


         //1. 메모리나 파일로 업로드 파일 보관하는 FileItem의 Factory 설정

         DiskFileItemFactory diskFactory = new DiskFileItemFactory(); //디스크 파일 아이템 공장

         diskFactory.setSizeThreshold(4096); //업로드시 사용할 임시 메모리

         diskFactory.setRepository(new File(contextRootPath + "/WEB-INF/temp")); //임시저장폴더

         

         //2. 업로드 요청을 처리하는 ServletFileUpload생성

         ServletFileUpload upload = new ServletFileUpload(diskFactory);

         upload.setSizeMax(3 * 1024 * 1024); //3MB : 전체 최대 업로드 파일 크기

         

         

         // 한글 깨짐 해결(버그)

         // String kor_a = new String(boardVO.getSubject().getBytes("8859_1"), "UTF-8");  

         System.out.println("게시물제목:" + HttpUtil.getISO8859toUTF8( boardVO.getSubject()) );

System.out.println("게시물작성자:" + HttpUtil.getISO8859toUTF8( boardVO.getName()) );

System.out.println("게시물내용:" + HttpUtil.getISO8859toUTF8( boardVO.getMemo()) );

System.out.println("파일(길이):" + files.length );

 

 

         for(MultipartFile mFile : files) {


             // 3. 파일 가져오기

    if ( mFile.getOriginalFilename().isEmpty() && 

      filechk == false ) {


            String msg = "Please select at least one mediaFile.<br/>(미디어 파일을 하나 이상 선택하십시오.)";

            model.addAttribute("msg", msg);

           

            return model.getAttribute("msg").toString();

    }

     

             // 4. 파일명 - 현재시간으로 생성

             String uploadedFileName = System.currentTimeMillis() + ""; 

             

        if (!mFile.getOriginalFilename().isEmpty()) {

         

        BufferedOutputStream outputStream = new BufferedOutputStream(

        new FileOutputStream(

        new File( contextRootPath + File.separator + "upload" + File.separator, uploadedFileName )

));

       

       

                  System.out.println("파일명:" + mFile.getOriginalFilename());

                 

                  outputStream.write(mFile.getBytes());

                  outputStream.flush();

                  outputStream.close();

                 

                  filechk = true;                 

              } 

         

         }

         

    return "fileUploadForm";

   

     }

     

}


파일명: FileController.java


[첨부(Attachments)]

FileController.zip


비고: HttpUtil.java의 HttpUtil에 정의된 한글 출력 문제 등이 적용되어 있음.




18. Controller - BoardRestController.java


경로: /src/main/java/com/example/restexample2/controller/BoardRestController.java


게시판 시스템을 적용한 Rest 컨트롤러이다.

자료구조를 적절하게 재배치하여 실습에는 DB없이 가능한 수준으로 구현하였다.


package com.example.restexample2.controller;


import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;


import javax.servlet.http.HttpServletRequest;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.web.bind.annotation.DeleteMapping;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.PatchMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.PutMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;


import com.example.restexample2.model.Board;


@RestController

@RequestMapping("/v1/api")

public class BoardRestController {

private static final Logger logger = LoggerFactory.getLogger(BoardRestController.class);


private static int num = 1;

private List<Object> tmpBoard = new ArrayList<Object>(); 

//@Autowired

    //private BoardService boardService;


    // 조회 = GET

    // (전체 게시물)

@GetMapping("board")

    public List<Object> listBoard(HttpServletRequest request, @ModelAttribute Board board) throws Exception {

   

    logger.info("게시판 목록");

    logger.info("----------------------------------");

   

    Board createNode = new Board();

    createNode.setId(num);

    createNode.setName("홍길동");

    createNode.setMemo("메모");

    createNode.setSubject("주소지");

   

    // tmpBoard.put(num, createNode);

    // num = num + 1;

   

    return tmpBoard;

    //return this.boardService.selectBoardList(request, board);

    }


    // 조회 = GET

    // (특정 세부 게시물)

    @GetMapping("board/{boardIdx}")

    public Board detailBoard(HttpServletRequest request, 

    @PathVariable(name="boardIdx", required=true) int boardIdx)

    throws Exception {


    logger.info("게시판 조회");

    logger.info("----------------------------------");

    logger.info("게시판 특정 게시물 번호" + boardIdx);

   

    //return this.boardService.selectBoard(request, boardIdx);

    return (Board) tmpBoard.get(boardIdx);

    }

    

    // 등록 = POST

    @PostMapping("board/new")

    public void insertBoard(HttpServletRequest request, @RequestBody Board board) throws Exception {

   

    logger.info("게시판 삽입");

   

    board.setId(num);


    tmpBoard.add(board);

    System.out.println(num);

    num = num + 1;

        //this.boardService.insertBoard(request, board);

    }


    // 수정 = PUT, PATCH (전송 방식)

    // /member/{id} + body (json데이터 등)

    @PutMapping("board/{boardIdx}")

    @PatchMapping("board/{boardidx}")

    public void updateBoard(HttpServletRequest request, 

    @PathVariable(name="boardIdx", required=true) int boardIdx, 

    @RequestBody Board board) throws Exception {

   

    logger.info("게시판 수정");

   

    if ( !tmpBoard.isEmpty() ) {

   

    board.setId(boardIdx); // 고유키 그대로 유지할 것

    tmpBoard.set(boardIdx - 1, board);

    }

    //board.setBoardIdx(boardIdx);

        //this.boardService.updateBoard(request, board);

   

    }

    

    // 삭제 = DELETE(전송 방식)

    @DeleteMapping("board/{boardIdx}")

    public void deleteBoard(HttpServletRequest request,

    @PathVariable(name="boardIdx", 

    required=true) int boardIdx) throws Exception {

   

    logger.info("게시판 삭제");

   

    try {   

    tmpBoard.remove(boardIdx);

    }

    catch(Exception e) {

        logger.info("Null값");

    e.getStackTrace();

    }

   

    //this.boardService.deleteBoard(request, boardIdx);

    }


}



파일명: BoardRestController.java


[첨부(Attachments)]

BoardRestController.zip




19. Util - HttpUtil.java


경로: /src/main/java/com/example/restexample2/util/HttpUtil.java


한글 언어에 대한 문제 해결에 대해서 정의하였다.


package com.example.restexample2.util;


import java.io.UnsupportedEncodingException;


public class HttpUtil {



    // 버그 개선: euc-kr 검증

    public static boolean isEucKr(String s) {

        int len = s.length();

        char c;

        for (int i = 0; i < len; i++) {

            c = s.charAt(i);

            /// System.out.println("" + c + " = " + toHex(c));

            if (((c & 0xFFFF) >= 0xAC00) && ((c & 0xFFFF) <= 0xD7A3))

                return true;

            /// else if (((c & 0xFF00) != 0) && ((c & 0x00) == 0))

            ///     return false;

        }

        return false;

    }

    // 버그 개선: ISO8859-1 검증

    public static boolean isISO8859(String s) {

     

        int len = s.length();

        char c;

        for (int i = 0; i < len; i++) {

            c = s.charAt(i);

            /// System.out.println("" + c + " = " + toHex(c));

            if ((c & 0xFF00) != 0)

                return false;

        }

        

        return true;

    }

    

    public static String getISO8859toUTF8(String s) {

   

    try {

return new String(s.getBytes("8859_1"), "UTF-8");

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

return s;

}

   

    }

}



파일명: HttpUtil.java


[첨부(Attachments)]

HttpUtil.zip



20. View - home.jsp


경로: /src/main/webapp/WEB-INF/views/home.jsp


기본 생성된 jsp파일이다.


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page session="false" %>

<html>

<head>

<title>Home</title>

</head>

<body>

<h1>

Hello world! - Rest(REST)

</h1>


<P>

</P>

</body>

</html>



파일명: home.jsp


[첨부(Attachments)]

home.zip



21. View - upload.jsp


경로: /src/main/webapp/WEB-INF/views/file/upload.jsp


업로드 페이지에 관한 정의이다.



그림 33. login.jsp 파일 모습 - 사용자 인터페이스(User Interfaces)


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page session="false" %>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<html>

<head>

<title>다중 파일 업로드</title>

<meta charset="UTF-8">

</head>

<body>

<h3>다중 파일 업로드 및 다중 변수</h3>


<form action="file/uploadFileModelAttribute/new" method="POST"

  enctype="multipart/form-data">

    <table>

        <tr>

            <td>

            제목:

            <input type="text" name="subject" >

            </td>

            <td>

            이름:

            <input type="text" name="name" >

            </td>

            <td>

            내용:

            <input type="text" name="memo" >

            </td>

        </tr>

        <tr>

            <td>Select Files(파일을 선택하시오)</td>

            <td>

            <input type="file" name="mediaFile" multiple>

            </td>

            <td>

            <input type="file" name="mediaFile" multiple>

            </td>

        </tr>

        <tr>

        <td colspan="3">

                <button type="submit">Upload(업로드)</button>

        </td>

        </tr>

    </table>

</form>

</body>

</html>



파일명: login.jsp


[첨부(Attachments)]

upload.zip



22. 의외로 어려운 REST 작업 - Client에서 작업하기


REST Client 앱을 설치하여 작업을 시도했을 때 명세 등을 정의하질 못해서 작업이 쉽게 이뤄지지 못한 경우도 있다.

그래서 하나 만들어보게 되었다.


비고: 작업할 때는 조금 많이 찍어봐야 한다. (힘들고 더딘 작업 중 하나이다.)



그림 34. REST Client에서 처리할 때 사용될 수 있는 정보들


이러한 정보는 그냥 나온 것은 아니고, 조금 코드로 셈플이 나오도록 찍어봐야 한다.

안 찍어보면, 어떤 구조인지 모른다. 알 길이 없다.



그림 35. 코드 등으로 객체를 찍어보는 형태로 변형해주기


셈플 코드가 출력될 수 있는 환경으로 코드를 변형해준다.



그림 36. 객체 정보의 체계 - 출력


이런 형태로 정보들이 나오면, 잘 대입해보고 정리해보기도 하고 그래야 한다.

(태스트 작업이 소요됨)



그림 37. YARC REST Client에서 사용하기


자료를 가공해서 입력한 후, Send Request를 누른다.



예를 들면, GET 관련 코드를 통해서 Send Request를 누른 후에 아래에서 결과를 찾아볼 수 있다.

그러면 아래처럼 관련 유추할 수 있는 정보 단위 코드를 출력해볼 수 있다.


"3":{"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}}

(양식)


(응용 -> POST 등록 명령)

-> {"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}



(응용 -> PUT, PATCH 수정 명령)

-> {"id":3,"subject":"주소지","name":"홍길동","memo":"메모"}



-----------------------------------------------------


POST /restexample2/file/uploadFileModelAttribute/new HTTP/1.1

Host: localhost:8080

Connection: keep-alive

Content-Length: 24211

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

Origin: http://localhost:8080

Content-Type: multipart/form-data; boundary=----WebKitFormBoundarya1HOiexOytPpWx8U

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 Edg/85.0.564.63

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Sec-Fetch-Site: same-origin

Sec-Fetch-Mode: navigate

Sec-Fetch-User: ?1

Sec-Fetch-Dest: document

Referer: http://localhost:8080/restexample2/fileUploadView

Accept-Encoding: gzip, deflate, br

Accept-Language: ko,en;q=0.9,en-US;q=0.8,ja;q=0.7,de;q=0.6

Cookie: JSESSIONID=C1BB03C6629263A6B77084AA15CDF947

         }


파일명: sample-keyword.txt


[첨부(Attachments)]

sample-keyword.txt

sample-keyword.zip




* 맺음글(Conclusion)


실전에서 REST를 쉽고 빠르게 적용할 수 있는 방법이 없는지 충분히 고민하였다.

시중 책도 정말 많이 사보고, 인터넷 정보 검색, 공식 메뉴얼 등 삽질을 많이 하였다.


그리하여 이 프로젝트가 작성된 것이다.

REST를 쉽고 빠르게 사용할 수 있었으면 하는 바람이다.



* 참고자료(References)


[RESTEasy - 프로젝트에 대한 것]

1. A Guide to RESTEasy, https://www.baeldung.com/resteasy-tutorial, Accessed by 2020-09-28, Last Modified 2020-02-12.

2. tutorials/resteasy at master · eugenp/tutorials · GitHub, https://github.com/eugenp/tutorials/tree/master/resteasy, Accessed by 2020-09-28, Last Modified .

3. jersey, resteasy(JBoss)

[비고] 

-> resteasy는 현재 xml 출력을 못함. (POST, GET 등은 동작함)

-> jersey는 최신 버전도 인식을 못해버림.


[RestController, RESTful Service - Spring Framework ]

4. Building a RESTful Web Service, https://spring.io/guides/gs/rest-service/, Accessed by 2020-09-28, Last Modified .

[참고할 때 메모]

-> Library없이 가능한지 확인할 것 

[비고]

-> Gradle 기반과 Spring Boot로 작성되었는데, Gradle 생략하고 Spring Framework 작업은 동일해서 많은 도움을 받았다.


5. RestController에 대해 알아보자, https://milkye.tistory.com/283, Accessed by 2020-09-28, Last Modified 2018-12-02.

[비고]

매우 간단하게 RestController를 사용하는 방법에 대해서 소개하고 있음.


6. [Spring Error] No converter found for return value of type: class java.util.ArrayList, https://keichee.tistory.com/274, Accessed by 2020-09-28, Last Modified 2016-11-27.

[비고]: jackson-bind의 적용 방법에 대해서 소개하고 있음. 클래스 변환에 대한 오류 해결은 미약함.


[다중 업로드]

7. Spring MVC - 유연한 다중 파일업로드 핸들러 만들기 (Multipart upload), https://galid1.tistory.com/684, Accessed by 2020-09-28, Last Modified 2020-01-29.


8. 스프링 파일 업로드 처리, https://advenoh.tistory.com/26, Accessed by 2020-09-28, Last Modified 2019-01-01.



[RESTful - 게시판 설계]

9. 4. springboot restful 방식으로 게시판 변경, https://linked2ev.github.io/gitlog/2019/12/28/springboot-restful-4-rest-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD/ , Accessed by 2020-09-28, Last Modified 2019-12-28.

[비고]: REST 기반의 게시판 설계 아이디어를 많이 얻었음.


[RESTTemplate 관련]

10. Spring REST API 생성, 호출, https://www.leafcats.com/173, Accessed by 2020-09-28, Last Modified 2017.


11. 스프링 (Spring) RestTemplate으로 POST 파라미터 (Parameter) 전송해보기, https://soshanstory.tistory.com/entry/스프링-Spring-RestTemplate으로-POST-파라미터-Parameter-전송해보기, Accessed by 2020-09-28, Last Modified 2014-08-31.

[비고]: Rest Template 사용방법을 매우 간단하게 잘 적어놨음.


[공부하면서 메모]

- XML, Java 설정 방식이 있다. (REST 셋팅 관련 - 범위가 방대해지므로 줄임)


12. RestTemplate (4가지 - 자바 내에서 클라이언트 작업), https://howtodoinjava.com/spring-boot2/resttemplate/spring-restful-client-resttemplate-example/, Accessed by 2020-09-28, Last Modified 2015-03.

[비고]: 추천하고 싶음. 매우 RestTemplate에 대해서 가장 깔끔하게 잘 정리하였음.


(공식적으로는 HTML에서는 POST, GET만 지원함.)


13. RestTemplate list 반환하기, https://luvstudy.tistory.com/52, Accessed by 2020-09-28, Last Modified 2018-11-16.


[REST 한글 깨짐 Response]

14. spring에서 json response 한글 깨짐, https://thswave.github.io/spring/2015/02/22/korean-json-response.html, Accessed by 2020-09-28, Last Modified 2015-02-22.


[POST, GET 이외의 문제]

15. Using PUT method in HTML form, https://stackoverflow.com/questions/8054165/using-put-method-in-html-form, Accessed by 2020-09-28, Last Modified 2012.

[비고]: 8년 전 문제이긴 하지만, 현재에도 해당되는 문제이다.


16. REST - HTML Form에서 GET/POST만 지원하는 이유, http://haah.kr/2017/05/23/rest-http-method-in-html-form/, Accessed by 2020-09-28, Last Modified 2017-05-23.

[비고]: 이론적으로 REST에 대해서 아주 잘 소개하고 있는 사이트이다.


17. HTML 양식에 PUT 및 DELETE 메소드가없는 이유는 무엇입니까?, https://qastack.kr/software/114156/why-are-there-are-no-put-and-delete-methods-on-html-forms, Accessed by 2020-09-28, Last Modified .

[비고]: 토론 형태로 문제에 대해서 의견을 공유하고 있다.


18. [REST] PUT, PATCH, DELETE 미지원 처리, https://velog.io/@ette9844/REST-PUT-PATCH-DELETE-%EB%AF%B8%EC%A7%80%EC%9B%90-%EC%B2%98%EB%A6%AC, Accessed by 2020-09-28, Last Modified 2020-05-19.

[비고]: 이 코드는 동작하지 않음. (이론적으로 동작되는 방법이라고 보는 게 타당함.)


[번외의 주제: Node.js - REST]

19. REST API 예제, https://hyun-am-coding.tistory.com/entry/REST-API-%EC%98%88%EC%A0%9C, Accessed by 2020-09-28, Last Modified 2019-11-17.

[비고]: REST Client와 Server 흐름에 대해서 살펴볼 수 있다. 물론 자바 코드에 직접 도움은 되는 건 아니지만 동일하게 구성될 수 있다는 아이디어를 제공해준다.



반응형
728x90
300x250

[Spring-Framework] 18. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (3)


이전 글을 보지 않았다면, 꼭 진행해보고 오길 권장한다.


[이전 글]

1. [Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (1)
https://yyman.tistory.com/1422


2. [Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (2)

https://yyman.tistory.com/1423




22. View - home.jsp


사용자 인터페이스는 아래의 그림처럼 표현하였다.



그림 26. 로그인 전 - "/" 페이지



그림 27. 로그인 후 - "/" 페이지



그림 28. 로그인 후 - 권한별 기능 표시, 계정 정보 출력



그림 29. 로그인 후 - 권한별 기능 표시, 계정 정보 출력



경로: /src/main/webapp/WEB-INF/views/home.jsp



<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ page session="false" %>

<html>

<head>

<title>Spring-Security 5 (Java 방식)</title>

<meta charset="UTF-8">

<style>

body{

font-family:'Arial';

font-size:12px;

}


a{

text-decoration:none;

color:#666;

}

</style>

</head>

<body>

<h1>

Hello world!(Spring-Security 5(Java 방식)) - DB연동(Oracle)

</h1>

<hr />

<sec:authorize access="isAnonymous()">

<!-- 로그인 전 -->

<p>

<a href="<c:url value="/member/loginForm" />">로그인</a>

</p>

</sec:authorize> 

<sec:authorize access="isAuthenticated()">

<!-- 로그인 성공 -->

<form:form action="${pageContext.request.contextPath}/logout" method="POST">

<input type="submit" value="로그아웃" />

</form:form>

<p>

<!-- 방법1. Sec 적용(이름 출력) -->

방법(sec 태그)1: <sec:authentication property="name" />

</p>

<p>

<!-- 방법2. c태그, Controller에서 가져오기 -->

방법(Model 정의)2: ${username}

</p>

</sec:authorize> 


<h3>

<!-- 관리자 권한을 가진 경우만 보이기 -->

<sec:authorize access="hasRole('ROLE_ADMIN')" >

<a href="<c:url value="/admin/home" />">관리자 홈</a>&nbsp;&nbsp;

</sec:authorize>

<a href="<c:url value="/encode-password?password=pass" />">비밀번호</a>

</h3>


<!-- 비밀번호 생성기 -->

<c:set var="gene_pwd" value="${encode}" />

<c:if test="${gene_pwd != null}">

    <c:out value="${gene_pwd}" />

</c:if>


</body>

</html>


파일명: home.jsp


[첨부(Attachments)]

home.zip





23. View - admin/home.jsp


관리자 페이지에 관한 것이다.



그림 30. 관리자 페이지


<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<%@ page session="false" %>

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" language="java" %>

<html>

<head>

<meta charset="UTF-8">

<title>Admin - Page(관리자 - 페이지)</title>

<meta charset="UTF-8">

</head>

<body>

<h1>

Hello world!

</h1>


<P>

<h3>[<a href="<c:url value="/" />">홈으로(Home)</a>]</h3>

</P>

</body>

</html>



파일명: home.jsp


[첨부(Attachments)]

home.zip




24. View - member/loginForm.jsp


로그인 폼에 대한 사용자 인터페이스이다.



그림 31. 로그인 폼 - 로그인 전



그림 32. 로그인 폼 - 로그인 시도(아이디 또는 비밀번호 틀렸을 때)


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="UTF-8">

    <title>로그인 - 페이지(Login - Page)</title>

    <style>

    body{

    font-family:'Arial';

    font-size:12px;

    }

    a{

    text-decoration:none;

    color:#666;

    }

    </style>

</head>

<body>


<h1>아이디와 비밀번호를 입력해주세요.</h1>

<hr />


<c:url value="/member/loginForm" var="loginUrl" />

<form:form name="f" action="${loginUrl}" method="POST">

    <p>

        <label for="username">아이디</label>

        <input type="text" id="id" name="id" />

    </p>

    <p>

        <label for="password">비밀번호</label>

        <input type="password" id="password" name="password"/>

    </p>

    <p>

        <label for="remember-me">Remember-me(로그인 상태 유지)</label>

        <input type="checkbox" id="remember-me" name="remember-me"/>

    </p>

    

    <%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> --%>

    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

    <button type="submit" class="btn">로그인</button>

    

    <!-- 에러 메시지 영역 -->

    <c:if test="${param.error != null}">

        <p>아이디와 비밀번호가 잘못되었습니다.</p>

    </c:if>

    <c:if test="${param.logout != null}">

        <p>로그아웃 하였습니다.</p>

    </c:if>


</form:form>

<h3>[<a href="<c:url value="/" />">홈으로(Home)</a>]</h3>


</body>

</html>


파일명: loginForm.jsp


[첨부(Attachments)]

loginForm.zip




25. View - member/accessDenied.jsp


접근 제한 페이지에 관한 소스이다.



그림 33. 접근 제한된 페이지


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE html>

<html lang="ko">

<head>

    <meta charset="UTF-8">

    <title>Access Denied</title>

</head>

<body>

<h1>Access Denied!</h1>

<h3>[<a href="<c:url value="/" />">홈</a>]</h3>

</body>

</html>


파일명: accessDenied.jsp


[첨부(Attachments)]

accessDenied.zip



26. 맺음글(Conclusion)


Spring Framework 5.4, Spring Security 5.4, Oracle 19g를 활용하여 보안 영역에 대해서 자세하게 살펴보았다.



* 참고 자료(References)


1. Hello Spring Security Java Config, https://docs.spring.io/spring-security/site/docs/5.0.16.RELEASE/guides/html5/helloworld-javaconfig.html#setting-up-the-sample, Accessed by 2020-09-27, Last Modified 2020-05-06.


추천(30점): 공식 사이트에서 제공하는 메뉴얼인데, 제작 흐름을 파악할 수 있다.


2. Tomcat ->Multiple Contexts have a path of 에러, https://kkangdda.tistory.com/12, Accessed by 2020-09-27, Last Modified 2019-11-06.


3. Spring Security 5 – Java Config, https://howtodoinjava.com/spring5/security/security-java-config-enablewebsecurity-example/, Accessed by 2020-09-27, Last Modified 2020-06-19.


추천(50점): June 19, 2020

- 1. @EnableWebSecurity is not found in any of the 3 jars. 

= 2. "spring-security-config-5.0.7.RELEASE", "spring-security-core-5.0.7.RELEASE", "spring-security-web-5.0.7.RELEASE", 

     - June 19, 2020, Spring version is 5.2.5


4. Spring 4.0 + Java Config - web.xml 없애기..., https://tiveloper.tistory.com/entry/Spring-40-Java-Config-webxml-없애기, 2014-10-24

추천(20점): 조금 코드가 바뀐 부분이 있다. 

public class WebInitializer implements WebApplicationInitializer { }
= 이 부분 클래스가 동작하지 않음. (변동 사항이 있었음.)
   - 2013년 12월 정도에 Spring 4.0이 출시되었으니깐 오래되었다고 봐도 됨.


5.Spring Security : Web MVC + Security - Custom Login Form 만들기, https://kogle.tistory.com/78?category=870263, Accessed by 2020-09-27, Last Modified 2020-05-16.


추천(25점): 커스텀 로그인 페이지 java 버전에 대해서 간결하게 잘 작성되어 있음. (그러나 태스트를 해본 바로는 완벽하게 동작하진 않음.)


6. Spring Security Authentication Provider, https://www.baeldung.com/spring-security-authentication-provider, Accessed by 2020-09-27, Last Modified 2020-08-19.


추천(40점): 국내 자료에는 "CustomAuthenticationProvider()"에 대해서 자세히 잘 적어놓은 글을 찾기 힘들었음.

- shouldAuthenticateAgainstThirdPartySystem()가 동작되지는 않았지만, 많은 도움이 되었음.


6.Spring Security - 인증 절차 인터페이스 구현 (1) UserDetailsService, UserDetails, https://to-dy.tistory.com/86?category=720806, Accessed by 2020-09-27, Last Modified 2018.


추천(25점): 인증 절차(쉽게 소개하면, "DB처리에 대한 방법과 과정")에 대해서 잘 작성하였음.

- iBatis(MyBatis)로 작성되어 있긴 한데, 구현 원리를 살펴볼 수 있었음.


7.How to refer brcypt encoder to customized authentication provider?, https://stackoverflow.com/questions/35900053/how-to-refer-brcypt-encoder-to-customized-authentication-provider, Accessed by 2020-09-27, Last Modified 2016.


추천(13점): customized authentication provider에 대해서 구현 원리와 암호 처리 등에 대해서 소개하고 있다.

이 글에서 알 수 있었던 중요한 부분은 Spring Security 5.4로 구현하면서 암호가 랜덤으로 처리되는 것을 확인하였는데, 암호 확인하는 방법을 찾던 도중에 Bcrypt 함수 내에 확인하는 함수가 있는 것을 알게 되었다.


8. [Spring/Security] 초보자가 이해하는 Spring Security, https://postitforhooney.tistory.com/entry/SpringSecurity-%EC%B4%88%EB%B3%B4%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-Spring-Security-%ED%8D%BC%EC%98%B4, Accessed by 2020-09-27, Last Modified 2017-03-31.


9. [SPRING SECURITY] 4.스프링 시큐리티 로그인 커스터마이징, https://debugdaldal.tistory.com/89, Accessed by 2020-09-27, Last Modified 2016.

추천(15점): 자바 기반으로 커스터마이징 구현하는 방법에 대해서 소개되어 있다.


10. Spring Security Custom AuthenticationProvider example, https://javaengine.tistory.com/entry/Spring-Security-Custom-AuthenticationProvider-example, Accessed by 2020-09-27, Last Modified 2017-02-06.

추천(20점): Spring Security with Java 프로젝트 환경설정만 잘 되어 있다면, 시도해봐도 괜찮은 글이다.


11. Spring Security Remember Me, https://www.baeldung.com/spring-security-remember-me, Accessed 2020-09-27, Last Modified 2020-08-15.

추천(15점): 로그인 유지 및 자동 로그인에 관한 자바 버전으로 구현하는 방법에 대해서 소개하고 있다.
쿠키 제거하고 몇 가지 간단한 사용방법만 소개하고 있다.


12. 6 Spring Security - Access Denied Handler(error page 처리), https://jungeunlee95.github.io/java/2019/07/18/6-Spring-Security-Access-Denied-Handler(errorpage-%EC%B2%98%EB%A6%AC)/, Accessed by 2020-09-27, Last Modified 2019-7-18.

-> 비고: 접근 제어 페이지에 대해서 소개하고 있음.


13. 3 Spring Security - Authorization(권한) 설정(ROLE), TagLib authorize 추가, https://jungeunlee95.github.io/java/2019/07/18/3-SpringSecurity-Authorization(%EA%B6%8C%ED%95%9C)-%EC%84%A4%EC%A0%95(ROLE),-TagLib-authorize-%EC%B6%94%EA%B0%80/, Accessed by 2020-09-27, Last Modified 2020-07-18.

-> 비고: 권한별 페이지 꾸리는 방법에 대해서 소개하고 있음.

반응형
728x90
300x250

[Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (2)


이번에는 이전 글에 이어서 Spring-Security를 구현해보려고 한다.

조금 이번 글부터는 난이도가 있어지니깐 개발 전략을 잘 숙지해서 작업하면 좋겠다.


1. [Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (1), 2020-09-27

https://yyman.tistory.com/1422



14. 개발 전략


하나 만들면 다 되는 게 아니다. 계속 연속해서 복합적으로 수정작업을 해줘야 한다.

그래서 개발 작업이 힘이 든다. 쉽지만 않다.


그림 24. 개발 전략도


SecurityWebApplicationInitializer.java, SecurityConfig.java는 Spring Security 구현에 있어서 핵심이라고 해도 무방하다.

두 개를 잘 구현한다면, 셈플 로그인 페이지는 볼 수 있다.


문제는 자바 버전으로 구현했을 때 보안 토큰 절차가 xml방식에 비해서 매우 까다롭게 반응한다는 것이다.

그래서 간단한 코드로 태스트를 해보기도 전에 DB 설계를 할 수 밖에 없었다.


이유는 토큰 인증 때문에 그렇다.


SqlMapSessionFactory도 계속 반복해서 다양한 영역에서 재사용될 것이다.



15. config의 SecurityWebApplicationInitializer.java (필수 파일)


이 파일을 보면, 제일 황당한 생각이 들 수 밖에 없는 이유가 코드는 몇 줄 안 되는데, 없으면 동작이 안 된다는 것이다.


package com.springMVC.javaSecurity5.config;


import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;


public class SecurityWebApplicationInitializer 

                        extends AbstractSecurityWebApplicationInitializer {


}


파일명: SecurityWebApplicationInitializer.java


[첨부(Attachments)]

SecurityWebApplicationInitializer.zip



16. config의 SecurityConfig.java (필수 파일)


"SecurityConfig.java" 이 파일도 없으면 Spring Security with 자바 버전이 동작되지 않는다.


패키지 경로: com.springMVC.javaSecurity5.config


package com.springMVC.javaSecurity5.config;


import javax.sql.DataSource;


import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

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.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;

import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import org.springframework.security.web.csrf.CsrfFilter;

import org.springframework.web.filter.CharacterEncodingFilter;


import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;

 

@Configuration

@EnableWebSecurity

public class SecurityConfig extends WebSecurityConfigurerAdapter {

 

    @Autowired

    PasswordEncoder passwordEncoder;

    

    @Autowired

    private CustomAuthenticationProvider authProvider;

    

    protected void configure(AuthenticationManagerBuilder auth, HttpSecurity http) throws Exception {

      

    CharacterEncodingFilter filter = new CharacterEncodingFilter();


    /* UTF-8 한글 보완 */

        filter.setEncoding("UTF-8");

        filter.setForceEncoding(true);

        http.addFilterBefore(filter,CsrfFilter.class);

   

        auth

        .authenticationProvider(authProvider);

        

    /* 현재 - 임시

        auth.inMemoryAuthentication()

        .passwordEncoder(passwordEncoder)

        .withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")

        .and()

        .withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");

        

         */

        

        // withUser("admin").password(passwordEncoder.encode("1234")).roles("USER", "ADMIN");

      

        /* 임시

   

    UserBuilder users = User.withDefaultPasswordEncoder();

      auth.inMemoryAuthentication()

        .withUser(users.username("admin").password("1234").roles("USER"))

        .withUser(users.username("user").password("1234").roles("ADMIN"));

    */

    }

 

 

    @Override

    protected void configure(HttpSecurity http) throws Exception {


        http.authorizeRequests()

        

        // index

        .antMatchers("/")

            .permitAll()


         // 접근 오류

     .antMatchers("/member/accessDenied")

         .permitAll()

         

         .antMatchers("/member/accessDeniedView")

         .permitAll()    


         // 회원 로그인 기능

     .antMatchers("/member/loginForm")

         .permitAll()

         

     // 관리자 페이지 기능

        .antMatchers("/admin/**")

        .hasRole("ADMIN")

        // "RULE_ADMIN이라고 DB에 입력되어 있다면, RULE_은 제거하고 입력해야 인식함."

        

        // 폼 로그인 명세

        .and()

            .formLogin()

        .permitAll()

                .loginPage("/member/loginForm")

                .failureForwardUrl("/member/loginForm?error")

                .defaultSuccessUrl("/")

                .usernameParameter("id")

                .passwordParameter("password")

       

        // 로그아웃 처리

.and()

                .logout()

                .logoutUrl("/logout")

                .logoutSuccessUrl("/")

                .invalidateHttpSession(true)

                .deleteCookies("JSESSION_ID")

                .deleteCookies("remember-me")

        // 로그인 

            .and()

            .rememberMe()

            .tokenValiditySeconds(604800)

            .tokenRepository(persistentTokenRepository())

            // 예외처리(

            .and()

        .exceptionHandling()

        .accessDeniedPage("/member/accessDenied")

        // csrf 설정

            .and()

                .csrf().disable();

       

        

            

        

    /*

        http.authorizeRequests()

        .antMatchers("/login")

            .permitAll()

        .antMatchers("/**")

            .hasAnyRole("ADMIN", "USER")

            .hasAnyAuthority("RULE_ADMIN", "RULE_USER")

        .and()

            .formLogin()

            .loginPage("/login")

            .defaultSuccessUrl("/")

            .failureUrl("/login?error=true")

            .permitAll()

            

            .loginPage("/login")

            .usernameParameter("email")

            .passwordParameter("password")

            .successHandler(successHandler())      

            .failureHandler(failureHandler())

            .permitAll();

            

        .and()

            .logout()

            .logoutSuccessUrl("/login?logout=true")

            .invalidateHttpSession(true)

            .permitAll()

        .and()

            .csrf()

            .disable();

    */

        

    }

    

    // 로그아웃 Persistent_Logins에 관한 설정   (주석 해도 무방)...

    @Bean 

public PersistentTokenRepository persistentTokenRepository() {

JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();

DataSource usrDS = getDataSource();

db.setDataSource(usrDS);

return db;

}

    


    // DataSource 불러오기    (주석 해도 무방)

@Bean

public DataSource getDataSource() {

       // BasicDataSource dataSource = new BasicDataSource(); - Apache DBCP2

SqlMapSessionFactory factory = SqlMapSessionFactory.getInstance();

       return factory.getOracleDataSource(); // 오라클 적용함.

}

    

// 비밀번호 생성 - 암호(BCryptPasswordEncoder)

    @Bean

    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();

        

    }

    

}


파일명: SecurityConfig.java


[첨부(Attachments)]

SecurityConfig.zip



비고: 주석 잘 쳐서 정리해서 빌드해보면, 내장 로그인 페이지를 볼 수 있다.
       - SecurityWebApplicationInitializer.java, SecurityConfig.java 두 개 파일의 힘이 얼마나 큰지 실감 해볼 수 있다.

         Spring-Framework 설정 전체를 제어해버린다고 해도 된다.




17. 로그인 인증 - Spring Security (CustomAuthenticationProvider.java)


이 코드 부분은 찾아보려고 해도 쉽게 나오지 않는다. 어려운 부분 중 하나이다.

공개하는 이유는 삽질을 적게 하라는 의미이다.


패키지 경로: com.springMVC.javaSecurity5.config


package com.springMVC.javaSecurity5.config;


import java.util.List;


import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.stereotype.Component;


import com.springMVC.javaSecurity5.service.CustomUserDetailsService;


@Component

public class CustomAuthenticationProvider implements AuthenticationProvider {

    

    private UserDetailsService userDeSer;

 

    @Override

    public Authentication authenticate(Authentication authentication) {

        

        String username = (String) authentication.getPrincipal();

        String password = (String) authentication.getCredentials();

        

        if ( username.equals("fail")) {

        System.out.println("(에러)아이디: 실패");

        return null;

        }

        

        // DB 정보 읽기

        userDeSer = new CustomUserDetailsService();

        UserDetails userDetail = userDeSer.loadUserByUsername(username);

        

        @SuppressWarnings("unchecked")

  List<GrantedAuthority> roles = (List<GrantedAuthority>) userDetail.getAuthorities();

        

        // 권한

        System.out.println("DB불러오기-권한:" + userDetail.getAuthorities());

        System.out.println("DB불러오기-비밀번호:" + userDetail.getPassword());

        System.out.println("roles:" + roles.get(0));

        

        if ( !matchPassword(password, userDetail.getPassword())) {

        System.out.println("(에러)비밀번호: 불일치" + password);

        return null;

        }

        

        UsernamePasswordAuthenticationToken result =

        new UsernamePasswordAuthenticationToken(username, password, roles);

        

        result.setDetails(userDetail);

        

        return result;

    }

 

    @Override

    public boolean supports(Class<?> authentication) {

        return true;

    }

    

    private boolean matchPassword(String loginPwd, String password) {

   

        BCryptPasswordEncoder secure = new BCryptPasswordEncoder();

        return secure.matches(loginPwd, password);

    }

 

}


파일명: CustomAuthenicationProvider.java


[첨부(Attachments)]

CustomAuthenticationProvider.zip


어디에 구체적으로 사용되는 부분인가?


사용하는 영역은 SecurityConfig.java에 http의 auth의 .authenicationProvider()에 사용된다.



그림 25. SecurityConfig.java


왜 이 코드를 사용하는지 소개해본다면,

"No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken"

이 문제가 디버그 오류창에 뜨는 것을 볼 수 있다.


기본 내장형 계정 생성 등으로 인증을 시도하면 xml방식에서는 처리해줬는데, java방식에서는 인증받지 못한다.


     /* 현재 - 임시


        auth.inMemoryAuthentication()

        .passwordEncoder(passwordEncoder)

        .withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")

        .and()

        .withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");

        

        */


[문제가 발생되는 기본형 - 코드]


이 코드로 작업하면, xml에서는 동작되었던 부분이 동작되질 않는다.


토큰 인증도 해결할 겸 "CustomAuthenicationProvider.java를 설계해서 문제를 해결한 것이다.




18. SQL (Factory) - SqlMapSessionFactory.java


나중에 CP(Connection Pool, 커넥션 풀)이라고 불리는 것으로 구현해봐도 좋을 듯 싶다.

iBatis를 남용해서 프로젝트에 기본마냥 소개하는 책들이 무척 많은데 기본은 순수한 DB를 사용하는 것부터 출발하는 것이다.


iBatis는 SQL 코드 개발 등에서 생산성이 좋아지는 도구 중 하나이지만, 필수 사항은 아니라고 본다.

차라리 필수 사항을 꼽아본다면, 커넥션 풀을 하나 추천해보고 싶다.


아무튼 커넥션 풀 주제가 아니기 때문에 생략한다.


패키지 경로: com.springMVC.javaSecurity5.db


package com.springMVC.javaSecurity5.db;


import java.io.IOException;

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.Properties;


import javax.sql.DataSource;


import oracle.jdbc.pool.OracleDataSource;


public class SqlMapSessionFactory {

private static SqlMapSessionFactory factory = new SqlMapSessionFactory();


private SqlMapSessionFactory() {}


private final String driverName = "oracle.jdbc.driver.OracleDriver";

private final String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";

private final String userName = "{사용자계정명}";

private final String userPassword = "{비밀번호}";

public static SqlMapSessionFactory getInstance() {

return factory;

}


/*

*     public static DataSource getMySQLDataSource() {

        Properties props = new Properties();

        FileInputStream fis = null;

        MysqlDataSource mysqlDS = null;

        try {

            fis = new FileInputStream("db.properties");

            props.load(fis);

            mysqlDS = new MysqlDataSource();

            mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));

            mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));

            mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));

        } catch (IOException e) {

            e.printStackTrace();

        }

        return mysqlDS;

    }

    */

    public DataSource getOracleDataSource(){

        

    OracleDataSource oracleDS = null;

        

    try {

            oracleDS = new OracleDataSource();

            oracleDS.setURL(dbUrl);

            oracleDS.setUser(userName);

            oracleDS.setPassword(userPassword);

        } catch (SQLException e) {

            e.printStackTrace();

        }

        return oracleDS;

        

    }

public Connection connect() {


Connection conn = null;


try {

Class.forName(driverName);

conn = DriverManager.getConnection(dbUrl, userName, userPassword);

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


return conn;


}


public void close(Connection conn, PreparedStatement ps, ResultSet rs) {


if ( rs != null ) {


try {

rs.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}


close(conn, ps); // Recursive 구조 응용(재귀 함수)


} // end of if


}


public void close(Connection conn, PreparedStatement ps) {


if (ps != null ) {

try {

ps.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


if (conn != null ) {

try {

conn.close();

}

catch(Exception ex) {

System.out.println("오류 발생: " + ex);

}

} // end of if


}

}



파일명: SqlMapSessionFactory.java


[첨부(Attachments)]

SqlMapSessionFactory.zip



비고: 재사용이 가능한 형태로 설계하였다.



19. Model - CustomUserDetails.java


순수한 Model 형태는 아니고, 부분 개량하였다.

Spring-Security에서 제공하는 UserDetails(인터페이스)를 Model 클래스에 구현해야 한다.

List<role> 기능 문제 등으로 인해서 String authorities를 List<GrantedAuthority>로 변경하였다.


@Override 된 부분들이 UserDetails에 정의된 내용이다.


패키지 경로: com.springMVC.javaSecurity5.model


package com.springMVC.javaSecurity5.model;


import java.util.ArrayList;

import java.util.Collection;

import java.util.List;


import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;


public class CustomUserDetails implements UserDetails{


private static final long serialVersionUID = 1L;

private String username;

    private String password;

    // 개량함. (다중 권한 고려)

    private List<GrantedAuthority> authorities;

    private boolean enabled;

        

public String getUsername() {

return username;

}


public void setUsername(String username) {

this.username = username;

}


public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

     public void setAuthority(String authority) {

// 권한 객체 생성

if ( authorities == null ) {

authorities = new ArrayList<GrantedAuthority>();

}

// 권한 추가

SimpleGrantedAuthority grantObj = new SimpleGrantedAuthority(authority);

authorities.add(grantObj);

     }


public boolean getEnabled() {

return enabled;

}

public void setEnabled(boolean enabled) {

this.enabled = enabled;

}


@Override

public Collection<? extends GrantedAuthority> getAuthorities() {


        return authorities;


}


@Override

public boolean isAccountNonExpired() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isAccountNonLocked() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isCredentialsNonExpired() {

// TODO Auto-generated method stub

return false;

}


@Override

public boolean isEnabled() {

// TODO Auto-generated method stub

return false;

}

    

}


파일명: CustomUserDetails.java


[첨부(Attachments)]

CustomUserDetails.zip





20. Service - CustomUserDetailsService.java


CustomUserDetailsService는 Spring-Security의 "UserDetailsService(인터페이스)"로 정의된 내용을 구현하는 것이다.

인터페이스의 영향도 있지만, DB를 실제로 불러올 때 사용자 관점에서 처리되는 부분이라고 본다.


package com.springMVC.javaSecurity5.service;


import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;


import com.springMVC.javaSecurity5.dao.SqlSessionTemplate;

import com.springMVC.javaSecurity5.model.CustomUserDetails;


public class CustomUserDetailsService implements UserDetailsService {

    

    private SqlSessionTemplate sqlSession = SqlSessionTemplate.getInstance();

 

    public CustomUserDetails getUserById(String username) {

        return sqlSession.selectOne("user.selectUserById", username);

    }

 

    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


    CustomUserDetails user = sqlSession.selectOne("null", username);

        

        if(user==null) {

            throw new UsernameNotFoundException(username);

        }

        return user;

    }

    

}


파일명: CustomUserDetailsService.java


[첨부(Attachments)]

CustomUserDetailsService.zip




21. DAO - SqlSessionTemplate.java


실제 DB를 구현하는 부분이다.


package com.springMVC.javaSecurity5.dao;


import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;


import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;

import com.springMVC.javaSecurity5.model.CustomUserDetails;


public class SqlSessionTemplate {


private SqlSessionTemplate() {}

private static SqlSessionTemplate sqlTemplate;

    private static SqlMapSessionFactory session; 

    

    public static SqlSessionTemplate getInstance(){

   

        if(sqlTemplate == null){

        sqlTemplate = new SqlSessionTemplate();

            session = SqlMapSessionFactory.getInstance();

        }


        return sqlTemplate;

    }

    

// 추후 iBatis 고려

public CustomUserDetails selectOne(String id, String username) {

Connection conn = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

   

    CustomUserDetails node = null;


    String sql = "select g1.username, g1.password, g2.authority, " + 

       "g1.enabled from comp_users g1, comp_authorities g2 where g1.username = g2.username " +

       "and g1.username = ?";


    System.out.println(sql);


    try {


    conn = session.connect();


    pstmt = conn.prepareStatement(sql);

    pstmt.setString(1, username);

   

    rs = pstmt.executeQuery();


    while ( rs.next() ) {

   

    // 데이터가 존재할 때, 노드 생성

    node = new CustomUserDetails();

    node.setUsername(rs.getNString(1));

    node.setPassword(rs.getNString(2));

    node.setAuthority(rs.getNString(3));

    System.out.println("rs:" + rs.getNString(3));

    node.setEnabled(rs.getBoolean(4));

    }


    }catch(Exception ex) {

    System.out.println("오류 발생: " + ex);

    }

    finally {

    session.close(conn, pstmt, rs);

    }


    return node;

}

}



파일명: SqlSessionTemplate.java


[첨부(Attachments)]

SqlSessionTemplate.zip



* 3부에서는 View에 대해서 구현하는 방법을 소개하겠다.


3부에서는 "jsp 파일" 등 사용자 인터페이스 화면에 대해서 소개하겠다.


1. [Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (3), 2020-09-27

https://yyman.tistory.com/1424


반응형
728x90
300x250

[Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (1)


이번에 소개할 프로젝트는 13, 14, 15번 게시글을 Java 방식으로 구현한 프로젝트이다.

동일하게 진행할 것이다. 실질적으로 제대로 소개하고 있는 책이나 게시글은 찾아보질 못했다.

많은 오류가 있었다는 이야기이다.


순수한 Spring-Framework에서도 Spring Security를 원만히 잘 사용할 수 있도록 하는 게 글의 핵심이다.

이번 게시글도 양이 많아서 나눠서 작성하였으니 잘 따라해보면 도움이 될 것으로 보인다.



[작업환경]


[WAS(Web Application Server), 웹 애플리케이션 서버]

1. apache-tomcat-9.0.37-windows-x64


[DBMS(DataBase Management System - Tools]

2. sqldeveloper-19.2.1.247.2212-x64


[DB(DataBase)]

3. Oracle Databases 19.3.0.0


[IDE(Integration Development Environment)]

4. Spring-Tool Suites 4-4.7.2. Releases.


[Framework(프레임워크)]

5. spring-security-taglibs(5.4)

6. spring-security-config(5.4)

7. spring-security-web(5.4)

8. spring-security-core(5.4)

9. javax.servlet-api(4.0.1)

10. spring-webmvc(5.2.9.RELEASE)

11. spring-context(5.2.9.RELEASE)

12. Maven 3.6.3/1.16.0.20200610-1735


[Java]

OpenJDK-14.0.2 (https://openjdk.java.net/)



[이전 글 주제]

1. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (1)

https://yyman.tistory.com/1419

2. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (2)

https://yyman.tistory.com/1420

3. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (3)  // 자동 로그인 유지, 유지 세션

https://yyman.tistory.com/1421




1. XML 기반 프로젝트 vs 자바 기반 프로젝트


이번에 소개할 주제는 자바 기반 프로젝트이다.

이전 게시물과 지금 구현할 게시물의 차이점에 대해서 간단하게 소개하겠다.


 


그림 1. XML 방식


 


그림 2. 자바 방식(Java)




(특징)
- Spring 폴더가 있으며, xml 파일이 존재함.

- web.xml 파일이 있음.

- 기본 프로젝트 pom.xml을 별도로 설정하지 않아도 됨.

(특징)
- Spring 폴더가 없음. (있으면 충돌나서 실행이 안 됨.)

- web.xml 파일이 없음. 

- 기본 프로젝트 생성 후 pom.xml을 별도로 설정해야 함.

(구현 난이도) 무난함.

(셋팅 값 및 지정된 변수 등을 잘 사용하는 것이 매우 중요함)


- 적당한 지식이면 무난할 것으로 보임.

(구현 난이도) 머리를 조금 사용해야 함.
- 초기 구축을 놓고 보면,
  코드가 늘어나고 조금 복잡할 수 있음.
  (쉽지만 않다. API도 읽어내야 하고 많은 실험이 요구됨.)


- 많은 프로그래밍 지식이 다소 필요함.

사용해보면, 장/단점이 있음.

-> 간단하게 관리하는 면에서는 편할 수도 있음.

   (xml)

-> 튜닝에 있어서 제약이 있을 수도 있음.

......


[이전 - XML 기반 게시글]

1. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (1), 2020. 9. 26

https://yyman.tistory.com/1419


2. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (2), 2020. 9. 26

https://yyman.tistory.com/1420


3. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (3), 2020. 9. 26

https://yyman.tistory.com/1421



2. 결과(1) - (Result)


출력 결과는 이전 프로젝트(13, 14, 15번)과 동일하게 구현 하였다.



그림 3. 결과(자바 버전) - 메인



그림 4. 결과(자바 버전) - 로그인 폼


그림 5. 결과(자바 버전) - 로그인 실패



그림 6. 결과(자바 버전) - 로그인 후



그림 7. 결과(자바 버전) - 비밀번호 출력의 예(암호화 패키지) - 업데이트 전



그림 8. 결과(자바 버전) - 로그인 후 모습 - 업데이트 전




그림 9. 결과(자바 버전) - 권한이 없는 계정이 특정 페이지를 접속할 때 모습(1) // 순정 (업데이트 전)



그림 9-1. 결과(자바 버전) - 권한이 없는 계정이 특정 페이지를 접속할 때 모습(2) // 커스텀 (업데이트 완료함)



그림 9-2. 결과(자바 버전) - 권한이 있는 계정의 메뉴 모습 // 커스텀 (업데이트 함)



그림 9-3. 결과(자바 버전) - 권한이 없는 계정의 메뉴 모습 // 커스텀 (업데이트 함)






3. 결과(2) - 프로젝트 구성


프로젝트 구성에 대한 것이다.

상당히 양이 많다. 초기 셋팅만 잘 해주면, 나머지는 순조로울 것으로 보인다.



그림 10, 11. 프로젝트 구성


다소 구현할 양이 많다는 것을 확인할 수 있다.


완성된 프로젝트 구성에 대해서 살펴보는 것은 무엇을 진행할 것인지 예상할 수 있는 방법 중 하나라고 주장해본다.



4. 데이터베이스 설계


크게 데이터베이스는 공식적으로 설계하라고 제시하는 규격 테이블 1개와 사용자 정의 테이블 5개로 총 6개의 테이블을 작성해야 한다.
아마 이런 부분은 시중 책이나 강의 등에서는 다뤄보지 못한 부분일 수도 있다.
권한이 있는 계정을 그룹별 권한, 페이지별 권한으로 체계화시킨 설계도이다.

Spring Security에서 요구하는 기능을 기본적으로 담은 설계이니 활용하면 참고하면 도움이 될 것으로 보인다.



그림 12. ER-D


테이블을 작성하는 데, 순번을 대략적으로 잡아본 이유는 외래키, 참조키 등에 기준이 되는 테이블을 먼저 생성하고 작업하는 것이 좋기 때문이다.

편집해서 외래키, 참조키 등 지정해줘도 무방하다. 단, 무결성이나 각종 조건 등 때문에 데이터가 존재하지 않은 상태에서 진행해야 한다.



그림 13. 테이블: COMP_GROUP



그림 14. 테이블: COMP_USERS



그림 15. 테이블: COMP_GROUP_AUTHORITIES



그림 16. 테이블: COMP_GROUP_MEMBERS



그림 17. 테이블: COMP_AUTHORITIES




그림 18. 테이블: PERSISTENT_LOGINS




5. 테이블 - SQL(Create table - DML)


테이블 작성에 관한 사항이다.


CREATE TABLE comp_users (

username VARCHAR(50) NOT NULL,

password VARCHAR(300) NOT NULL,

enabled INT NOT NULL,

PRIMARY KEY (username)

);


CREATE TABLE comp_authorities (

  username VARCHAR(50) NOT NULL,

  authority VARCHAR(50) NOT NULL,

  CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES comp_users (username)

); 


CREATE TABLE comp_groups(

id VARCHAR2(20) NOT NULL,

group_name VARCHAR2(20) NULL

);


CREATE TABLE comp_group_authorities(

group_id VARCHAR2(20) NOT NULL,

authority VARCHAR2(20) NOT NULL

);


CREATE TABLE comp_group_members(

group_id VARCHAR2(20) NOT NULL,

username VARCHAR2(20) NOT NULL

);


CREATE TABLE persistent_logins (

username VARCHAR(64) NOT NULL,

series VARCHAR(64) PRIMARY KEY,

token VARCHAR(64) NOT NULL,

last_used TIMESTAMP NOT NULL

);



-- 계정

INSERT INTO comp_users (username, password, enabled) VALUES ('user', '$2a$10$x04djNV2e9rpcPPRyXoLk.rMm6iZe2/vYdzpqHQcLeNSYdt7kc30O', 1);

INSERT INTO comp_users (username, password, enabled) VALUES ('admin', '$2a$10$QUddY3O/6ZgkYCR6MFlv9.nqA501Fm0cc/ZxQHX5pwb1o0CYCTiIS', 1);


-- 사용자 권한

INSERT INTO comp_authorities (username, authority) VALUES ('user', 'ROLE_ADMIN');

INSERT INTO comp_authorities (username, authority) VALUES ('admin', 'ROLE_USER');


-- 그룹

INSERT INTO comp_groups (id, group_name) VALUES ('G01', '관리자 그룹');

INSERT INTO comp_groups (id, group_name) VALUES ('G02', '사용자 그룹');


-- 그룹 권한

INSERT INTO comp_group_authorities (group_id, authority) VALUES ('G01', 'ROLE_ADMIN');

INSERT INTO comp_group_authorities (group_id, authority) VALUES ('G01', 'ROLE_USER');

INSERT INTO comp_group_authorities (group_id, authority) VALUES ('G02', 'ROLE_USER');


-- 그룹 회원

INSERT INTO comp_group_members (group_id, username) VALUES ('G01', 'user');

INSERT INTO comp_group_members (group_id, username) VALUES ('G02', 'admin');



파일명: sampleDb-oracledb.sql


[첨부(Attachments)]

sampleDb-oracledb.zip



비고: 꼭 오라클만 되는 것은 아니고, 살짝 튜닝하면 MySQL 등에서도 사용할 수 있는 형태로 작성하였다.

(SQL 명령어가 일부 데이터베이스에서는 차이가 있을 수도 있음. 특정 DB의 함수 등)



6. 프로젝트 - 신규 생성하기


Spring-Legacy-Project를 생성한다.



그림 19. 테이블: Spring MVC Project 생성하기


Spring MVC Project를 선택한다.

Project Name의 항목을 입력한다.

Next를 누른다.




그림 20. 테이블: Spring MVC Project 생성하기


top-level-package를 입력한 후 Finish를 누른다.




7. 프로젝트 - pom.xml 수정작업


양이 많긴 하지만, 작성해본다.


<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<groupId>com.springMVC</groupId>

<artifactId>javaSecurity5</artifactId>

<name>SpringSecurity5-Java</name>

<packaging>war</packaging>

<version>1.0.0-BUILD-SNAPSHOT</version>

<properties>

<!-- web.xml 사용 안함 표기 -->

<failOnMissingWebXml>false</failOnMissingWebXml>

<java-version>14</java-version>

<org.springframework-version>5.2.9.RELEASE</org.springframework-version>

<org.aspectj-version>1.6.10</org.aspectj-version>

<org.slf4j-version>1.6.6</org.slf4j-version>

</properties>

<dependencies>

<!-- Spring -->

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-context</artifactId>

<version>${org.springframework-version}</version>

<exclusions>

<!-- Exclude Commons Logging in favor of SLF4j -->

<exclusion>

<groupId>commons-logging</groupId>

<artifactId>commons-logging</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.springframework</groupId>

<artifactId>spring-webmvc</artifactId>

<version>${org.springframework-version}</version>

</dependency>

<!-- AspectJ -->

<dependency>

<groupId>org.aspectj</groupId>

<artifactId>aspectjrt</artifactId>

<version>${org.aspectj-version}</version>

</dependency>

<!-- Logging -->

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-api</artifactId>

<version>${org.slf4j-version}</version>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>jcl-over-slf4j</artifactId>

<version>${org.slf4j-version}</version>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>org.slf4j</groupId>

<artifactId>slf4j-log4j12</artifactId>

<version>${org.slf4j-version}</version>

<scope>runtime</scope>

</dependency>

<dependency>

<groupId>log4j</groupId>

<artifactId>log4j</artifactId>

<version>1.2.15</version>

<exclusions>

<exclusion>

<groupId>javax.mail</groupId>

<artifactId>mail</artifactId>

</exclusion>

<exclusion>

<groupId>javax.jms</groupId>

<artifactId>jms</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jdmk</groupId>

<artifactId>jmxtools</artifactId>

</exclusion>

<exclusion>

<groupId>com.sun.jmx</groupId>

<artifactId>jmxri</artifactId>

</exclusion>

</exclusions>

<scope>runtime</scope>

</dependency>


<!-- @Inject -->

<dependency>

<groupId>javax.inject</groupId>

<artifactId>javax.inject</artifactId>

<version>1</version>

</dependency>

<!-- Servlet -->

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->

<dependency>

    <groupId>javax.servlet</groupId>

    <artifactId>javax.servlet-api</artifactId>

    <version>4.0.1</version>

    <scope>provided</scope>

</dependency>

<dependency>

<groupId>javax.servlet.jsp</groupId>

<artifactId>jsp-api</artifactId>

<version>2.1</version>

<scope>provided</scope>

</dependency>

<dependency>

<groupId>javax.servlet</groupId>

<artifactId>jstl</artifactId>

<version>1.2</version>

</dependency>

<!-- Test -->

<dependency>

<groupId>junit</groupId>

<artifactId>junit</artifactId>

<version>4.7</version>

<scope>test</scope>

</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-core</artifactId>

    <version>5.4.0</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-web</artifactId>

    <version>5.4.0</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-config</artifactId>

    <version>5.4.0</version>

</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->

<dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-taglibs</artifactId>

    <version>5.4.0</version>

</dependency>

<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->

<dependency>

    <groupId>javax.validation</groupId>

    <artifactId>validation-api</artifactId>

    <version>2.0.1.Final</version>

</dependency>


<dependency>

<groupId>com.oracle.database.jdbc</groupId>

<artifactId>ojdbc8</artifactId>

<version>19.7.0.0</version>

</dependency>

</dependencies>

    <build>

        <plugins>

            <plugin>

                <artifactId>maven-eclipse-plugin</artifactId>

                <version>2.9</version>

                <configuration>

                    <additionalProjectnatures>

                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>

                    </additionalProjectnatures>

                    <additionalBuildcommands>

                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>

                    </additionalBuildcommands>

                    <downloadSources>true</downloadSources>

                    <downloadJavadocs>true</downloadJavadocs>

                </configuration>

            </plugin>

            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-compiler-plugin</artifactId>

                <version>2.5.1</version>

                <configuration>

                    <source>1.6</source>

                    <target>1.6</target>

                    <compilerArgument>-Xlint:all</compilerArgument>

                    <showWarnings>true</showWarnings>

                    <showDeprecation>true</showDeprecation>

                </configuration>

            </plugin>

            <plugin>

                <groupId>org.codehaus.mojo</groupId>

                <artifactId>exec-maven-plugin</artifactId>

                <version>1.2.1</version>

                <configuration>

                    <mainClass>org.test.int1.Main</mainClass>

                </configuration>

            </plugin>

        </plugins>

    </build>

</project>




- oracle jdbc를 못 찾는 사람들을 위해서, oracle jdbc라고 검색하거나 ojdbc8.jar 파일을 구해서 사용하면 된다.
- Oracle Databases를 설치해서 사용중인 경우에는 아래처럼 하면 사용할 수 있다.


그림 21. pom.xml 파일 선택 후 마우스 오른쪽 버튼 메뉴 모습


pom.xml을 선택한다.

마우스 오른쪽 버튼을 누른다.

maven-> Add Dependency를 클릭한다.



그림 22. Add Dependency에서 Oracle 검색하기


oracle을 검색한다.
com.oracle.database.jdbc  | ojdbc8을 선택한 후 OK를 누른다.






8. 프로젝트 - web.xml, spring 폴더 제거 작업


파일: /src/main/web-app/WEB-INF/web.xml   (삭제할 것)

폴더: /src/main/web-app/WEB-INF/spring      (삭제할 것)




9. 프로젝트 - Build Path, Project factes 버전 바꾸기


초기 Spring MVC Project를 생성하면 1.6버전으로 설정되어 있다. 14버전으로 바꿔주겠다.



그림 23. Properties 속성 바꿔주기(1) / Build-Path - JavaSE 14로


JRE System Library [JavaSE-14]로 Edit 버튼을 통해서 수정해준다.



그림 23. Properties 속성 바꿔주기(2) / Project Factes - JavaSE 14로


Java 버전을 1.6에서 14로 바꿔준다.




10. Controller - web.xml 제거 (핵심작업)


web.xml 제거한 Spring MVC라는 주제로 접근하여 작성하려고 한다.

지금 작업, pom.xml 수정 작업을 중심으로 HomeController.java(Servlet) 파일을 구성해서 view파일 jsp를 만들면 web.xml없이 동작하는 화면을 

볼 수 있다.


package com.springMVC.javaSecurity5.config;


import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;


public class SpringMvcAnnotation extends AbstractAnnotationConfigDispatcherServletInitializer {


  @Override

  protected Class<?>[] getRootConfigClasses() {

    return null;

  }


// bean 설정과 spring container 설정을 위한 Config 클래스를 등록한다.

// Config 클래스는 web.xml의 dispatcher servlet 초기화에 사용된 xml과 같은 기능을 한다.

  @Override

  protected Class<?>[] getServletConfigClasses() {

    return new Class[] { WebConfig.class };

  }


// web.xml의 servlet mapping 부분을 대체한다.

  @Override

  protected String[] getServletMappings() {

    return new String[] { "/" };

  }


}


파일명: SpringMvcAnnotation.java


[첨부(Attachments)]

SpringMvcAnnotation.zip




이 파일을 시작점으로 한다.


package com.springMVC.javaSecurity5.config;


import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import org.springframework.web.servlet.view.InternalResourceViewResolver;

 

@EnableWebMvc // <mvc:annotation-driven>에 해당.

// @ComponentScan(basePackages = {"com.figo.web"})  // <context:component-scan base-package="”com.figo.web”/">에 해당됨.

@ComponentScan("com.springMVC.javaSecurity5")

@Configuration

public class WebConfig extends WebMvcConfigurerAdapter {

 

    // <resources mapping="/resources/**" location="/resources/">에 해당됨.

    @Override

    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/").setCachePeriod(31556926);

    }

 

    // <mvc:default-servlet-handler>에 해당됨.

    @Override

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {

        configurer.enable();

    }   

     

    // web.xml에서 봤던 내용들임.

    @Bean

    public InternalResourceViewResolver getInternalResourceViewResolver() {

        InternalResourceViewResolver resolver = new InternalResourceViewResolver();

        resolver.setPrefix("/WEB-INF/views/");

        resolver.setSuffix(".jsp");

        return resolver;

    }

    

    /*

    @Override

    public void addViewControllers(ViewControllerRegistry registry) {

   

        // registry.addViewController("/web").setViewName("home");

        registry.addViewController("/").setViewName("home");

        

    }

    */

 

}


파일명: WebConfig.java


[첨부(Attachments)]

WebConfig.zip



참고: HomeController의 초기내용으로 두고 서버 상에서 실행시켜봤다면, 동작했을 것으로 보인다.
(10번까지 잘 따라왔으면 화면 출력을 볼 수 있음.)



11. Controller - HomeController.java


HomeController에 관한 내용이다. 자세히 보면, "13, 14, 15번"글하고 거의 동일하다는 것을 알 수 있다.

쉽게 이야기하면, web.xml 파일부터 xml로 구성된 환경설정 파일을 프로젝트 내에서 제거한 것이다.

그러니 Controller 등은 동일할 수 밖에 없다고 본다.



package com.springMVC.javaSecurity5.controller;


import java.security.Principal;

import java.text.DateFormat;

import java.util.Date;

import java.util.Locale;


import javax.servlet.http.HttpServletRequest;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;


//@CrossOrigin(origins = "*", allowedHeaders = "*")

@Controller

public class HomeController {

private static final Logger logger = LoggerFactory.getLogger(HomeController.class);


@RequestMapping("/")

public String home(Locale locale, Model model, Principal principal) {


logger.info("Welcome home! The client locale is {}.", locale);


Date date = new Date();

DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

String username = null;

// Principal 예제

if (principal != null) {

username = principal.getName();

System.out.println("타입정보 : " + principal.getClass());

System.out.println("ID정보 : " + principal.getName());

}

model.addAttribute("username", username);

model.addAttribute("serverTime", formattedDate );


return "home";

}

@RequestMapping("/admin")

public String admin(Locale locale, Model model) {

return "admin";

}


@RequestMapping(value = "/encode-password", method = RequestMethod.GET)

public String passwordEncode(Locale locale, Model model, HttpServletRequest req) {


// 1. xml 방식에서 java 방식으로 전환

// web.xml 파일 제거로 인한 사용 불가(ServletConfig sc 연결됨)

// WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(req.getServletContext());

        // PasswordEncoder passwordEncoder = context.getBean(PasswordEncoder.class);


PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        String password = req.getParameter("password");

        String encode = passwordEncoder.encode(password);


        model.addAttribute("encode", encode);

        System.out.println(encode);

        

        return "home";

        

}


}


파일명: HomeController.java


[첨부(Attachments)]

HomeController.zip





12. Controller - AdminController.java


관리자 페이지에 관한 내용이다.


package com.springMVC.javaSecurity5.controller;

import java.util.Locale;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;


@Controller

public class AdminController {


private static final Logger logger = LoggerFactory.getLogger(AdminController.class);


@RequestMapping(value = "/admin/home", method = RequestMethod.GET)

public String home(Locale locale, Model model) {

logger.info("Welcome - 관리자 페이지(Admin Home)!");


return "admin/home";

}


}



파일명: AdminController.java


[첨부(Attachments)]

AdminController.zip



13. Controller - MemberController.java


MemberController.java에 관한 내용이다.


package com.springMVC.javaSecurity5.controller;


import java.util.Locale;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;


@Controller

public class MemberController {


// 커스텀 페이지 - 양식만

private static final Logger logger = LoggerFactory.getLogger(MemberController.class);


@RequestMapping(value = "/member/loginForm")

public String loginForm(Locale locale, Model model) {

logger.info("안녕 - 로그인 폼(Hello - Login Form");

// model.addAttribute("serverTime", formattedDate );

return "member/loginForm";

}


@RequestMapping(value = "/member/accessDenied")

public String accessDenied(Locale locale, Model model) {

logger.info("접근 금지 - 이동(Accessed Denied)");

// model.addAttribute("serverTime", formattedDate );

return "redirect:/member/accessDeniedView";

}

@RequestMapping(value = "/member/accessDeniedView")

public String accessDeniedView(Locale locale, Model model) {

logger.info("접근 금지 - 출력(Accessed Denied)");

// model.addAttribute("serverTime", formattedDate );

return "member/accessDenied";


}

}



파일명: MemberController.java


[첨부(Attachments)]

MemberController.zip




* 2부에서는


2부에서는 Spring-Security 설정에 대해서 집중적으로 다뤄보도록 하겠다.

조금 어려울 수도 있으니 마음을 편안하게 하고 따라해보면 좋겠다.


1. [Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (2), 2020-09-27

https://yyman.tistory.com/1423


반응형
728x90
300x250

[Spring-Framework] 15. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (3)


이번에 소개할 내용은 1, 2부에 이어서 "자동 로그인"과 "로그아웃 시 쿠키 삭제"에 대해서 소개하겠다.


1. [Spring-Framework] 13. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (1), 2020-09-26

https://yyman.tistory.com/1419


2. [Spring-Framework] 14. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (2), 2020-09-26

https://yyman.tistory.com/1420



1. 결과(Result)

로그인 상태 유지에 관한 기술이다.

이 기능은 쉽게 말하면, "로그인 기억하기", "로그인 상태 유지"를 체크하면 일정 시간 동안 다시 로그인을 하지 않아도 되는 기능을 말한다.




그림 1. 로그인 상태 유지 - 기술 페이지




그림 2. 로그인 상태 유지 - 기술 페이지 (로그인 후)



그림 3. 로그인 상태 유지 - 데이터베이스



2. 데이터베이스 - 영구 토큰(Persistent Token)


설계를 따로 해야 하는 건 아니고, 스프링 시큐리티의 공식 문서에 정의가 되어 있는 부분이다.


https://docs.spring.io/spring-security/site/docs/current/reference/html5/#remember-me-persistent-token



그림 4. Spring-Security References 사이트에 정의된 Persistent Login(Remember-Me) 스키마


자동 로그인에 대한 스키마가 기술되어 있다. 



그림 5. Spring-Security References 사이트에 기술된 Persistent Token Approach


사용 방법에 대해서 기술되어 있다.



3. 데이터베이스 - SQL 코드


CREATE TABLE persistent_logins (

username VARCHAR(64) NOT NULL,

series VARCHAR(64) PRIMARY KEY,

token VARCHAR(64) NOT NULL,

last_used TIMESTAMP NOT NULL

);


이 코드를 DBMS로 질의하면 기본적인 준비는 끝난다고 보면 된다.



그림 6. Oracle SQL Developer - 질의 창



그림 7. PERSISTENT_LOGINS 테이블



그림 8. PERSISTENT_LOGINS 테이블 ER-D




3. security-content.xml - 수정


경로: /src/main/webapp/WEB-INF/spring/security-context.xml


아래의 그림처럼 수정해주면 된다.



그림 9. 쿠키 제거 기능 주석 부분과 자동 로그인 주석 부분 - 수정 및 추가 할 것


<!-- 쿠키 제거 기능 개선 -->

<logout logout-url="/logout" logout-success-url="/"

invalidate-session="true" delete-cookies="remember-me,JSESSION_ID" />


<!-- 자동 로그인(2020-09-26 // 추가 작업 -->

<remember-me data-source-ref="dataSource" token-validity-seconds="604800" />


dataSource의 정의에 대해서 다시 한번 기술하도록 하겠다.

건강한 비판을 해보면, 일부 시중 책을 보면, 전혀 DataSource 내용은 누락해서 나온 책들도 꽤 있다.

삽질하는 시간이 줄어들고 잠도 푹 자는 개발자가 되길 희망한다.


(중략)

<!-- DataSource 추후 지원 -->

<!-- 1. HSQLDB -->

<!-- ClassDriver = org.hsqldb.jdbcDriver -->

<!-- Url = jdbc:hsqldb:hsql://localhost:9001 -->

<!-- 2. Oracle JDBC -->

<!-- ClassDriver = oracle.jdbc.driver.OracleDriver -->

<!-- Url = jdbc:oracle:thin:@127.0.0.1:1521:orcl -->

<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<beans:property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />

<beans:property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl" />

<beans:property name="username" value="{사용자계정명}" />

<beans:property name="password" value="{비밀번호}" />

</beans:bean>


(중략)


파일명: security-context.xml


[첨부(Attachments)]

security-context-modify.zip




4. View = src/main/webapp/WEB-INF/views/member/loginForm.jsp


아래처럼 화면을 설계해주면 된다.



그림 10. 로그인 상태 유지 - 기술 페이지(User-Interface)


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="UTF-8">

    <title>로그인 - 페이지(Login - Page)</title>

    <style>

    body{

    font-family:'Arial';

    font-size:12px;

    }

   

    a{

    text-decoration:none;

    color:#666;

    }

   

    </style>

</head>


<body>


<h1>아이디와 비밀번호를 입력해주세요.</h1>

<hr />


<c:url value="/login" var="loginUrl" />


<form:form name="f" action="${loginUrl}" method="POST">

    <p>

        <label for="username">아이디</label>

        <input type="text" id="id" name="id" />

    </p>

    <p>

        <label for="password">비밀번호</label>

        <input type="password" id="password" name="password"/>

    </p>

    <p>

        <label for="remember-me">Remember-me(로그인 상태 유지)</label>

        <input type="checkbox" id="remember-me" name="remember-me"/>

    </p>

    <%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> --%>

    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

    <button type="submit" class="btn">로그인</button>

    

    <!-- 에러 메시지 영역 -->

    <c:if test="${param.error != null}">

        <p>아이디와 비밀번호가 잘못되었습니다.</p>

    </c:if>

    <c:if test="${param.logout != null}">

        <p>로그아웃 하였습니다.</p>

    </c:if>

    

</form:form>

<h3>[<a href="<c:url value="/" />">홈으로(Home)</a>]</h3>


</body>

</html>



파일명: loginForm.jsp


[첨부(Attachments)]

loginForm-modify.zip



* 맺음글(Conclusion)


간단하게 "자동-로그인(remember-me)" 기능에 대해서 Spring-Framework 5.x, Spring-Security 5.4를 기반으로 살펴보았다.


다음 글은 "자동 로그인 또는 로그인 상태 유지"라는 주제로 글을 작성하였으니 참고하면 좋겠다.


1. [Spring-Framework] 15. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-XML) (3), 2020-09-26

https://yyman.tistory.com/1422


반응형

+ Recent posts