반응형

Spring Boot 에서 Properties 를 설정하는 방법에 대해서 알아보자.


우선 Properties 파일을 3개를 만들어 보았다.

src/main/resources 하위에 application.properties, application-server1.properties, application-server2.properties 이렇게 3개의 파일을 만들었다.


application.properties

1
2
3
4
application-name: my applicatoin
spring.output.ansi.enabled=always
logging.level.org.springframework.web=debug
server.port=9000
cs


application-server1.properties

1
server.port=9001
cs


application-server2.properties

1
server.port=9002
cs


일단 이런 상태로 어플리케이션을 실행 시켜서 CommandLineRunner 로 서버 포트랑 application name 을 찍어보았다.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin
c.p.s.SpringPropertiesApplication        : server port : 9000
cs


찍히는 것은 당연히 application.properties 파일에 있는 내용들이 찍혀 나온다.


그럼 이번에는  application.properties  파일에  spring.profiles.active=server1 이라고 입력해보자.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin
c.p.s.SpringPropertiesApplication        : server port : 9001
cs


이번에는 application-server1.properties 에 application-name: my applicatoin 9001 이라고 입력해보자.


1
2
c.p.s.SpringPropertiesApplication        : application Name : my applicatoin 9001
c.p.s.SpringPropertiesApplication        : server port : 9001
cs


지금까지 테스트한 결론은 이렇다.


1. properties 의 profile  을 active 안하면 기본적으로 default properties를 읽는다.

2. active 한 properties 는 default properties의 값들을 override 한다.


In addition to application.properties files, profile-specific properties can also be defined by using the following naming convention: application-{profile}.properties. The Environment has a set of default profiles (by default, [default]) that are used if no active profiles are set. In other words, if no profiles are explicitly activated, then properties from application-default.properties are loaded.

Profile-specific properties are loaded from the same locations as standard application.properties, with profile-specific files always overriding the non-specific ones, whether or not the profile-specific files are inside or outside your packaged jar.


spring boot document 에도 위와 같이 나와있다. 마지막 문장을 번역해보면 다음과 같다.


profile-specific properties 는 기본적인 application.properties 파일처럼 같은 위치에서 로드된다. 그리고 profile-specific 파일들은 이 파일들이 패키지된 jar파일 안에 있거나 밖에 있거나 상관 없이 non-specific  파일들을 항상 오버라이딩 한다. 


일단 내가 이 테스트를 통해서 알고 싶었던 것은 이부분이었다. active 한 프로파일이 아예 기존 properties 파일을 교체하는지 아니면 기존 파일에 있는 값들을 유지 하되 중복되는 값들은 override 하는지 알고 싶었다. 결과는 기존값을 유지하고 중복값은 override  한다였다.


프로젝트를 여러군데 배포하다 보면 배포 환경에 따라서 값들이 변경되는 값들 있다. 편하게 관리를 하려면 profile을 나누는게 확실히 도움이 되는데 항상 우선순위 때문에 헷갈렸다. 물론 properties 파일이 외부에 있는 경우, commandline 으로 들어오는 경우등 여러 가지 케이스가 더 있긴 할테지만 우선 이것으로 내 궁금증은 풀렸다.


프로젝트 소스 : https://github.com/blusky10/study_spring



728x90
반응형
반응형

테스트 케이스를 작성하다가 좀 헷갈리는게 있었다. @Mock, @MockBean 차이가 뭐지??? 쓰긴 하고 있는데 알고 써야 하지 않을까라는 의문이 들었다. 그래서 찾아봤다.

 

먼저 Mock 객체를 선언할 때에는 2가지 방법이 있다.

 

1. 첫번째 : mock() 을 이용해서 선언

 

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
 
    @InjectMocks
    private UserService userService;
    UserRepository userRepository = mock(UserRepository.class);
 
    @Test
    public void findByEmail_test(){
        when(userRepository.findByEmail("test@test.com")).thenReturn(Optional.of(new User()));
        userService.findByEmail("test@test.com");
        verify(userRepository).findByEmail("test@test.com");
    }
}
cs

 

2. 두번째 : @Mock 을 이용해서 선언

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
 
    @InjectMocks
    private UserService userService;
 
    @Mock
    private UserRepository userRepository;
 
    @Test
    public void findByEmail_test(){
        when(userRepository.findByEmail("test@test.com")).thenReturn(Optional.of(new User()));
        userService.findByEmail("test@test.com");
 
        verify(userRepository).findByEmail("test@test.com");
    }
}
cs

 

이 2개의 테스트 케이스는 동일하게 동작한다. 

 

그리고 선언한 Mock 객체를 테스트를 실행하는 동안 사용할수 있게 하기 위한 작업이 있다. 바로 위에서 보이는 @RunWith(MockitoJUnitRunner.class) 가 바로 그 역할을 해준다.  또다른 방법으로는 아래와 같은 메소드를 테스트케이스 실행 전에 실행 시키면 된다. 

 

 

1
2
3
4
@Before 
public void initMocks() {
    MockitoAnnotations.initMocks(this);
}
cs

 

여태껏 아무생각없이 둘다 @RunWith랑 initMocks랑 둘다 썼는데 둘다 쓸 필요가 없었다. ㅡㅡ;

 

그럼 @MockBean 은 뭐지??

 

@MockBean 은 Mock 과는 달리 org.springframework.boot.test.mock.mockito 패키지 하위에 존재한다. 즉 spring-boot-test에서 제공하는 어노테이션이다. Mockito 의 Mock 객체들을 Spring의 ApplicationContext에 넣어준다. 그리고 동일한 타입의 Bean  이 존재할 경우 MockBean으로 교체해준다.

 

그럼 언제 @Mock을 쓰고 언제 @MockBean 을 써야 하나??

 

결론적으로 Spring Boot Container가 필요하고 Bean이 container 에 존재 해야 한다면 @MockBean 을 쓰면 되고 아니라면 @Mock 을 쓰면 된다. 그런데 개인적으로는 그걸 잘 판단을 못하겠다.

 

1. MockitoJUnitRunner 를 이용해서 Mock을 사용한 Testcase.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(MockitoJUnitRunner.class)
public class RegisterControllerMockTest {
 
    @InjectMocks
    private RegisterController registerController;
 
    private ModelAndView model;
 
    @Before
    public void setup(){
        model = new ModelAndView();
    }
 
    @Test
    public void showRegistrationPage_test() throws Exception {
 
        registerController.showRegistrationPage(model, new User());
        Assert.assertEquals("register", model.getViewName());
    }
}
 
cs

 

2. SpringRunner와 @WebMvcTest를 사용해서 @MockBean 을 사용한 Testcase

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RunWith(SpringRunner.class)
@WebMvcTest(value = RegisterController.class, secure = false)
public class RegisterControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean
    private UserService userService;
 
    @MockBean
    private EmailService emailService;
 
    @Test
    public void showRegistrationPage_test() throws Exception {
 
        mockMvc.perform(MockMvcRequestBuilders.get("/register"))
                .andExpect(status().isOk())
                .andExpect(view().name("register"))
                .andDo(print());
    }
}
cs

분명 이 2개의 테스트 케이스 는 결과는 같지만 실제 내부에서 동작하는 부분은 다르다. 2번만 spring applicaiton context 가 올라간다. 그래서 부득이하게 @MockBean 으로 UserService 와 EmailService를 넣어줘야 했다. 1번은 Controller 객체에 대해서 Mock 을 만들어서 그거에 대해서만 테스트를 한다.(뭐라고 표현을 해야할지 잘 모르겠다.)

 

이렇게 같은 컨트롤러에 대한 테스트 이지만 작성하기에 따라서 여러가지 방법으로 테스트 케이스를 만들 수 있다. 아직은 미숙하지만 좀더 많이 작성하다 보면 방법도 많이 알게 되고 실제 구현 코드들의 모습도 많이 나아질거라는 생각이 든다.

 

참고 : https://stackoverflow.com/questions/44200720/difference-between-mock-mockbean-and-mockito-mock

728x90
반응형
반응형

https://spring.io/guides/tutorials/spring-boot-oauth2/


위 사이트에 가면 Spring boot 를 이용해서 Oauth를 이용해서 Login 을 할 수 있는 샘플을 만들어볼 수 있다. 그래서 나도 해봤는데.. 그게 삽질의 시작이었다...

Tutorial 자체는 그렇게 어렵지 않게 따라 할 수 있다. 따라하기가 어렵다면 Git에서 소스를 내려 받아서 해볼 수도 있다.


이제 이 Tutorial 을 진행하기 위해서 Facebook Developer 사이트에서 앱을 등록을 해야 한다. 그래야 Client Id 하고 Client  Secret을 받을 수 있다.


https://developers.facebook.com


위 사이트에 들어 가면 본인의 Facebook 계정으로 앱을 등록할 수 있다.


위 화면에서와 같이 앱ID 와 앱 시크릿 코드 를 받을 수 있는데 이게 바로 Client Id 와 Client Secret으로 사용된다.

그리고 redirect URI를 등록하면 준비는 끝난다. (끝난건줄 알았다...)


이제 샘플을 실행 시켜봤다.


로그인 까지 멀쩡하게 됐는데 error가 딱 나온다.


아니 왜????? 대체 왜 에러가 나오는 거지??? 분명 할것 다 한것 같은데..



로그를 보니 분명 access_token 까지는 잘 가져 왔다. 그런데 https://graph.facebook.com/me 라는 url을 호출 할때 400 error 가 났다. Http request 400 Error 는 그냥 Bad Request(요청이 잘못됐다)라는 의미는 아니다.


400(잘못된 요청): 서버가 요청의 구문을 인식하지 못했다

(출처 : https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C#4xx_(%EC%9A%94%EC%B2%AD_%EC%98%A4%EB%A5%98)


한마디로 요청을 하는 syntax 가 뭔가 잘못됐다는 의미이다.


똑같은 요청을 Postman 으로 보내봤다.


리턴된 메세지에 appsecret_proof argument가 있어야 된다고 나온다.. 이게 뭐지??? client_secret 말고 뭐가 또있나???

appsecret_proof로 그래프 API 호출 인증

그래프 API는 클라이언트 또는 클라이언트를 대신하여 서버에서 호출할 수 있습니다. 서버의 호출은 appsecret_proof라고 하는 매개변수를 추가하여 앱의 보안을 개선할 수 있습니다.

액세스 토큰은 이동 가능합니다. 클라이언트에서 Facebook의 SDK에 의해 생성된 액세스 토큰을 취하여 서버에 보낸 다음, 해당 서버에서 클라이언트를 대신하여 호출할 수 있습니다. 사용자 컴퓨터의 악성 소프트웨어 또는 메시지 가로채기(man in the middle) 공격에서 액세스 토큰을 훔칠 수도 있습니다. 그런 다음 클라이언트나 서버가 아닌 완전히 다른 시스템에서 이 액세스 토큰을 사용하여 스팸을 생성하고 데이터를 훔칠 수 있습니다.

서버의 모든 API 호출에 appsecret_proof 매개변수를 추가하고 모든 호출에 대해 인증서를 요청하도록 설정하여 이를 방지할 수 있습니다. 이렇게 하면 악의적인 개발자가 자신의 서버에서 다른 개발자의 액세스 토큰으로 API를 호출할 수 없게 됩니다. Facebook PHP SDK를 사용하고 있다면 appsecret_proof 매개변수는 자동으로 추가되어 있습니다.

(출처 : https://developers.facebook.com/docs/graph-api/securing-requests?locale=ko_KR)


그런 이유로 access_token 이외에 appsecret_proof를 같이 보내야 한다는 거다.



그런데 이런 설명도 있다. 그래서 저기 보이는  Require App Secret(앱시크릿 코드요청) 에 대한 설정을 안하면 appsecret_proof를 추가하지 않아도 된다는 이야기이다. 그래서 저 설정을 No로 설정하면 된다.


이렇게 해서 Facebook 로그인에 대한 샘플이 제대로 작동하는 것을 확인 할 수 있었다. 

간단하게 끝날줄 알았느데 설정 하나 때문에 온갖 고생을 했다. 


728x90
반응형
반응형

공부하면서 만든 Oauth Server에 대한 테스트를 Postman으로는 했는데 실제로 Client Code가 필요하게 되었다. 


2017/09/04 - [Development/Java] - [Spring Boot]Oauth server 적용해보기


테스트만 할 경우에는 Postman만 써도 상관이 없지만 실제 Client 가 호출을 하려면 code가 필요하다. 그래서 여기저기 구글링을 해가면서 찾아봤다. 


우선 Oauth Token을 발급 받기위한 코드가 필요하다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public String getOAuth2Token(String username, String password) {
        final String CLIENT_ID = "myclient";
        final String CLIENT_SECRET = "secret";
        final String GRANT_TYPE = "password";
        final String SERVER_URL = "http://localhost:" + port;
        final String API_OAUTH_TOKEN = "/oauth/token";
 
        String clientCredentials = CLIENT_ID + ":" + CLIENT_SECRET;
        String base64ClientCredentials = new String(Base64.encodeBase64(clientCredentials.getBytes()));
 
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.add("Authorization""Basic " + base64ClientCredentials);
 
        MultiValueMap<StringString> parameters = new LinkedMultiValueMap<>();
        parameters.add("grant_type", GRANT_TYPE);
        parameters.add("username", username);
        parameters.add("password", password);
 
        HttpEntity<MultiValueMap<StringString>> request = new HttpEntity<>(parameters, headers);
 
        @SuppressWarnings("rawtypes")
        ResponseEntity<Map> response;
 
        URI uri = URI.create(SERVER_URL + API_OAUTH_TOKEN);
        response = restTemplate.postForEntity(uri, request, Map.class);
        return  (String) response.getBody().get("access_token");
    }
cs


위에 작성된 메소드를 보면 파라메터로 id, password를 받게 되어있다. 이건 실제 사용자의 id, password를 넣으면 된다. 그리고 그 이외에 필요한 정보들은 final 상수로 정의를 해놓았다. 다만 Testcase로 작성 하다 보니 port 는 Random하게 들어간다. 호출 형태는 Postman에서 작성했던 것을 그대로 Code로 옮긴걸로 생가하면 된다.  저렇게 호출을 하게되면 결과값으로 access_token값을 받게된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void getOAuth2TokenTest(){
    Assert.assertNotNull(accessToken);
}
 
@Test
public void getTestWithAccessToken(){
    final String SERVER_URL = "http://localhost:" + port;
    final String API_URL = "/private?access_token={access_token}";
 
    ResponseEntity<String> responseEntity = restTemplate.getForEntity(
            SERVER_URL + API_URL,
            String.class,
            accessToken);
 
    Assert.assertEquals("private", responseEntity.getBody());
}
cs


이렇게 받은 access_token을 실제 API 에 넣어서 보내주면 전에 글에서 postman  으로 실행했던 것과 동일한 결과를 얻을 수 있다.

위에 작성된 소스 코드는 https://github.com/blusky10/study_spring 에 가서 OauthServiceTest.java파일을 보면 확인 할 수 있다.


728x90
반응형
반응형

Spring Boot Project에 OauthServer를 설정해보았다.

 

소스는 https://github.com/blusky10/study_spring 의 simple-spring-oauth 브랜치를 다운로드 받으면 된다.

 

 Client 는 private이라는 api에 접근하기 위해서 oauthserver 에 token 발급 요청을 한다.

발급된 token을 가지고 private이라는 api에 접근한다.

(Client 는 미리 등록되어있다고 가정한다. 따라서 Client를 Oauth서버에 등록하는 과정은 생략된다.)

 

1. 먼저 ResourceServer를 설정한다. ResourceServer는 Resource Owner의 정보를 가지고 있는 서버를 의미한다. 

 

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/private").authenticated();
    }
}
cs

 

"/" 주소로 오는 url은 모두 허용하게 되지만 "/private"으로 접근 되는 url은 인증이 필요하다. 

 

2. 두번째로 Authserver를 설정한다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("isAuthenticated()");
    }
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("myclient")
                .authorizedGrantTypes("client_credentials""password")
                .authorities("ROLE_CLIENT""ROLE_TRUSTED_CLIENT")
                .scopes("read""write""trust")
                .resourceIds("oauth2-resource")
                .accessTokenValiditySeconds(500)
                .secret("secret");
 
    }
 
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
}
cs

 

중간에 보이는 client를 설정하는 부분을 보면 "myclient"는 client의 이름을 나타낸다. 그리고 secret은 client에게 발급되는 비밀번호이다. Facebook 과 같은 곳에서 인증을 하게 되면 client를 따로 등록하게 되는데 이때 등록을 하게 되면 Client 고유의 비밀번호를 발급받게 된다. 이 번호는 절대로 노출되어서는 안된다. 지금 내가 만드는 서버는 myclient라는 client가 등록되어있고 그 client 에게 발급된 비밀번호는 secret이라고 생각하면 된다.

접근할수 있는 client의 role은 "client" 이다.

 

 

 

3. AuthenticationManager를 이용해서 userDetailService를 재정의 해준다. 

 

1
2
3
4
5
6
7
8
9
10
    @Autowired
    public void authenticationManager(AuthenticationManagerBuilder builder, AccountService accountService) throws Exception{
 
        builder.userDetailsService(new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                return new CustomUserDetails(accountService.get(username));
            }
        });
    }
cs

 

 

 

 

 

 

 

여기에 Parameter로 받는 AccoutService는 사용자를 조회하는 서비스이다. 그런데 여기에서 UserDetailServiced의 loadUserByUsername 메소드는 UserDetails 를 리턴해야 하기 때문에 UserDetails를 재정의 해줘야 한다. 그게 바로 CustomUserDetails 이다. accountService.get(username) 하면 Account 객체를 리턴해주고 그 Account 객체가 가지고 있는 정보를 UserDatails 에 셋팅해준다.

 

4. CustomUserDetails

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CustomUserDetails implements UserDetails {
 
    private String username;
    private String password;
    Collection<extends GrantedAuthority> authorities;
 
    public CustomUserDetails(Account account) {
        this.username = account.getLoingId();
        this.password = account.getPassword();
 
        List<GrantedAuthority> authorityList = new ArrayList<>();
        for (Role role : account.getRoles()){
            authorityList.add(new SimpleGrantedAuthority(role.getName().toUpperCase()));
        }
 
        this.authorities = authorityList;
    }
    // Getter, Setter 생략
}
cs

 

Custom UserDetails 생성자에서 받은 Account 객체로 username, password, Role 정보를 설정해준다. 

 

이제 Test를 해보자.

 

인증이 필요없는 URL

 

인증이 필요한 URL

 

인증이 필요없는 URL 은 상관 없지만 /private으로 호출을 하면 error 가 나온다. 인증되지 않은 User가 접근을 했기 때문이다.

 

 

/oauth/token URL 로 인증토큰 발급 요청을 한다. 이때 필요한 것이 clientid 와 secret 이다. 내가 위에서 설정한 client id 와 secret을 써준다.

 

 

그리고 Body 에는 grant_type, username, password 를 넣어준다. 

 

 

request를 보내면 이렇게 access_token을 발급 받을 수 있다. 

 

 

이제 발급 받은 Access_token을 private url 뒤에 넣어서 보내면 위에 그림처럼 private이라는 메세지를 볼수 있다. 

 

 

간단한 예제를 만들어 봤다. 좀더 자세한 내용을 보려면 좀더 공부를 해야하고 이론적인 부분도 상당히 많이 알아야 할것이다. 

 

이예제는 아래 Youtube 동영상을 보면서 따라서 만들어본 예제이다. 

 

 

728x90
반응형
반응형

Spring Security 를 적용하는 내용을 처음부터 차근차근 정리를 해보려고 한다. 

목표는 Spring Security 를 공부하면서 각각의 기능들을 적용해보는것이다. 진행하다보면 Spring Security 뿐만 아니라 다른 내용들도 점점 추가될것 같다. 다 만들고 나서는 git에 소스를 공유할 생각이다. ^^;; 언제가 될지는 잘 모르겠다.


환경 : java 1.8, Spring Boot 1.5.3 Release, Maria DB, JPA, gradle


build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
buildscript {
    ext {
        springBootVersion = '1.5.3.RELEASE'
    }
    repositories {
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
 
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
 
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
 
repositories {
    jcenter()
    mavenCentral()
}
 
 
dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.springframework.boot:spring-boot-starter-security')
 
    testCompile('org.springframework.boot:spring-boot-starter-test')
 
    runtime ('org.mariadb.jdbc:mariadb-java-client')
}
cs



아직 초반이어서 라이브러리가 몇개 없다. spring-boot 에서 사용하는 jpa, security, test, web 라이브러리가 끝이다. 그리고 DB를 사용하기 위한 mariadb client까지만 추가되어있다.


도메인.

Account.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Account {
    @Id
    private String loingId;
 
    private String username;
 
    private String password;
 
    private String email;
 
    private boolean enabled;
}
cs


Account 도메인이 있다. 만들기는 더 여러개 만들어놓았는데 우선은 이것만 필요해서 적었다. 위 Account.java 소스에는 getter/setter 메소드는 삭제하고 올렸다. (너무 길어서)


다음은 Account 관련 Service와 Repository 이다. 


AccountRepository.java

1
2
public interface AccountRepository extends JpaRepository<Account, String> {
}
cs


AccountService.java

1
2
3
4
public interface AccountService {
    Account get(String loginId);
}
 
cs


AccountServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
@Service
public class AccountServiceImpl implements AccountService {
 
    @Autowired
    private AccountRepository accountRepository;
 
    @Override
    public Account get(String loginId) {
        return accountRepository.findOne(loginId);
    }
}
cs


Repository를 다이렉트로 호출해도 되긴 하지만 아무래도 구조상 service 레이어가 있는게 맞다고 판단해서 repository는 Service 에서 호출하도록 한번 감쌌다. 지금은 간단히 호출만 하지만 나중에는 로직도 들어갈것이 예상된다.


CustomUserDetailService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class CustomUserDetailService implements UserDetailsService{
 
    @Autowired
    private AccountService accountService;
 
    @Override
    public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
 
        Account account = accountService.get(loginId);
 
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
 
        User user = new User(account.getLoingId(), account.getPassword(), grantedAuthorities);
 
        return user;
    }
}
cs


UserDetailsService에 있는 loadUserByUsername 메소드를 제정의 해서 UserDetails 객체를 정의해준다. UserDetails는 사용자의 인증정보를 담고있다. 위에서 Account Service에서 loginId로 정보를 조회해서 가져온 id, password, 권한(아직 미구현으로 객체만 생성했다) 정보를 user로 생성해주고 있다.




ResourceSecurityConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableGlobalAuthentication
public class ResourceSecurityConfiguration extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private CustomUserDetailService customUserDetailService;
 
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.customUserDetailService);
    }
}
 
cs


WebSecurityConfigurerAdapter에 있는 init 메소드를 사용해서 AuthenticationManangerBuilder에 userDetailService를 내가 새로 만든 CustomUserDetailService를 사용하겠다고 설정해준다.


StudySpringApplication.java

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class StudySpringApplication {
 
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(StudySpringApplication.class);
 
        app.run(args);
    }
}
 
cs


application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
  port: 9000
 
spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/holmes
    username: root
    validation-query: select 1
    test-while-idle: true
    time-between-eviction-runs-millis: 300000
    test-on-borrow: false
 
  jpa:
    database: mysql
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQLDialect
        default_schema: holmes
cs


이제 어플리케이션을 실행해보면 로그인 페이지가 나온다. 





위에 소스와 약간 차이가 있을수 있지만 아래 Git repository 에 가면 소스를 확인할 수 있다. 


https://github.com/blusky10/study_spring


728x90
반응형
반응형


회사에서 Spring boot를 사용하기 시작한지는 한 1~2년 정도 된것 같다. 쓴다기 보다는 Spring 사이트에 있는 소스들을 가져다 붙이는 수준이었다. 체계적으로 공부해본적은 없고 눈앞에 닥치면 찾아서 하다보니 부족한 점이 많이 느껴졌다. 이번에 받은 이 "실전 스프링 부트 워크북"은 그런 부족한 점을 채워줄수 있는 좋은 가이드가 되었다. 


Chapter 1에서 부터 4까지는 Spring Boot를 실습하기 위한 준비 단계정도로 볼수 있다. 기본적인 이론과 설명들, 프로젝트 구성에 대해서 소개를 해주고 있다. 그리고 Chapter 5부터 본격적으로 Spring Boot를 가지고 Web 어플리케이션을 만들기 시작한다. 



특히 Chapter 6 을 보면 Spring Boot Test 에 대해서 설명을 하고 있는데 책을 읽을 당시 회사에서 Spring Boot Test에 대한 내용을 한참 구글링 하던 시기였다. 내가 개발중인 코드에 대한 Controller Test case를 어떻게 작성을 해야 하나 고민을 하던 중이었는데 책의 내용들이 내게 많은 도움이 되었다. 아마도 이 책이 없었으면 코드가 뭐가 뭔지도 모를 코드들을 가져다가 썼을것이다. 




그리고 이 책의 좋은 점이 실제 작성된 코드에 대해서 중요한 부분에 대한 설명들이 많이 있다는 점이다. 다른 Spring 관련 책들도 소스 코드에 대한 설명들이 있기는 하지만 너무 추상적이거나 어렵게 설명한 책들이 많다. 하지만 이책에서는 적어도 내 기준에는 각각의 소스 코드에 대한 설명들이 이해하기가 쉬웠다. 그리고 개발관련 서적의 딱딱함이 덜 하다는 느낌이 들었다. 


기본적인 Spring Boot 에 대한 설명부터 시작해서 security, 메세징등 기본적으로 알아야 할 기능들은 잘 설명해 놓은 책이다. 물론 이거 한권으로 Spring Boot에 대한 모든 기능을 마스터 할수는 없지만 기본기를 다지기에는 아주 좋은 책이라고 생각이 든다. 단지 아쉬운 점은 이 책에도 중간중간 언급이 되어있지만 지금 사용하고 있는 Spring Boot 최신 버전과는 약간 차이가 있을 수 있다는 점이다. 책에서는 1.3.3 Release 버전을 사용하고 있는데 현재 Spring Boot 최신 버전을 1.4를 넘어 1.5, 2.0을 바라보고 있다. 이부분에 대한 것만 제외 한다면 Spring Boot에 관심 있는 분들에게 한번쯤 읽어보라고 추천해주고 싶다. 


728x90
반응형
반응형

테스트케이스를 만들어서 작업을 하면 소스코드가 수정될 경우 코드를 테스트 해보기가 참 수월하다. 그런데 이 테스트 케이스 작성하는게 생각보다 만만치는 않다. 

실제 DB 를 읽어서 테스트를 해야 하는지. 아니면 Mock 객체를 정의를 해서 사용을 해야 하는지. 실제 DB 를 사용할 경우 저장된 data 가 변경이 되어서 구현했을 당시 테스트 케이스는 Pass였지만 나중에 빌드 시점에 테스트 케이스가 실행될 경우에 Fail 이 나면 어떻게 할것인지. 

생각해보면 그냥 서비스 구현해서 화면 띄우고 버튼 눌러서 테스트 하는것이 더 편할지도 모른다는 생각이 들기도 한다. 

작성할 때마나 서비스 테스트,  repository테스트, 컨트롤러 테스트에 대해서 구글링 하면서 작성을 하다보니 뭔가 남는게 없는것 같아서 샘플을 한번 만들어보기로 했다. 

 

최근에 필요하기도 했고 나중에 또 써먹을 일도 있을것 같아서 Controller 테스트 케이스를 작성한 것을 공유해 본다.

 

각각의 구성은 아래와 같이 되어있다. 

(java : 1.8, SpringBoot : 1.5.3)

 

Book.java

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long bookId;

    private String title;

    // Getter & Setter 생략
}

 

BookRepository.java

public interface BookRepository extends JpaRepository<Book, Long>{}

BookService.java

public interface BookService {
    Book getBook(Long id);
}
 

BookServiceImpl.java

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookRepository bookRepository;

    @Override
    public Book getBook(Long id) {
        return bookRepository.findOne(id);
    }
}
 

BookController.java

@RestController
public class BookController {

    @Autowired
    private BookService bookService;

    @RequestMapping(value = "/book/{bookId}", method = RequestMethod.GET, produces = "application/json")
    public Book getBook(@PathVariable Long bookId){
        return bookService.getBook(bookId);
    }
}

/book/{bookId} 라는 url 로 request 를 보내면 bookId 에 맞는 Book 객체를 리턴해주면 되는 형태이다. 테스트 케이스 없이 테스트 하려면 톰캣으로 띄워놓고 실제로 화면에서 위에 정의한 서비스를 호출하는 컴포넌트를 클릭해서 정상 동작을 하는지 확인해봐야한다. 그러다가 소스에 글자라도 하나 틀리면 수정한다음에 다시 톰캣 재기동을 하는 번거로운 작업을 진행해야 한다. 

 

 

 

 

 

 

 

 

 

 

이런 번거로움을 피하기 위해 테스트 케이스를 작성해 보았다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class BookControllerTest {

    private MockMvc mockMvc;

    @MockBean
    BookController bookController;

    @Before
    public void setup(){
        mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();
    }


    @Test
    public void getBookTest() throws Exception {
        given(this.bookController.getBook(new Long(1)))
                .willReturn(new Book("Homes"));

        mockMvc.perform(get("/book/{bookId}", 1))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$['title']", containsString("Homes")))
                .andDo(print());
    }
}

BookController를  MockBean으로 정의를 해주었다. 그리고 BookController 의 getBook메소드에 파라메터가 1이 들어왔을 때 리턴 받는 결과를 미리 정의한다. (18~19 라인) 그리고 화면에서 요청하는 것처럼 Request를 수행해준다.  perform에 있는 파라메터를 보면 get 메소드를 호출하게 되며 파라메터로 1값을 넣어서 실행을 한다. OK 응답을 받게 되고 리턴 받는객체의 title이 "Homes"  인지 비교를 한다. 19라인에서 책 이름을 미리 Homes  로 정의 했기때문에 테스트는  Pass가 된다. 마지막에 andDo(print()) 는 실제 수행된 로그가 콘솔창을 통해 볼수 있도록 처리해 준것이다.

 

처음에 만들때는 좀 삽질을 하긴 했지만 만들고 보니 앞으로 자주 써먹을것 같다. 앞으로도 바쁘지만 테스트케이스를 만들면서 코드 작성을 하도록 해야겠다.

 

참고로 위 소스를 작성한 gradle.build 파일은 아래와 같다.

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')

    runtime('group:com.h2database:h2');

    testCompile('group:com.jayway.jsonpath:json-path')

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

 

728x90
반응형

'Development > Java' 카테고리의 다른 글

[OAuth] Oauth의 간략한 흐름.  (0) 2017.07.04
[Spring Security]간단 Spring Security  (0) 2017.06.27
[Spring]Jasypt 를 이용한 properties 암호화  (6) 2017.04.25
[SpringCloud]Spring Config..  (0) 2016.01.26
spring Cache  (0) 2015.12.05
반응형


프로젝트 내부에는 설정파일들이 많이 있다. 대표적인 항목이 DB 접속 정보가 있다. 

그런데 이 접속정보에는 ID, PASSWORD 가 항상 존재 한다. ID는 상관이 없지만 PASSWORD 정보가 파일 내부에 평문으로 적혀있으면 외부에 노출될 위험이 있다. 그래서 암호화를 해야 한다. 


Jasypt를 이용하면 이런 항목들을 쉽게 암호화 할 수 있다. 



먼저 라이브러리를 다운로드 받는다.


http://www.jasypt.org/download.html


사이트에 들어가보면 상단에 DOWNLOAD JASYPT 라는 링크가 있다. 그 걸 누르면 라이브러리를 다운로드 받을 수 있다. 

이 글을 쓰는 시점의 버전은 1.9.2 이다. 


다운로드 한다음 사용 방법은 간단하다. 압축을 푼후에 bin 폴더로 이동한다. 


그리고 콘솔창(윈도우cmd 창)에서 아래와 같이 입력한다. 




encrypt input="password" password="pwkey" algorithm="PBEWITHMD5ANDDES" 



input 항목에는 실제 사용하고 있는 패스워드를 입력하면 되고 password 항목에는 암호화된 값을 복호화 할때 사용되는 key 값을 넣으면 된다. 임의로 정해서 넣으면 된다.


명령어 실행 결과.



----ARGUMENTS-------------------

algorithm: PBEWITHMD5ANDDES

input: password

password: pwkey

----OUTPUT----------------------

oQV892dssDi5Xzs9tQoVuaqyRaFa7Za5 



여기에서 보이는 OUTPUT 항목이 실제 암호화 된 값이 된다.


이제 암호화 된 값 생성까지는 완료가 됐고 실제 spring 프로젝트에 적용을 하면 된다.


먼저 Jasypt 라이브러리르 추가한다. 


Mvn repository 에 가서 jasypt 검색을 하면 정말 많이 나온다.  최신버전으로 찾아서 넣으면 되긴 하는데 여기에서 주의할 점이 있다. 


최신버전의 라이브러를 추가하고 어플리케이션을 run 했을때 version  관련 오류가 날수가 있다. 이경우에는 현재 Spring 버전이 jasypt 최신버전과 호환이 안되던지 아니면 jdk 버전이 호환이 안되던지 둘중 하나이다. 실제로 jasypt 라이브러리 depency를 보면 spring 4.3.8 Release 와 dependency 가 되어있다. 그래서 주의를 해야 한다.


나 같은 경우는 실제로 버전 충돌때문에 build.gradle 에 아래와 같이 추가를 했다. (spring boot 버전도 1.3.8 아래였고 jdk 버전도 7이었다.)


compile('com.github.ulisesbocchio:jasypt-spring-boot-starter:1.4-java7')




spring 설정 파일에 아래와 같이 정의를 한다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <bean id="encryptorConfig" class="org.jasypt.encryption.pbe.config.EnvironmentStringPBEConfig">
        <property name="algorithm" value="PBEWithMD5AndDES" />
        <property name="password" value="pwkey" />
    </bean>
     
    <bean id="encryptor" class="org.jasypt.encryption.pbe.StandardPBEStringEncryptor">
        <property name="config" ref="encryptorConfig" />
    </bean>
     
    <bean class="org.jasypt.spring3.properties.EncryptablePropertyPlaceholderConfigurer">
        <constructor-arg ref="encryptor" />
        <property name="locations">
            <list>
                <value>classpath:/properties/db.properties</value>
            </list>
        </property>
    </bean>
cs


그리고 마지막으로 설정 파일의 password 값을 암호화 한 값으로 넣어준다. 반드시 ENC라고 쓰고 괄호안에 값을 넣어야 한다.


1
datasource.password=ENC(EywTY3v00EbqKyxlLzkjag==)
cs


이렇게 하면 password 값을 읽어들일때 암호화 된 값을 읽어서 자동으로 복호화 한다.


Spring boot 일 경우에는 아래와 같이 설정해주면 된다.


먼저 Encryptor 를 정의한 class를 만들어준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class PropertyEncryptConfiguration {
 
   @Bean
   static public StandardPBEStringEncryptor stringEncryptor() {
      StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
      EnvironmentPBEConfig config = new EnvironmentPBEConfig();
      config.setPassword("pwkey");
      config.setAlgorithm("PBEWITHMD5ANDDES");
      encryptor.setConfig(config);
      return encryptor;
   }
}
cs



그리고 main class 에 @EnableEncryptableProperties 를 추가해준다. 그럼 설정이 마무리 된다. 


암호화 하는 과정이 번거롭다면 그 부분만 따로 테스트 케이스를 만들어 놓는것도 좋은 방법이다.


728x90
반응형

'Development > Java' 카테고리의 다른 글

[Spring Security]간단 Spring Security  (0) 2017.06.27
[Spring]Controller Test 하기  (0) 2017.06.12
[SpringCloud]Spring Config..  (0) 2016.01.26
spring Cache  (0) 2015.12.05
Spring propagation  (0) 2015.12.01

+ Recent posts