no image
[MyBatis] Mapper XML 내 ResultMap 사용법
RestAPI를 통해 MyBatis로 테이블 셀렉 작업을 하는데 결과 값이 모두 Null값이어서 당황했다. 대체 왜이런건지 컨트롤러, 서비스, 매퍼 xml 등 살펴보다가 Select 쿼리 결과를 담는 DTO의 변수명들과 해당 테이블들의 칼럼명이 일치하지 않기 때문이라는 것을 알게되었다. @Getter@ToStringpublic class FileResponse { private int seq; private int boardSeq; private String originalName; private String saveName; private long size; private String deleteYn; private Timestamp createdDate; private Timestamp deleted..
2024.06.29
no image
[Spring Boot] MyBatis SQL 쿼리 로그 설정
Spring Boot 내에서 연동한 MyBatis의 쿼리 실행 로그를 볼 수 있도록 설정해보자. 1. Dependency 추가 org.bgee.log4jdbc-log4j2 log4jdbc-log4j2-jdbc4.1 1.16Log4jdbc Dependency를 추가한다. 2. application.properties DB 설정 변경spring.datasource.driver-class-name=org.postgresql.Driverspring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgresspring.datasource.username=postgresspring.datasource.password=1234기존의 applicati..
2024.06.29
[Spring Boot] Thymeleaf HTML 템플릿 수정 시 브라우저 반영 설정
application.properties에 다음과 같이 설정한다.spring.thymeleaf.cache=falseThymeleaf 템플릿 파일은 캐싱이 적용된 파일을 우선하여 브라우저에 적용이 되기 때문에 HTML 내 수정사항을 바로 브라우저 내에서 확인할 수 없다. 브라우저에서 캐시 비우기 및 강력 새로고침을 해도 어림도 없다.이를 위해서는 캐싱을 비활성화 시켜 수정된 HTML 파일을 바로 브라우저에 반영되도록 한다.
2024.06.26
[Spring Boot] MyBatis + PostgreSQL 연동
* 스프링부트에서 MyBatis를 사용하기 위해 다음 dependency를 추가한다. org.postgresql postgresql org.mybatis.spring.boot mybatis-spring-boot-starter 1.3.2  1. 데이터베이스 연결 정보 등록spring.datasource.driver-class-name=org.postgresql.Driverspring.datasource.url = jdbc:postgresql://localhost:5432/postgresspring.datasource.username = postgresspring.datasource.password = 1234다음과 같이 application.properties에 내가 연결하고 싶은 DB 정보 변수를 등록한..
2024.06.25
[Spring Boot] 스프링 부트 JWT 인증 방식 구현
스프링 부트에서는 공식적으로 JWT 인증 방식을 스프링 시큐리티와 함께 구현하는 것을 권장하고 있다. 스프링 시큐리티란?스프링 시큐리티는 스프링 프레임워크 기반으로 제작한 프로젝트의 인증과 권한 기능을 담당하는 프레임워크다. 어플리케이션의 로그인, 회원가입 등의 기능을 자체적으로 처리하는 기능이 내장되어 있기 때문에이런 식으로 스프링 시큐리티 없이 로그인 로직을 직접 작성하였다면,if (param.get("USER_PW").toString().equals(member.get("USER_PW").toString())) { system.out.println("로그인 성공!"); return "main"; }  스프링 시큐리티 사용 시 이렇게 한줄 코드로 로그인 처리를 할 수 있다.Member member..
2024.03.04
no image
JWT(JSON Web Tokens)에 대해서 알아보자
JWT는 Json Web Token의 약자로 일반적으로 클라이언트와 서버 사이에서 통신할 때 권한을 위해 사용하는 토큰이다. 토큰을 발급 받고 매 통신마다 토큰 인증에 성공해야만 JSON 통신이 가능함. 1. JWT의 구조 JWT의 구조는 Header, Payload Signature 세 가지로 구성되어 있다. 각각의 제외한 JSON 부분들은 하나의 데이터로 표현될 때 Base64로 인코딩되어 표현된다. Header { "alg": "HS256", "typ": "JWT" } alg ⇒ JWT 구조에서 Signature에는 Header와 Payload가 합쳐져 특정 알고리즘으로 암호화가 진행이 된 다음에 만들어지는데, 그 때 사용되는 알고리즘이다. "alg": "HS256" 는 Signature 부분을 H..
2024.02.22
[OpenLayers] WFS API 호출 with Vworld
브이월드에서 제공하는 API를 이용하여 WFS 방식의 지도 레이어를 그려보자 먼저, 알아야할 몇가지들을 보자 WFS 란? - 지도를 호출하는 방식 중에, 지도서버 내에서 지도와 관련된 데이터 자체를 호출하는 방식의 프로토콜이다. - 호출된 해당 데이터 중에 피처(Feature) 라고 하는 공간 및 지리정보 데이터를 이용하여 지도 위에 다양한 기능을 추가 할 수 있다. 1. API 신청 - https://www.vworld.co.kr 브이월드 홈페이지에서 API 신청을 할 수 있다. 2. WFS를 호출하기 위한 GET 방식 URL 파라미터 설정 - 지도에 추가할 특정 기능을 위해 본인이 원하는 레이어 종류가 있을 것이다. 그런것들을 포함하여 피처 데이터 값을 받는 방식, 피처의 최대 갯수 등의 여러 부가정..
2023.10.31
no image
[OpenLayers] WMS API 호출 with Vworld
브이월드에서 제공하는 API를 이용하여 WMS 방식의 지도 레이어를 그려보자 먼저, 알아야할 몇가지들을 보자 WMS 란? - 지도를 호출하는 방식 중에, 지도서버 내에서 지도와 관련된 데이터를 이용하여 생성된 이미지를 호출하는 방식의 프로토콜이다. - 보통, 타일 방식으로 생성된 기본 지도 위에 특별한 기능을 추가하기 위해 선을 그리거나, 지도에서 건물을 감싸는 레이어를 호출하는 목적으로 사용 된다. 1. API 신청 - https://www.vworld.co.kr 브이월드 홈페이지에서 API 신청을 할 수 있다. 2. WMS를 호출하기 위한 파라미터 설정 - 지도에 추가할 특정 기능을 위해 본인이 원하는 레이어 종류가 있을 것이다. 그런것들을 포함하여 해당 지도 정보 값을 받는 방식, 이미지의 크기 등..
2023.10.23
728x90

RestAPI를 통해 MyBatis로 테이블 셀렉 작업을 하는데 결과 값이 모두 Null값이어서 당황했다.

Null이 풍년

 

대체 왜이런건지 컨트롤러, 서비스, 매퍼 xml 등 살펴보다가 

Select 쿼리 결과를 담는 DTO의 변수명들과 해당 테이블들의 칼럼명이 일치하지 않기 때문이라는 것을 알게되었다.

 

@Getter
@ToString
public class FileResponse {
	private int seq;
	private int boardSeq;
	private String originalName;
	private String saveName;
	private long size;
	private String deleteYn;
	private Timestamp createdDate;
	private Timestamp deletedDate;
}

DTO의 변수명과 테이블 칼럼명이 다르다.

 

이렇게 서로 다를 경우에는 내가 만든 DTO에 테이블 조회 결과값들을 못 담는다.

 

그래서 Mapper XML 내에 따로 작업을 해줘야 하는데 그 방법이 바로 ResultMap 태그를 이용하는 것이다.

 

 

1. Mapper.xml 내 ResultMap 설정

	<resultMap type="com.example.file.dto.FileResponse" id="fileResponse">
		<result property="seq" column="SEQ"/>
		<result property="boardSeq" column="BOARD_SEQ"/>
		<result property="originalName" column="ORIGINAL_NAME"/>
		<result property="saveName" column="SAVE_NAME"/>
		<result property="size" column="SIZE"/>
		<result property="deleteYn" column="DELETE_YN"/>
		<result property="createdDate" column="CREATED_DATE"/>
		<result property="deletedDate" column="DELETED_DATE"/>
	</resultMap>
  • type="com.example.file.dto.FileResponse":  쿼리 조회 결과를 담을 DTO 클래스 경로를 입력한다.
  • id="fileResponse": 해당 ResultMap 태그의 독자적인 식별 아이디를 입력한다.
  • property="DTO 변수명": 칼럼과 연결할 DTO의 변수명을 입력한다.
  • column="칼럼명": DTO 변수와 연결할 칼럼명을 입력한다.

 

2. Mapper.xml 내 Select 태그 수정

	<select id="getFilesByBoardSeq" resultMap="fileResponse">
		SELECT 
			SEQ,
			<include refid="fileColumns"></include>
		FROM
			BOARD_FILE
		WHERE
			DELETE_YN = 'N'
			AND BOARD_SEQ = #{boardSeq}
	</select>
  • resultMap="fileResponse": 앞서 정의한 resultMap 태그의 id 값을 입력한다.

 

※ 결과

728x90
728x90

Spring Boot 내에서 연동한 MyBatis의 쿼리 실행 로그를 볼 수 있도록 설정해보자.

 

1. Dependency 추가

<dependency>
    <groupId>org.bgee.log4jdbc-log4j2</groupId>
    <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
    <version>1.16</version>
</dependency>
  • Log4jdbc Dependency를 추가한다.

 

2. application.properties DB 설정 변경

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.jdbc-url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=1234
  • 기존의 application.properties 내에서 DB 설정 부분이 이렇게 되어 있었다면 수정할 부분은 딱 2가지가 된다.
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.jdbc-url=jdbc:log4jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=1234
  • spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy - 드라이버 클래스 이름 변경
  • spring.datasource.jdbc-url=jdbc:log4jdbc:postgresql://localhost:5432/postgres - url 주소에 'log4jdbc' 삽입

 

3. log4jdbc.log4j2.properties 생성

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
  • resources 폴더 바로 밑에 생성해준다.
  • 다음 설정을 입력해둔다.

 

4. logback.xml 생성 및 설정

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="STDOUT"
		class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{yyyyMMdd HH:mm:ss.SSS} [%thread] %-3level %logger{5} - %msg %n</pattern>
		</encoder>
	</appender>
	<logger name="jdbc" level="OFF" />
	<logger name="jdbc.sqlonly" level="OFF" />
	<logger name="jdbc.sqltiming" level="DEBUG" />
	<logger name="jdbc.audit" level="OFF" />
	<logger name="jdbc.resultset" level="OFF" />
	<logger name="jdbc.resultsettable" level="DEBUG" />
	<logger name="jdbc.connection" level="OFF" />
	<root level="INFO">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>
  • 쿼리 로그를 어떻게 보여줄지에 대해 직접적으로 설정하는 파일이다.
  • 이것도 마찬가지로 resources 폴더 바로 밑에 생성한다.
  • 가장 중요한 3가지 설정을 보자면,
  • jdbc.sqlonly: 실행된 SQL 문만을 로그로 출력할지 여부
  • jdbc.sqltiming: 실행된 SQL문과 함께 해당 쿼리를 실행하는 데 걸린 시간을 로그로 출력할지 여부 
  • jdbc.resultsettable: SELECT 쿼리 결과인 ResultSet의 내용을 테이블 형식으로 로그에 출력할지 여부

 

※ 결과

728x90
728x90

application.properties에 다음과 같이 설정한다.

spring.thymeleaf.cache=false
  • Thymeleaf 템플릿 파일은 캐싱이 적용된 파일을 우선하여 브라우저에 적용이 되기 때문에 HTML 내 수정사항을 바로 브라우저 내에서 확인할 수 없다. 브라우저에서 캐시 비우기 및 강력 새로고침을 해도 어림도 없다.
  • 이를 위해서는 캐싱을 비활성화 시켜 수정된 HTML 파일을 바로 브라우저에 반영되도록 한다.

 

 

 

728x90
728x90

* 스프링부트에서 MyBatis를 사용하기 위해 다음 dependency를 추가한다.

<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
</dependency>
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>1.3.2</version>
</dependency>

 

 

1. 데이터베이스 연결 정보 등록

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url = jdbc:postgresql://localhost:5432/postgres
spring.datasource.username = postgres
spring.datasource.password = 1234
  • 다음과 같이 application.properties에 내가 연결하고 싶은 DB 정보 변수를 등록한다.

 

2. DataSource 빈 설정

@Configuration
public class DataSourceConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")
	@Bean(name="dataSource")
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();   
	}

}
  • MyBatis의 SqlSession을 생성할 때 데이터베이스 정보 객체인 DataSource가 필요하다.
  • DataSource는 application.properties에 등록한 DB 정보를 가지고 생성한다.
  • @ConfigurationProperties(prefix = "spring.datasource") 어노테이션을 특정 객체에 붙이면 application.properties 에 있는 "spirng.datasource" 로 시작하는 모든 변수의 값을 해당 객체에 바인딩 시킨다는 뜻이다.

 

3. MyBatis Mapper 설정

@Configuration
@MapperScan(basePackages="*")
public class MyBatisConfig {

	@Bean
	public SqlSessionFactoryBean sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws IOException {
		PathMatchingResourcePatternResolver pmrpr = new PathMatchingResourcePatternResolver();
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(dataSource);
		sqlSessionFactoryBean.setConfigLocation(pmrpr.getResource("classpath:/config/sqlmap/sql-mapper-config.xml"));
		sqlSessionFactoryBean.setMapperLocations(pmrpr.getResources("classpath:/config/sqlmap/mappers/*.xml"));
		return sqlSessionFactoryBean;
	}

	@Bean
	public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}

}
  • MyBatis의 xml 문서 내 쿼리를 Mapper 인터페이스 내 정해둔 메소드로 이용하기 위해서는 SqlSession 객체가 필요하다. 위에서 등록한 DataSource를 가지고 SqlSession 빈을 생성하고 등록하는 코드이다.
  • 등록된 SqlSession 빈이 쿼리를 모아놓은 xml 문서에서 원하는 쿼리를 호출하게 해주는 것이다.
  • @MapperScan(basePackages=""): Mapper 인터페이스로 인식될 경로를 지정한다.
  • sqlSessionFactoryBean.setConfigLocation(): MyBatis 부가 설정이 담긴 경로를 지정한다. 해당 파일에서 별칭 설정 등의 부가 설정이 가능하다.
  • sqlSessionFactoryBean.setMapperLocations(): 실행될 쿼리들을 모아놓은 xml 파일을 지정한다. 보통 최상위 폴더까지 가서 *.xml로 지정한다.
  • 이렇게 설정이 되어야 MyBatis를 사용할 수 있다.

 

4. MyBatis 부가 설정 파일 생성

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <typeAliases>
		<typeAlias alias="boardMapper" type="egovframework.example.board.service.BoardMapper"/>
    </typeAliases>
</configuration>
  • MyBatis 부가 설정을 해주는 파일이다. 파일 이름은 보통 sql-mapper-config.xml이다.
  • 쿼리를 모아놓은 xml 파일에서 사용할 별칭들을 설정할 수 있다. typeAliases를 통해 기다란 패키지에서 부터 클래스 까지의 경로를 변수 이름으로 설정한다.

 

5. Mapper XML 파일 생성

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="egovframework.example.board.service.BoardMapper">
	
	<insert id="registBoard" parameterType="Map">
		INSERT INTO BOARD(TITLE, WRITER, CONTENT, REG_DATE, COUNT)
		VALUES(#{title}, #{writer}, #{content}, NOW(), 0)
	</insert>


</mapper>
  • Mapper 인터페이스에서 호출할 쿼리들을 작성한 xml 문서다.
  • insert, select, update, delete 등 여러 쿼리를 등록하고 각 쿼리의 id 값은 Mapper 인터페이스 메소드 명과 일치시켜야 한다.
  • 각 쿼리의 parameterType과 resultType 속성 값이 있는데 파라미터를 받을 때의 데이터 타입과 쿼리의 결과가 출력될 때의 데이터 타입을 지정한다.

 

6. Mapper 인터페이스 생성

@Mapper("boardMapper")
public interface BoardMapper {
	
	void registBoard(Map<String, Object> board);
}
  • 컨트롤러 및 서비스 단에서 호출할 메소드를 정의한다. 호출하고 싶은 쿼리의 id 값과 일치하도록 메소드를 생성하면 된다.
728x90
728x90

스프링 부트에서는 공식적으로 JWT 인증 방식을 스프링 시큐리티와 함께 구현하는 것을 권장하고 있다.

 

스프링 시큐리티란?

스프링 시큐리티는 스프링 프레임워크 기반으로 제작한 프로젝트의 인증과 권한 기능을 담당하는 프레임워크다.

 

어플리케이션의 로그인, 회원가입 등의 기능을 자체적으로 처리하는 기능이 내장되어 있기 때문에

이런 식으로 스프링 시큐리티 없이 로그인 로직을 직접 작성하였다면,

if (param.get("USER_PW").toString().equals(member.get("USER_PW").toString())) {
		system.out.println("로그인 성공!");
		return "main";
	}

 

 

스프링 시큐리티 사용 시 이렇게 한줄 코드로 로그인 처리를 할 수 있다.

Member member = loginService.loadUserByUsername(param.get("USER_ID"));

템플릿 엔진을 사용한다면 로그인이 완료된 후에 어떤 뷰로 이동할지도 Config 파일에서 설정이 가능하다.

 

 

덧붙여 로그인한 사용자만 특정 API를 이용 할 수 있게

기존 스프링의 인터셉터를 직접 작성하였다면,

String requestUrl = request.getRequestURL().toString();

if(requestUrl.contains("/gis")) {

	if("ATH0003".equals(session.getAttribute("USER_AUTH"))){
		return true;
		
	} else {

		printwriter.print("<script>alert('접근 권한이 없습니다.');location.href='/replotting/main';</script>");
		printwriter.flush();
		printwriter.close();
    return false;
	}

 

 

이렇게 한 줄 코드로 인터셉터 기능을 구현할 수 있다.

request.requestMatchers("/gis").hasRole("ATH0003") // ATH0003 권한이 있는 사용자만 허용

 

 

이게 스프링 시큐리티의 매력이다.

일단 JWT를 중점으로 작업하기 위해 스프링 시큐리티를 최소화하여 구현해보자.

스프링에서 구현해야 하는 JWT의 동작 원리는 다음과 같다.

  1. 로그인 요청이 들어오면 로그인 성공 시 JWT 토큰을 클라이언트에게 발급
  2. 클라이언트가 토큰을 발급 받고 인증이 필요한 API에 접속 시 해당 토큰이 유효한지 검증

 

간단하게 위의 2가지 과정으로 인증이 이루어지는데 해당 과정을 구현하기 위해서 코드를 작성해야 할 파일이 3개가 있다.

  1. JWT 토큰을 생성해주는 토큰 생성 클래스 (JwtProvider.java)
  2. 인증이 필요한 API 접속 시 토큰을 검증하고 인증을 처리하는 필터 클래스 (JwtAuthenticationFilter.java)
  3. 해당 토큰 검증 필터를 적용 시킬 수 있게 설정하는 스프링 시큐리티 설정 클래스(WebSecurityConfig.java)

해당 정보를 바탕으로 구현해보자.

 

 

1. 라이브러리 추가

  implementation 'org.springframework.boot:spring-boot-starter-security'

	implementation 'io.jsonwebtoken:jjwt-api:0.11.5' 
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
  • 해당 라이브러리들을 설치한다.

 

 

2. application.properties 파일에 보안 키 설정

jwt.secret = 7b64445f014cec357c8b8e7e175320997c4f2ed468f5fe7cee1d9a5cf231f6bd
  • JWT 토큰을 생성하고 검증할 때 application.properties 안에 있는 시크릿 키를 인증하여 생성과 검증이 이루어진다.
  • 해당 키는 토큰 발행 및 검증을 하기 전에 서버 내의 1차적인 보안을 위한 키라고 생각하면 된다.
  • 해당 키는 base64 랜덤 값 32자리로 이루어져 있으며, 해당 키를 생성하기 위해서는 리눅스 터미널에서 openssl rand -hex 64 명령어를 입력한다. (Git Bash에서도 가능)

 

 

3. TokenInfoDTO 작성

@Builder
@Data
@Getter
@AllArgsConstructor
public class TokenInfo { // 클라이언트에 토큰을 보내기 위한 DTO
    private String grantType;  // 토큰 타입
    private String accessToken; // 접근 토큰
    private String refreshToken; // 접큰 토큰이 만료 될 시 재발급 받기 위한 토큰
}
  • 로그인에 성공 시 토큰을 발급한 후 클라이언트에게 보낼 토큰 정보 DTO다.
  • 변수 grantType 에는 보통 “Bearer”라는 단어가 들어가는데 이는 해당 토큰이 JWT 토큰이라는 것을 나타내는 식별자다.
  • JwtTokenProvider 클래스에서 토큰을 생성하고 해당 DTO를 만들 것이다.

 

 

4. JwtTokenProvider 작성

@Slf4j
@Component
public class JwtTokenProvider { // 토큰을 생성하고 검증한다.
    private final Key key;

    public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
        // application.properties에서 secret key를 가져온다.
        // 토큰 발행 및 검증을 하기 전에 서버 내의 1차적인 보안을 위한 키임.
        byte[] keyBytes = Base64.getDecoder().decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    public TokenInfo generateToken(Member member) { // 토큰을 생성한다.
        Calendar cal1 = Calendar.getInstance();
        cal1.add(Calendar.HOUR_OF_DAY, 2);
        Date accessTokenExpiration = new Date(cal1.getTimeInMillis()); // 현재부터 2시간 뒤의 날짜 및 시간

        Calendar cal2 = Calendar.getInstance();
        cal2.add(Calendar.DATE, 14);
        Date refreshTokenExpiration = new Date(cal2.getTimeInMillis()); // 현재부터 2주 뒤의 날짜 및 시간

        // Access Token 생성
        String accessToken = Jwts.builder()
                .setHeader(createHeader())
                .setClaims(createClaims(member, "access"))
                .setSubject(member.getId()) // 페이로드의 sub 설정, 이 토큰의 주인이 누구인지?
                .signWith(key, SignatureAlgorithm.HS256) // 서명 부분에 진행할 암호화 알고리즘 설정
                .setExpiration(accessTokenExpiration) // 엑세스 토큰의 만료 기한 설정
                .compact();

        String refreshToken = Jwts.builder()
                .setHeader(createHeader())
                .setClaims(createClaims(member, "refresh"))
                .setSubject(member.getId()) // 페이로드의 sub 설정, 이 토큰의 주인이 누구인지?
                .signWith(key, SignatureAlgorithm.HS256) // 서명 부분에 진행할 암호화 알고리즘 설정
                .setExpiration(refreshTokenExpiration) // 리프레쉬 토큰의 만료 기한 설정
                .compact();

        return TokenInfo.builder() // 위에서 생성한 토큰을 포함하여 토큰 정보 반환
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    public boolean validateToken(String token, HttpServletResponse response) throws IOException { // 토큰을 검증한다.
        try {
            Claims claims = parseClaims(token); // 토큰을 검증함과 동시에 복호화하여 클레임을 가져온다.

            return true;
        } catch (SecurityException | MalformedJwtException e) { // 위에서 검증함과 동시에 예외처리를 한다.
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.error("토큰의 기한이 만료되었습니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty", e);
        }
        return false;

    }

    private Claims parseClaims(String accessToken) {  // 토큰을 복호화하여 클레임 부분을 반환한다.
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(accessToken) // 실제 토큰 검증이 일어나는 부분
                .getBody();
    }

    private Map<String, Object> createHeader() { // 토큰의 헤더 부분을 만든다.
        Map<String, Object> header = new HashMap<>();
        header.put("typ", "JWT");
        header.put("alg", "HS256");
        return header;
    }

    private Map<String, Object> createClaims(Member member, String classification) { // 토큰의 클레임을 만든다.
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", member.getId());
        claims.put("nm", member.getName());
        claims.put("seq", member.getSeq());
        claims.put("typ", classification);
        return claims;
    }

    public String getUserIdfromToken(String token) { // 토큰에서 유저의 아이디를 반환한다.
        Claims claim = parseClaims(token);
        return (String) claim.get("id");
    }
}
  • generateToken(): 엑세스 토큰과 리프레쉬 토큰을 만들어서 빌더패턴으로 정의된 TokenInfo DTO에 담는다. 토큰 내부의 헤더, 페이로드, 클레임, 서명 부분을 직접 setHeader() , setClaims() , signWith() 메소드들을 통해서 설정하고 setExpiration() 메소드를 통해 만료기한을 설정한다. signWith() 메소드는 시크릿키를 통해 인증이 필요해서 첫 번째 파라미터로 넘겨준다.
  • validateToken(): 토큰을 검증하는 메소드다. 클라이언트가 인증이 필요한 API 호출 시 바로 작동하는 필터 클래스에서 호출하기 위해 JwtTokenProvider 클래스에 정의했다. 해당 코드를 보면 클레임을 복호화 하는 parseClaims() 메소드를 호출하고 있는데 이 메소드 안에 실질적으로 토큰이 유효한지 검증하는 코드가 담겨있기 때문이다. 해당 메소드 호출 후 예외가 발생하지 않는다면 유효한 토큰이고, 문제가 있는 토큰이라면 각 문제에 따라 다른 예외들을 발생시킨다.
  • parseClaims(): 토큰의 클레임 부분을 복호화하는 코드이다. 해당 코드 내에서 .setSigningKey(key) 를 통해 시크릿 키 인증을 하고, .parseClaimsJws(accessToken) 를 통해 클레임 복호화 및 토큰 검증을 실행한다.
  • createHeader(): 토큰의 헤더 부분을 만든다. 헤더 값은 typ: JWT, alg: HS256 고정이다.
  • createClaims(): 토큰의 클레임 부분을 만든다. 클레임에는 해당 토큰의 사용자 정보가 들어간다. 토큰이 탈취 당할 시를 대비해 회원의 간략한 정보만 넣는다. 여기서 넣어준 사용자 정보는 클라이언트 쪽에서 토큰을 복호화 하여 사용할 수 있다.

 

 

5. 로그인 로직 작성

public TokenInfo login(String username, String password) {
    Member member = memberMapper.getMember(username);

    log.info("찾은 아이디: {}", member.getId());

    log.info("DB 암호화 비밀번호: {}", member.getPw());

    if (!passwordEncoder.matches(password, member.getPw())) {  // 요청 비밀번호와 DB 안의 비밀번호와 일치하는지 확인
        throw new PasswordNotMatchException(); // 일치하지 않으면 예외처리로 인해 중단
    }

    TokenInfo tokenInfo = jwtTokenProvider.generateToken(member); // 로그인에 성공한 회원에 대해 토큰 생성

    return tokenInfo;
}
  • 토큰을 발급 받기 전 아이디와 비밀번호 유효성 검증을 해야 하지 않겠는가 로그인 서비스 클래스에 해당 로직을 손수 짜준다.
  • 원래는 스프링 시큐리티의 기능을 이용하여 구현해야 한다.

 

 

6. JwtAuthenticationFilter 작성

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    private AuthenticationManager authenticationManager;

    @Override
    public void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws IOException, ServletException {
        // 1. Request Header에서 JWT 토큰 추출
        String token = resolveToken(request);

        log.info("필터에 들어온 토큰: {}", token);

        // 2. vaidateToken으로 토큰 검증
        try {
            if(token != null && jwtTokenProvider.validateToken(token, response)) {
                // 토큰이 유효할 경우 토큰에서 클레임 부분에 유저 아이디를 가지고 와서 SecurityContext에 저장
                String userId = jwtTokenProvider.getUserIdfromToken(token);
                log.info("토큰의 주인: {}", userId);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userId, null, null);

                SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
                log.info("해당 사용자 인증여부: {}", auth.isAuthenticated());
            }
        } catch (ExpiredJwtException e) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "토큰 만료");
        }
        filterChain.doFilter(request, response);

    }

    // Request Header에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if(StringUtils.hasText(token) && token.startsWith("Bearer")) {
            return token.substring(7);
        }
        return null;
    }
}
  • 스프링 시큐리티는 인증을 처리하기 위한 필터가 여러가지 있는데 그 중 OncePerRequestFilter를 상속 받아 Jwt 필터로 커스텀한다.
  • doFilterInternel() 안의 코드들이 필터로 인식되어 처리 된다.
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userId, null, null);

SecurityContextHolder.getContext().setAuthentication(authenticationToken); 
  • resolveToken(HttpServletRequest request) 메소드를 통해 요청 헤더에서 토큰 값을 뽑아온다.
  • jwtTokenProvider.validateToken() 을 통해 문제가 없는 토큰인지 검증이 완료되면 new UsernamePasswordAuthenticationToken(userId, null, null) 을 통해 jwtTokenProvider.getUserIdfromToken(token) 메소드로 뽑아온 유저 아이디를 인증이 완료된 인증 객체로 만들어준다.
  • 스프링 시큐리티에는 인증 객체라는 것이 존재한다. Authentication이라는 객체인데 이 객체에는 유저의 아이디 등을 포함한 유저의 정보가 들어있다. UsernamePasswordAuthenticationToken 객체는 이를 상속받은 객체이다.
  • 인증객체로 만들어준 UsernamePasswordAuthenticationToken을 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 을 통해 시큐리티 컨텍스트에 넣어준다. SecurityContext는 Authentication 객체의 집이다. 여기에 들어간 인증객체는 스프링 시큐리티가 인증이 된 사용자라고 자동으로 인식을 한다.

 

 

7. 스프링 시큐리티 설정 파일 작성

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig{
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // 스프링 시큐리티 접근 보안 설정
        return httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(request -> {
                    // 로그인 요청에 대해서는 모두 접근 허용
                    request.requestMatchers("/member/login").permitAll()
                            .requestMatchers("/api").permitAll()
                            // 이 밖에 모든 요청들은 인증을 필요로 한다.
                            .anyRequest().authenticated();
                }).addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
                        UsernamePasswordAuthenticationFilter.class).build();
                        // 요청 사용자에게 토큰이 있는지 먼저 검사하기 위해 JWT 필터를 먼저 거치도록 설정
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
  • 스프링 시큐리티 설정 파일에는 일반 스프링 설정 파일과 다르게 @EnableWebSecurity 라는 어노테이션을 붙여야 한다. 동시에 웹 인증 및 보안에 대해 스프링 시큐리티가 모든 것을 관여하게 된다.
  • SecurityFilterChain 이라는 객체를 빈으로 등록해줌으로서 웹의 접근 권한을 설정하고 해당 접근 권한이 있는지 판단하기 위한 필터를 등록해준다.
  • request.requestMatchers("/member/login").permitAll() 은 “/member/login” 매핑에 대해 인증 여부든 상관 없이 모두 접근을 허용한다는 것이다. 로그인은 어떤 사람이든 해야 하지 않겠는가
  • .anyRequest().authenticated() 은 앞서 설정해준 매핑들을 제외한 모든 요청들은 인증이 되어야 한다. 로그인을 진행하고 필터 클래스에서 인증이 완료된 Authentication 객체가 시큐리티 컨텍스트 안에 들어가야 접근할 수 있다.
  • .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class).build() 해당 코드는 우리가 커스텀 한 JWT 필터를 UsernamePasswordAuthenticationFilter 전에 배치한다는 뜻이다. 보통 이 필터가 가장 먼저 실행되기 때문에 JWT 필터를 먼저 실행하게 하는 것이다.
728x90
728x90

JWT는 Json Web Token의 약자로 일반적으로 클라이언트와 서버 사이에서 통신할 때 권한을 위해 사용하는 토큰이다. 토큰을 발급 받고 매 통신마다 토큰 인증에 성공해야만 JSON 통신이 가능함.

 

 

1. JWT의 구조

  • JWT의 구조는 Header, Payload Signature 세 가지로 구성되어 있다.
  • 각각의 제외한 JSON 부분들은 하나의 데이터로 표현될 때 Base64로 인코딩되어 표현된다.

 

Header   

{ 
   "alg": "HS256",
   "typ": "JWT"
 }

 

    • alg ⇒ JWT 구조에서 Signature에는 Header와 Payload가 합쳐져 특정 알고리즘으로 암호화가 진행이 된 다음에 만들어지는데, 그 때 사용되는 알고리즘이다. "alg": "HS256" 는 Signature 부분을 HS256 알고리즘으로 암호화 되었다는 뜻이다.
    • typ ⇒ 토큰의 유형이다. JWT 토큰을 사용한다.

 

Payload

{
  "sub": "1234567890",
  "lat": 1516239022
}

 

    • Payload(페이로드)에는 사용자의 인증 정보가 담긴다. 여기에 담기는 정보의 조각 단위를 Claim이라 불린다.
    • Claim에는 등록된 클레임, 공개 클레임, 비공개 클레임 3가지 종류가 있는데 자주 사용되는 것은 등록된 클레임이다.
    • iss ⇒ 토큰 발급자(issuer)
    • sub ⇒ 토큰 제목(subject) 주로 사용자 이메일 사용한다.
    • aud ⇒ 토큰 대상자(audience)
    • exp ⇒ 토큰 만료 시간(expiration)
    • nbf ⇒ 토큰 활성 날짜(not before)
    • iat ⇒ 토큰 발급 경과 시간(issued at) 토큰 발급 이후의 경과 시간
    • jti ⇒ JWT 토큰 식별자(JWT ID) 중복 방지를 위해 사용한다.

 

Signature

  • Signature(서명)은 헤더와 페이로드가 합쳐져 암호화 후에 담겨져 있는 데이터다.
    1. 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩
    2. 인코딩한 헤더(Header)에서 정의한 알고리즘으로 암호화
    3. 암호화한 값을 다시 BASE64로 인코딩하여 생성

 

 

2. JWT 응용 원리

  1. 사용자가 아이디와 비밀번호를 입력하고 로그인을 요청
  2. DB 내 아이디 비밀번호 데이터와 일치하는지 확인
  3. 해당 사용자 정보를 기반으로 JWT 토큰 발급
  4. 응답 헤더에 해당 토큰을 추가하여 클라이언트로 전송하고 클라이언트는 이를 저장
  5. 이후 JSON 통신을 할 때마다 서버로 토큰을 요청 헤더에 추가하여 전송
  6. 서버에서 해당 토큰을 검증
  7. 검증이 완료되면 요청 데이터를 응답
728x90
728x90

브이월드에서 제공하는 API를 이용하여 WFS 방식의 지도 레이어를 그려보자

 

먼저, 알아야할 몇가지들을 보자

 

WFS 란?

  - 지도를 호출하는 방식 중에, 지도서버 내에서 지도와 관련된 데이터 자체를 호출하는

    방식의 프로토콜이다.

  - 호출된 해당 데이터 중에 피처(Feature) 라고 하는 공간 및 지리정보 데이터를 이용하여

    지도 위에 다양한 기능을 추가 할 수 있다.

 

1. API 신청

  - https://www.vworld.co.kr 브이월드 홈페이지에서 API 신청을 할 수 있다.

 

 

2. WFS를 호출하기 위한 GET 방식 URL 파라미터 설정

  - 지도에 추가할 특정 기능을 위해 본인이 원하는 레이어 종류가 있을 것이다.

    그런것들을 포함하여 피처 데이터 값을 받는 방식, 피처의 최대 갯수 등의 여러 부가정보를 설정할 수 있다.
    자세한 것은 브이월드 홈페이지에 나와있는 공식 API문서를 확인해야 한다.

let wfsUrl = `https://api.vworld.kr/req/wfs?
SERVICE=WFS&  -- 지도 호출 방식
REQUEST=GetFeature&
TYPENAME=lt_c_uq111& -- 지도 종류
BBOX=13987670,3912271,14359383,4642932&
PROPERTYNAME=mnum,sido_cd,sigungu_cd,dyear,dnum,ucode,bon_bun,bu_bun,uname,sido_name,sigg_name,ag_geom& -- 호출할 데이터
VERSION=1.1.0& -- 버전
MAXFEATURES=40& -- 최대 피처갯수
SRSNAME=EPSG:900913& -- 좌표계
OUTPUT=application/json& -- 데이터 받는 방식
EXCEPTIONS=text/xml&
KEY='API 인증키'&
DOMAIN='127.0.0.1:8201`

 

 

3. WFS 피처 호출

  - 앞서 파라미터를 설정해준 URL을 가지고 fetch 방식으로 호출해야한다.

fetch(url)
    .then(response => response.json())
    .then(data => {
	for(let i = 0; i < data.features.length; i++){
		const feature = new ol.format.GeoJSON().readFeature(data.features[i]);
			
		const source = new ol.source.Vector({
			features: [feature]
 		});
	
        	const layer = new ol.layer.Vector({
            		source: source,
        	});		
	 	mapView.addLayer(layer);
	}
    })

 

  - 데이터를 호출하고 레이어까지 생성하는 코드이다. 하나씩 살펴보자.

 

 

4. 호출된 피처(Feature)를 GeoJSON으로 변환하기

  - GeoJSON은 간단하게 공간, 지리, 위치 정보를 가진 JSON이라 할 수 있다.

    원본 Feature 값으로는 해당 정보들을 처리 할 수 없기 때문에 JSON으로 변환을 시켜주는 것이다.

const feature = new ol.format.GeoJSON().readFeature(data.features[i]);

 

 

5. VectorSource 생성

  - 변환된 GeoJSON을 가지고 Vector 객체를 만들어준다.

const source = new ol.source.Vector({
    features: [feature]
});

 

 

6. VectorLayer 생성

  - 앞서 생성한 VectorSource를 가지고 기존의 베이스 맵 객체를 가져와 최종 레이어를 생성한다.

const layer = new ol.layer.Vector({
        source: source,
});		
mapView.addLayer(layer); // 레이어 추가

 

이렇게 공간정보 전문 플랫폼에서 제공하는 WFS API 레이어를 생성하기 위해서는

1. WFS API 호출

2. Feature 정보를 GeoJSON으로 변환

3. VectorSource 생성

4. VectorLayer 생성

5. 레이어 추가

 

이와 같은 과정이 필요하다.

 

그리고,

1. OpenLayers의 ol.source.Vector 와 ol.layer.Vector 객체의 적절한 조합이 중요하다.

2. Feature 데이터를 무조건 GeoJSON으로 변환할 것.

 

이 두가지를 명심하자.

 

 

728x90

'웹개발 > OpenLayers' 카테고리의 다른 글

[OpenLayers] WMS API 호출 with Vworld  (1) 2023.10.23
728x90

브이월드에서 제공하는 API를 이용하여 WMS 방식의 지도 레이어를 그려보자

 

먼저, 알아야할 몇가지들을 보자

 

WMS 란?

  - 지도를 호출하는 방식 중에, 지도서버 내에서 지도와 관련된 데이터를 이용하여

    생성된 이미지를 호출하는 방식의 프로토콜이다.

  - 보통, 타일 방식으로 생성된 기본 지도 위에 특별한 기능을 추가하기 위해 선을 그리거나,

    지도에서 건물을 감싸는 레이어를 호출하는 목적으로 사용 된다.

 

1. API 신청

  - https://www.vworld.co.kr 브이월드 홈페이지에서 API 신청을 할 수 있다.

 

 

2. WMS를 호출하기 위한 파라미터 설정

  - 지도에 추가할 특정 기능을 위해 본인이 원하는 레이어 종류가 있을 것이다.

    그런것들을 포함하여 해당 지도 정보 값을 받는 방식, 이미지의 크기 등을 설정할 수 있다.
    자세한 것은 브이월드 홈페이지에 나와있는 공식 API문서를 확인해야 한다.

let mapParam = {
    SERVICE: 'WMS',  // 지도 호출 방식
    REQUEST: 'GetMap',
    VERSION: '1.3.0',  // 버전
    LAYERS: ['lp_pa_cbnd_bonbun','lp_pa_cbnd_bubun'],  // 지도 종류
    STYLES: ['lp_pa_cbnd_bonbun','lp_pa_cbnd_bubun'],  // 지도의 스타일 종류(지도 색상, 글자크기 등)
    CRS: EPSG,
    WIDTH: '256',  // 이미지 너비
    HEIGHT: '256',  // 이미지 높이
    FORMAT: 'image/png',  // 지도 정보 값 받는 방식
    TRANSPARENT: 'TRUE',
    BGCOLOR: '0xFFFFFF',
    EXCEPTIONS: 'text/xml',
    KEY: 'API 인증키',  // 브이월드에서 신청한 API 인증 키값을 넣으면 된다.
    DOMAIN: '127.0.0.1:8201',
};

 

 

3. WMS 레이어 생성

  - 지도를 본격적으로 셋팅해주는 OpenLayers의 기능을 사용하여 레이어를 생성해주자

let wmsLayer = new ol.layer.Tile({
    source: new ol.source.TileWMS({
        url: 'https://api.vworld.kr/req/wms',
        params: mapParam,
    })
});

  - 브이월드 API URL을 입력하고 앞에서 설정한 파라미터를 넣어주면 생성이 완료된다. 

 

 

4. 생성된 레이어 그리기

  - WMS를 호출하기 이전에 기본 지도가 존재한다는 것을 가정하고

    해당 지도 오픈레이어스 객체의 함수를 이용하여 추가해주기만 하면 된다.

mapView.addLayer(wmsLayer);

 

 

 

 

 

 

 

 

 

728x90

'웹개발 > OpenLayers' 카테고리의 다른 글

[OpenLayers] WFS API 호출 with Vworld  (0) 2023.10.31