어느덧 신규 프로젝트에 투입된지 2달여가 지나가고 있다.

현재 프로젝트는 17년 기준으로 개발 된 프로젝트에 대해 마이그레이션 작업을 하는 프로젝트여서 과거 소스 및 라이브러리를 유지해야하는 부분이 있었다.

 

오늘은 sitemesh를 통해 같은 url로 접근시 pc, mobile에 대한 레이아웃을 분기하는 작업에 대해 정리하려한다.

 

보통 sitemesh를 통해 pc, mobile의 레이아웃 분기는 url에 /mobile 과 같이 특정한 값을 추가하여 분기하는데, 필자의 경우는 같은 url을 유지하고 싶어하는 요구조건이 있어 sitemesh의 내부 작업이 불가피했다.

 

sitemesh 기본 설정은 검색을 통해 금방 알 수 있으니 생략하고, 이번 프로젝트에서 위의 경우를 해결하기 위한 내용을 중점적으로 다루겠다.

 

우선 /WEB-INF/sitemesh.xml 파일에 접근하여 아래와 같이 작성한다.

<sitemesh>
	<property name="decorators-file" value="/WEB-INF/decorators.xml"/>
	<property name="decorators-file-mobile" value="/WEB-INF/mobile-decorators.xml"/>
    
	...
    
	<decorator-mappers>
		<mapper class="com.example.home.common.sitemesh.CustomConfigDecoratorMapper">
			<param name="config" value="${decorators-file}" />
			<param name="mConfig" value="${decorators-file-mobile}" />
		</mapper>
	</decorator-mappers>
</sitemesh>

위 소스에서의 중점은 기존 sitemesh.jar의 ConfigDecoratorMapper를 CustomConfigDecoratorMapper로 생성하여 변경한다.

com.opensymphony.module.sitemesh.mapper.ConfigDecoratorMapper -> com.example.home.common.sitemesh.CustomConfigDecoratorMapper

 

이렇게 하는 이유는 내부에서 요청 접속 기기의 유형을 판별하여 decorators.xml을 유동적으로 적용하기 위함이다.

 

추가로, pc, mobile을 구분하고자 하는 경우 spring-mobile-device dependecy를 추가하면 손쉽게 적용 할 수 있다.

public class CustomConfigDecoratorMapper extends AbstractDecoratorMapper {

	private ConfigLoader configLoader = null;
	private ConfigLoader mConfigLoader = null;
    
	/** Create new ConfigLoader using '/WEB-INF/decorators.xml' file. */
	public void init(Config config, Properties properties, DecoratorMapper parent) throws InstantiationException {
		super.init(config, properties, parent);
		try {
			String fileName = properties.getProperty("config", "/WEB-INF/decorators.xml");
			String mFileName = properties.getProperty("mConfig", "/WEB-INF/mobile-decorators.xml");
			configLoader = new ConfigLoader(fileName, config);
			mConfigLoader = new ConfigLoader(mFileName, config);
		} catch (Exception e) {
			throw new InstantiationException(e.toString());
		}
	}

	public Decorator getDecorator(HttpServletRequest request, Page page) {
		...
        
		Device device = DeviceUtils.getCurrentDevice(request);
		
		...

		try {
			if(device != null && device.isMobile()) {
				name = mConfigLoader.getMappedName(thisPath);
			} else {
				name = configLoader.getMappedName(thisPath);
			}
			
		} catch (ServletException e) {
			e.printStackTrace();
		}
        
		...
	}
    
	public Decorator getNamedDecorator(HttpServletRequest request, String name) {
		...
		Device device = DeviceUtils.getCurrentDevice(request);
		try {
			if(device != null && device.isMobile()) {
				result = mConfigLoader.getDecoratorByName(name);
			} else {
				result = configLoader.getDecoratorByName(name);
			}
		} catch (ServletException e) {
			e.printStackTrace();
		}
		...
	}
}

위의 소스를 보면 device로 접속기기의 유형을 판별하고 이를 통해 pc, mobile에 맞는 decorators.xml을 분기처리 하였다.

이렇게 하면 화면에서 각 기기 유형별로 sitemesh가 적용됨을 확인 할 수 있다.

금년도에 들어서면서 신규 프로젝트를 맡게되었습니다.

해당 프로젝트에서 역할은 Backend 영역으로 REST API 프로젝트에 대한 환경설정 및 서비스 개발입니다.

우선 해당 프로젝트는 Front 영역과의 인증 부분에서 JWT 방식을 이용했습니다. 그리고 추가로 Spring Security를 통한 보안성을 강화했습니다. 이렇게 기본적인 구성과 함께 Test API를 배포했습니다. 이때는 사용자 권한에 대해서 기본적으로 ROLE_USER, ROLE_ADMIN을 부여하였습니다. Spring Security에서 기본적으로 권한의 prefix가 `ROLE_`입니다. 그렇기에 Test API에서도 사용자 정보에서 권한정보값을 앞에서와 같이 지정하였습니다. 그러나 해당 프로젝트에서 권한 설정값은 앞의 규칙을 준수하지 않았습니다. 그렇기에 권한에 있어 prefix의 설정값을 변경해줄 필요가 있었습니다. 이 부분에 대해 알아보고 해결한 내용을 정리해보려합니다.

 

 

1. loadUserByUsername 재정의를 통해 해당 프로젝트에서 쓰이는 권한 값을 UserDetails interface를 implement한 AuthenticatedUser에 넣습니다.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    AuthenticatedUser user = authMapper.selectSecurityUserInfo(username);

    if (user != null) {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        authorities.add(new SimpleGrantedAuthority(user.getThrzGrpId()));

        user.setAuthorities(authorities);
    }

    return user;
}

 

2. 정상적인 로그인시 JWT를 통해 사용자 정보가 담긴 Token값을 반환하고 해당 Token은 로그인 외 타 API를 호출 할 때 인증값으로 보내며, 서버에서 해당 사용자의 권한을 알 수 있습니다.

이때, SecurityConfig에서 JWT에 대한 Filter를 부여해서 정상적인 토큰인지 확인을 했습니다.

이후 해당 Token을 Request Header Authorization에 추가하고 특정 컨트롤러에 @PreAuthorize를 추가함으로써 정상 로그인 된 사용자의 권한을 체크하는 로직을 세웠습니다. 그러나 정상적으로 권한이 담겼음을 확인하였는데도 권한여부에서 false가 떨어졌습니다. 확인 결과 Spring Security에서 기본적으로 권한에 대해 prefix가 존재하면 해당 값은 `ROLE_`인걸 확인했습니다.(RoleVoter.class에서 확인 가능.)

// 시큐리티 컨텍스트 객체를 얻습니다.
SecurityContext context = SecurityContextHolder.getContext();

// 인증 객체를 얻습니다.
Authentication authentication = context.getAuthentication();

// 로그인한 사용자정보를 가진 객체를 얻습니다.
Principal principal = authentication.getPrincipal();

// 사용자가  가진 모든 롤 정보를 얻습니다.
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

 

3. 해당 prefix를 원하는 값으로 변경하는 과정은 매우 쉽습니다.

SecurityConfig에서 아래와 같이 GrantedAuthorityDefaults를 정의해주면 됩니다. 해당 값 설정 후 정상 로그인 후 해당 계정에 대한 권한 접근 가능여부 확인 결과 true를 확인 할 수 있었습니다.

@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("TEST_");
}

 

알고보면 별거 아닌 내용이였지만, 지금까지 프로젝트 중에 권한에 있어서 ROLE_와 같이 prefix를 변경하는 일은 없었기에 해결하는 과정에서 조금 어려움은 있었지만 뜻깊은 과정이었습니다.

CORS란?

CORS(Cross-Origin Resource Sharing)는 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조입니다.

기본적으로 HTTP request는 Cross-Site HTTP Requests가 가능합니다. 그러나 보안상의 이유로 브라우저에서는 Same Origin Policy를 적용 받기 때문에 요청이 불가합니다. 즉, 프로토콜, 호스트명, 포트가 같아야만 요청이 가능합니다.

출처  : https://t1.daumcdn.net/cfile/tistory/256C904258CB85E01E

 

최근 프로젝트를 진행하면서 Back End 서버를 구성하고 REST API를 구성하여 Front End 개발자분께 제공하였습니다.

처음 프로젝트를 구성할때 WebMvcConfigurer를 임플리먼트하여 클래스를 생성하고 @Configuration 어노테이션을 선언하였습니다. 이 안에는 addCorsMappings을 재정의하여 Cors관련하여 처리를 미리하였습니다. 이후 postman과 내부에서 테스트를 진행하였을때 문제없다고 판단하고, 개발서버에 배포한 후 Front End 개발자분께 테스트 요청을 드렸습니다. 그러나 이때 Cors 허용 관련 문제가 발생하였습니다. 처음에는 Cors 설정을 했음에도 불구하고 왜 이런 문제가 발생하는지 이해하지 못했습니다. 그러다가 구글링을 통해 아래와 같은 답을 얻었습니다.

 

참조 사이트부터 먼저 링크 걸겠습니다.

https://stackoverflow.com/questions/40418441/spring-security-cors-filter/43559288#43559288

 

Spring security CORS Filter

We added Spring Security to our existing project. From this moment on we get a 401 No 'Access-Control-Allow-Origin' header is present on the requested resource error from the our server. That's bec...

stackoverflow.com

https://toycoms.tistory.com/37

 

Spring Security CORS

CORS란? - HTTP 요청은 기본적으로 Cross-Site HTTP Requests가 가능합니다. Simple 하게 다른 도메인의 Resource를 사용하는것을 말합니다. 하지만 Cross-Site HTTP Requests는 Same Origin Policy를 적용 받기 때문에 요청

toycoms.tistory.com

 

결론을 말하자면, 진행되고 있는 프로젝트는 Spring Security가 적용된 프로젝트입니다. Spring Security가 적용되었을시에는 Cors설정과 관련하여 HttpSecurity 설정 부분에서 cors().configurationSource(corsConfigurationSource())을 등록해줘야합니다.

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();

    configuration.addAllowedOrigin("*");
    configuration.addAllowedHeader("*");
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .antMatcher("/api/**")
        .authorizeRequests()
        .anyRequest().authenticated()
        .and()
            .headers()
                .cacheControl()
        .and()
            .contentTypeOptions()
        .and()
            .httpStrictTransportSecurity()
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000)
        .and()
            .frameOptions().deny()
            .xssProtection().block(false)
        .and().and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .exceptionHandling()
                .authenticationEntryPoint(new RestUserAuthenticationEntryPoint())
                .accessDeniedHandler(new RestAccessDeniedHandler())
        .and()
            .csrf().disable()
            .formLogin().disable()
            .cors().configurationSource(corsConfigurationSource());

    JwtAuthenticationApiFilter jwtAuthFilter = new JwtAuthenticationApiFilter(jwtTokenProvider);

    http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

또한, addCorsMappings를 재정의한 내용도 유지하여야 저는 Cors 관련 이슈 부분을 해결 할 수 있었습니다.

 

Cors 관련해서는 항상 어려우면서도 또 이렇게 간단하게 해결이 되어서 정말 다행입니다.

1. XSS란 : 크로스 사이트 스크립팅이라하며, 웹 애플리케이션에서 많이 나타나는 취약점의 하나로 웹사이트 관리자가 아닌 이가 웹 페이지에 악성 스크립트를 삽입할 수 있는 취약점이다.

2. SCRF란 : 크로스 사이트 요청 위조 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.

OSIV(Open Session In View)

OSIV는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다.

 

위의 경우 LAZY전략으로 조회시 롯데팀에 대한 정보가 조회되지 않는다.

이유는 서비스에서 영속성 컨텍스트가 종료되었기 때문이다.

이와 같은 경우를 해결하기 위해 영속성 컨텍스트 종료를 컨트롤러에서 종료하게 하여,

컨트롤러에서도 롯데팀에 대해서 조회 할 수 있게 한다.

단, 이때 프록시를 통해 정보를 조회 할 수는 있으나, I, U, D와 같은 기능은 할 수 없다.(트랜잭션이 종료되었기 때문에)

 

정리해서 다시 그리면 아래와 같습니다.

 

 

강의 주소 : https://youtu.be/fSXh4hWJtKo

 

 

강의 주소 : https://youtu.be/y4a0X7dS2q8

2. MySQL

    InnoDB 스토리지 엔진

    Repeatable read 이상 방식을 사용 -> 부정합 발생하지 않습니다.

 

리핏테이블 리드 방식에서는 트랜젝션이 종료되지 않은 순간까지는 동일한 조회 결과가 나옵니다.

보통 스프링에서 CRUD에서 CUD에서만 트랜잭셔널 어노테이션을 붙이게 되는데, R에서도 트랜잭셔널 어노테이션을 붙이는게 좋습니다.

 

 

강의 주소 : https://youtu.be/n1lvzeffDMk

- 일어날 수 있는 문제의 경우

위와같이 정산 서비스에 대해서 조회시 만원의 결과를 보여주다가 어느순간 커밋이후 2만원으로 결과가 보여진다면, 데이터의 정합성이 깨지는 것입니다.

이러한 현상을 PHANTOM READ(팬텀 리드)라 합니다.

 

이러한 팬텀 리드 현상을 해결하기 위해서는REPEATABLE READ 방식을 써야 합니다.

 

 

 

 

강의 주소 : https://youtu.be/4iDNmluK6DE

- 트랜잭션 : 일이 처리되기 위한 가장 작은 단위

 

- DB 격리 수준

  1. 오라클

      READ COMMIT

오라클의 경우는 변경하려고하는 데이터가 커밋이 되기 전까지는 undo영역의 데이터를 읽어옵니다.

좌측의 A트랜잭션이 업데이트 후 커밋 직전에 B트랜잭션에서 empno=11을 조회하게 되면, 장보고가 아닌 임꺽정이 조회됩니다.

 

 

강의 주소 : https://youtu.be/FbeU3ZHgOhs

- ResponseDto.java의 status의 형태를 HttpStatus -> int로 변경합니다.

package com.cos.blog.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
	int status;
	T data;
}

 

- UserApiController.java

package com.cos.blog.controller.api;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.cos.blog.dto.ResponseDto;
import com.cos.blog.model.RoleType;
import com.cos.blog.model.User;
import com.cos.blog.service.UserService;

@RestController
public class UserApiController {

	@Autowired
	private UserService userService;
	
	@PostMapping("/api/user")
	public ResponseDto<Integer> save(@RequestBody User user) {
		
		user.setRole(RoleType.USER);
		int result = userService.회원가입(user);
		
		return new ResponseDto<Integer>(HttpStatus.OK.value(), result);
	}
}

 

- GlobalExceptionHandler.java

package com.cos.blog.handler;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import com.cos.blog.dto.ResponseDto;

@ControllerAdvice
@RestController
public class GlobalExceptionHandler {

	@ExceptionHandler(value=Exception.class)
	public ResponseDto<String> handleArgumentException(IllegalArgumentException e) {
		return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
	}
}

 

 

강의 주소 : https://youtu.be/f5zHFb1BHmY

+ Recent posts