어느덧 신규 프로젝트에 투입된지 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란 : 크로스 사이트 요청 위조 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.

1. Captive portal이란?

 Captive portal(캡티브 포털)이란 Wi-Fi 또는 유선 네트워크에 새로 연결된 사용자에게 네트워크 리소스에 대한 광범위한 액세 권한이 부여되기 전에 표시되는 웹브라우저로 액세스하는 웹 페이지입니다.

https://en.wikipedia.org/wiki/Captive_portal

 

Captive portal - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Web page displayed to new users of a network An example of a captive web portal used to log onto a restricted network. A captive portal is a web page accessed with a web browser that i

en.wikipedia.org

 

2. Captive portal을 통한 모바일 웹 서비스 적용시 이슈

우선 필자는 Captive portal을 통해 모바일 웹 서비스를 적용중에 있다. 이때 발생하는 이슈로 불완전한 script 및 인증시의 캡티브 포털 창의 자동 닫힘으로 인해 서비스를 지속적으로 제공 할 수 없는 문제가 있었다.

이를 해결하기 위해 Captive portal 접속시 자체 브라우저로 모바일 웹 서비스를 연동 해줘야하는 필요성이 제기되었다.

우선 안드로이드, IOS에서 발생하는 문제의 차이점이 있는데,

안드로이드의 경우에는 Captive portal 인증 후 창이 자동적으로 닫혀서 이후 웹 서비스로의 연결에 성공해도 지속적인 사용이 불가능하였다.

IOS의 경우에는 Captive portal 인증, 웹 서비스로의 연결에 성공하였으나 script에 대해 정상적인 조작이 불가능하였다. 단적인 예로 alert 이벤트도 작동하지 않았다.

위와 같은 문제를 해결하기 위해서는 모바일 브라우저로 인증 및 웹 서비스 연결을 통해 정상적인 서비스의 제공이 꼭 필요한 상황이었다.

 

3. Captive portal 인증 및 모바일 브라우저 웹 서비스 연결 해결책

필자는 어찌저찌 위의 문제들을 해결을 하였으나, 이같은 방법은 정상적인 방법이라 판단되지 않음을 미리 알리겠다.

또한, 이러한 방법을 통해서라도 정상적인 서비스를 제공 하고자 하는 필자와 같은 사람들에게 도움이 되기를 바란다.

 

우선, 필자가 이용한 AP는 CISCO사의 AP이다.

CISCO 장비 내에선 Captive portal 기능과 관련하여 splash page 설정 및 인증 이후의 redirection에 대한 설정까지 지원하고 있다.

splach page를 설정함에 있어 프로토콜을 http, https를 지정 할 수 있다. 다른 AP의 경우는 http만 되는 경우도 있을텐데 필자가 성공한 케이스는 https를 통한 케이스만 성공하였다.

 

안드로이드의 경우 Captive portal 인증 후 인증 창이 닫히기 때문에 우선적으로 모바일 브라우저로 연동이 필요하였다. 이때 이용 할 수 있는 방법이 intent:// 를 통한 방법이다.

 

location.href = 'intent://splash_page_url#Intent;scheme=http;package=com.android.chrome;end';

필자의 경우는 화면 접속과 함께 Captive portal 접속임을 판단하여 위와 같이 작성하였다.

그러면 이미지와 같이 Captive portal 인증 화면이 전환되면서 브라우저에서 계속이라는 링크가 활성화 된다.

활성화 된 링크를 누르게 되면 모바일 브라우저로 전환되면서 AP에서 설정한 splash page로 이동하게 된다. 이때 http일 경우에는 정상적인 화면으로 넘어가지 못한다. 그래서 위에서 적었듯이 https로만 성공한 케이스인 것이다.

모바일 브라우저를 통해 인증 및 웹 서비스 연결이 정상적으로 이뤄졌으며, 일반적인 wifi와 같이 인터넷을 이용 할 수 있다.

 

 

 

 

 

 

 

 

 

IOS의 경우는 안드로이드와 같이 순탄한 과정은 아니였다. 그러나 결과적으로 safari로 웹 서비스를 전환하는데 성공했다.

우선 IOS의 경우에는 인증 전에 safari로 전환을 하지 못했다. 필자는 여러 참고 블로그를 통해 서비스를 구현 해보려했으나 모두 실패했다. 참고한 블로그에 대해서는 아래에 기재해 놓겠다.

 

IOS의 경우에는 ftp 서버를 만들고 이를 통해서 bridge 페이지에서 호출 하는 방식으로 처리를 하고 있었다. 그러나 이 방식도 IOS15버전부터는 기능이 제한된다하였다. 필자는 이런 가운데 가장 아래의 스택오버플로우에서 한줄기 빛과 같은 답변을 받았다.

답변의 내용은 다음과 같다.

Captive portal 인증 후 요청하고자 하는 url을 a tag에 full path로 기재하면 safari로 전환이 가능하는 답변이었다.

<a href="https://redirection_url">Safari로 열기</a>

위와 같이 href 속성 내에 요청하고자 하는 url을 기재하였다니 다음과 같이 성공하였다.

safari로 전환이 성공적으로 이뤄졌으며, 일반적인 wifi와 같이 인터넷을 이용 할 수 있었다.

 

 

https://www.burndogfather.com/201

 

카카오, 네이버 인앱에서 외부 브라우저 띄우는 방법 정리 [Android/iOS]

현재 이 방법은 iOS15 업데이트로 인해 사용이 불가능한 방법입니다. https://burndogfather.tistory.com/257 사용자가 조금 더 불편하게 접속하는 방법이 현재까지는 차선책이네요 ㅠㅜ 더 쾌적하게? 웹 서

www.burndogfather.com

https://burndogfather.tistory.com/257

 

iOS15 업데이트와 함께 찾아온 인앱브라우저 지옥과 최선

카카오, 네이버 인앱에서 외부 브라우저 띄우는 방법 정리 [Android/iOS] 더 쾌적하게? 웹 서비스를 개발하는 입장에서 클라이언트가 위와 같은 브라우저를 사용할때 정말 피곤합니다. 휴대폰 본인

www.burndogfather.com

https://stackoverflow.com/questions/29744245/ios-open-a-welcome-page-in-safari-not-cna-post-authentication/38462827#38462827?newreg=d3fbde7acabb4a5cb7f6a5ab6e97a1c2 

 

iOS: Open a Welcome Page in Safari, not CNA (post-authentication)

I'm running a captive portal, target audience are mobile devices only. I would like to open a "welcome page" to the user after he authenticated in the CNA. This page should open in (mobile) Safari,...

stackoverflow.com

 

4. 느낀점

사실 Captive portal이라는 개념에 대해서는 프로젝트를 접하면서 알게되었다. 이전에 공공와이파이를 이용하여 서비스 자체에 대해서는 접해본적은 있었으나, 이와같이 구성 및 인증에 대해서까지는 알지 못하였다. 이번 기회로 해당 기술에 대해서 배울 수 있어서 좋았으며, 사실 모바일 브라우저로 연동하는 부분에 있어 너무나도 빡침이 있었지만 그건 어찌저찌 해결되었으니 만족한다. 위의 링크해둔 블로그 작성자님께 무한함 감사를 드리며 마치겠다.

+ Recent posts