관리 메뉴

HAMA 블로그

Spring for android - 예제 소스 분석 (2) 본문

Spring

Spring for android - 예제 소스 분석 (2)

[하마] 이승현 (wowlsh93@gmail.com) 2015. 7. 3. 15:05


서버분석 


아래과 같은 라이브러리가 필요합니다.

dependencies {

compile("org.springframework.boot:spring-boot-starter-web")

compile("org.springframework.boot:spring-boot-starter-thymeleaf")

compile("org.springframework.boot:spring-boot-starter-security")

testCompile("org.springframework.boot:spring-boot-starter-test")

}

특이한건 스프링 boot 를 사용했다는건데, 이번 예제를 통해서 저도 처음 boot 를 접해보았는데 설정부분 및 REST 서비스를  굉장히 단순화 시켜 놓았더군요.

 XML  설정파일은 필요없고, 어노테이션을 통해서 설정을 해줍니다. 

(자바 코드를 통한 설정은 스프링 3.1에 거의 완성되었습니다.) 


중요한 2개의 자바 파일을 분석해 보겠습니다.(Application.java 에서 스프링을 실행시켜줍니다.)




1WebSecurityConfiguration 


첫번째 소스를 분석하기전에 기본 지식을 쌓도록 합시다. 

(http://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/ ) 

WebSecurityConfigurerAdapter 

@EnableWebSecurity 어노테이션과  WebSecurityConfigurerAdapter 은 웹기반 보안을 위해 함께 작동 하며  WebSecurityConfigurerAdapter 을 상속받아서 몇줄만 코딩하면 다음과 같은것을 할수있게된다.

  • 우리의 어플리케이션에 어떤 URL 로 들어오던간에 사용자에게 인증을 요구할수있음.
  • 사용자이름과 비밀번호 및 롤기반으로 사용자를 생성할수있다.
  • HTTP Basic 과 폼기반 인증을 가능하게함.
  • 스프링 시큐리티는 자동적으로 로그인페이지,인증실패 url 등을 제공한다.

@EnableWebSecurity @Configuration public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) { auth .inMemoryAuthentication() .withUser("user") // #1 .password("password") .roles("USER") .and() .withUser("admin") // #2 .password("password") .roles("ADMIN","USER"); } @Override public void configure(WebSecurity web) throws Exception { web .ignoring() .antMatchers("/resources/**"); // #3 } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeUrls() .antMatchers("/signup","/about").permitAll() // #4 .antMatchers("/admin/**").hasRole("ADMIN") // #6 .anyRequest().authenticated() // 7 .and() .formLogin() // #8 .loginUrl("/login") // #9 .permitAll(); // #5 } }
  • #1  사용자 이름 “user” 에 대해서 유저ROLE로 인메모리 인증을 허락한다.
  • #2  사용자 이름  “admin” 에 대해 관리자ROLE로  인메모리 인증을 부여한다.
  • #3 “/resources/”  경로 호출은 인증 무시한다. XML 설정에서는 다음과 같다. http@security=none  
  • #4 인증없는 사용자를 포함한 누구한테나  “/signup” and “/about”  경로는  허용한다.
  • #5 “/login” and “/login?error” 경로는 누구한테나 허용
  • #6 /admin/ 로 시작되는 경로는 반드시 관리자 ROLE 에게만 허용된다.
  • #7 모든 남은 URLs 는 인증이 성공해야지만 접근가능하다.
  • #8 자바 세팅을 통해 폼기반인증셋업. 인증은  /login 으로 username/password 가 POST 로 날라올때
        수행된다. 
  • #9  명시적으로 GET /login 이 요청되었을때 로그인페이지를 보여지는게 요구되어진다. 

위의 것은 아래와 같은  기존의  XML based configuration 과 비슷하다.


<http security="none" pattern="/resources/**"/> <http use-expressions="true"> <intercept-url pattern="/logout" access="permitAll"/> <intercept-url pattern="/login" access="permitAll"/> <intercept-url pattern="/signup" access="permitAll"/> <intercept-url pattern="/about" access="permitAll"/> <intercept-url pattern="/**" access="hasRole('ROLE_USER')"/> #7 <logout logout-success-url="/login?logout" logout-url="/logout" /> <form-login authentication-failure-url="/login?error" login-page="/login" login-processing-url="/login" password-parameter="password" username-parameter="username" /> </http> <authentication-manager> <authentication-provider> <user-service> <user name="user" #1 password="password" authorities="ROLE_USER"/> <user name="admin" #2 password="password" authorities="ROLE_USER,ROLE_ADMIN"/> </user-service> </authentication-provider> </authentication-manager>

XML 설정과 몇가지 차이점 

  • #1 , #2 에서 보면 자바설정에서는 ROLE_ 이 없다. 자동적으로 추가해주기때문이다.
  • Java configuration has different defaults URLs and parameters. Keep this in mind when creating custom login pages. The result is that our URLs are more RESTful. Additionally, it is not quite so obvious we are using Spring Security which helps to prevent information leaks. For example:

  • /spring_security_login 대신해서 GET /login 호출하면 로그인 페이지를 연다.
  • /j_spring_security_check 대신해서 POST /login 를 호출하면 인증체크를 한다. 
  • 사용자이름 기본 설정은  j_username 대신해서 username  이다.
  • 패스워드 기본 설정은   j_password 대신해서  password 이다.
  • 자바 설정은  여러가지 요청 URL 에 대하여 동일한 롤에 의해 쉽게  매핑할수있게 해준다. #4 참고 
  • 자바설정은 여분의 쓸모없는 코드들을 제거한다 . intercept-url 같은 경우 중복이 심하다.
  • When mapping HTTP requests using the hasRole method as we did in #6, we do not need to specify “ROLE_” as we would in XML. Again, this is so common of a convention that the hasRole method automatically adds “ROLE_” for you. If you did not want to automatically prefix with “ROLE_”, you could use the “access” method.

실제 코드 

package showcase;

// org.springframework.security 를 사용합니다.

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;


// 스프링 시큐리티 Java Config 설정 (security-context.xml 로 설정할수도있다.)

// configuration 은 springSecurityFilterChain 로 알려진 서블릿 필터를 생성한다. 

// configuration 은 URLs 보호, ID/PWD 검증 같은 모든 보안에 대해서 책임진다.

@Configuration

@EnableWebSecurity


public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {


@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()  // roy , spring 이면 인증 성공, USER 롤 받음

.withUser("roy") 

.password("spring")

.roles("USER");

}


@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().antMatchers("/**")  // 모든 요청에 대해서 USER 롤을 가지고있는지 확인

.hasRole("USER").and().httpBasic();

}

}





둘째. HomeController 

package showcase;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Controller;

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

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

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


@Controller

@RequestMapping("/*")

public class HomeController {


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


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

public String home() {

logger.info("Spring Android Basic Auth");

return "home";    // home.html 호출 해줌

}


@RequestMapping(value = "/getmessage", method = RequestMethod.GET, produces = "application/json")

public @ResponseBody Message getMessage() {

logger.info("Accessing protected resource");  // USER 롤을 가지고있으면 여기에 들어옴

  // response body 로 Message 객체를 JSON 형식으로 보내줌. 

return new Message(100, "Congratulations!", "You have accessed a Basic Auth protected resource.");

}


}







클라이언트 분석 


아래과 같은 라이브러리가 필요합니다.

dependencies {
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.11'
}


안드로이드 코드를 살펴보기전에 간련 기술을 알아봅시다. (아래 링크 참고)


HTTP basic Authentication

HTTP 기본 인증에서는 서버로 아래와 같이 Request Header 를 보낸다.

1
2
3
4
5
6
7
8
9
10
GET /spring-security-http-basic-authentication/secured/mypage HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/20100101 Firefox/17.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Cookie: JSESSIONID=896331E26095C95449516FCBF2E0E93C; __atuvc=28%7C31%2C0%7C32%2C0%7C33%2C215%7C34%2C59%7C35
Authorization: Basic c3JjY29kZXM6cGFzc3dvcmQ=
Note : 'c3JjY29kZXM6cGFzc3dvcmQ=' is Base64 encoded version of 'username:password' i.e. 'srccodes:password'.


실제 코드 

package org.springframework.android.basicauth;

import java.util.Collections;

import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
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.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

/**
* @author Roy Clarkson
*/
public class MainActivity extends AbstractAsyncActivity {

protected static final String TAG = MainActivity.class.getSimpleName();


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity_layout);

// Initiate the request to the protected service
final Button submitButton = (Button) findViewById(R.id.submit);
submitButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
new FetchSecuredResourceTask().execute();
}
});
}

private void displayResponse(Message response) {
Toast.makeText(this, response.getText(), Toast.LENGTH_LONG).show();
}


private class FetchSecuredResourceTask extends AsyncTask<Void, Void, Message> {

private String username;

private String password;

@Override
protected void onPreExecute() {
showLoadingProgressDialog();

// build the message object
EditText editText = (EditText) findViewById(R.id.username);
this.username = editText.getText().toString();

editText = (EditText) findViewById(R.id.password);
this.password = editText.getText().toString();
}

@Override
protected Message doInBackground(Void... params) {
final String url = getString(R.string.base_uri) + "/getmessage";

// HTTP 기본인증헤더를 username , password를 이용하여 생성한다.
HttpAuthentication authHeader = new HttpBasicAuthentication(username, password);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAuthorization(authHeader);
requestHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

// Create a new RestTemplate instance
RestTemplate restTemplate = new RestTemplate();

// JSON 형식 컨버터 설정
restTemplate.getMessageConverters().add(new MappingJacksonHttpMessageConverter());

try {
// 서버의 URL에 데이터를 요청한다.받은데이터는 Message 객체로 변환
Log.d(TAG, url);
ResponseEntity<Message> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<Object>(requestHeaders), Message.class);
return response.getBody();
} catch (HttpClientErrorException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
return new Message(0, e.getStatusText(), e.getLocalizedMessage());
} catch (ResourceAccessException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
return new Message(0, e.getClass().getSimpleName(), e.getLocalizedMessage());
}
}

@Override
protected void onPostExecute(Message result) {
dismissProgressDialog();
displayResponse(result);
}

}

}




Comments