반응형

테이블을 설계할때 항상 빠지지 않고 들어있는 컬럼이 있다. 바로 생성일자, 수정일자, 생성자, 수정자 컬럼이다.

거의 모든 테이블에 디폴트로 들어있는 컬럼이고 상당히 중요한(?) 정보이다. 그렇다 보니 도메인에도 항상 똑같은 컬럼이 존재하게 된다. 그런데 spring-data-jpa에 재미난 기능이 있다. 바로 생성일자, 수정일자, 생성자, 수정자 컬럼에 값을 자동으로 넣어주는 기능이다. 


Audit : 감시하다.


바로 spring-data-jpa 에서는 audit 기능을 제공하고 있다. 방법은 여러가지가 있다. 각 도메인에 @PrePersist, @PreUpdate 등을 붙여서 할수도 있다. 그런데 이게 단점이 각각의 도메인에 같은 컬럼이 정의되어있다는 것은 변하지 않는다는 것이다. 모든 도메인에 똑같이 createDate, modifyDate, createUser, modifyUser가 있어야 한다. 그래서 이런 부분을 피하기 위해 다른 방법을 택했다.


1
2
3
4
5
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
public class AuditableDomain extends AbstractAuditable<Account, Long> {
    private static final long serialVersionUID = 1L;
}
cs


위 소스에 보면 @EntityListenersAuditingEntityListener를 넣어줬다. 그리고 지금 구현한 Class는  AbstractAuditable을 상속받고 있다. AbstractAuditable 클래스의 Account는 실제 User 정보를 담고 있는 Domain 이고 Long은 그 도메인의 Key 값을 의미한다. 

AbstractAuditable 클래스의 필드를 보면 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@MappedSuperclass
public abstract class AbstractAuditable<U, PK extends Serializable> extends AbstractPersistable<PK> implements
        Auditable<U, PK> {
 
    private static final long serialVersionUID = 141481953116476081L;
 
    @ManyToOne
    private U createdBy;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
 
    @ManyToOne
    private U lastModifiedBy;
 
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
 
    .. 중략
}
cs


이렇게 createBy, createDate, lastModifedBy, lastModifiedDate 필드를 가지고 있기 때문에 더이상 하위 Domain 에서는 이 필드들을 정의할 필요가 없어진다. 그리고 다시 AbstractAuditable  클래스는 AbstractPersistable을 상속받고 있다. 보면 PK 를 정의하고 있다.


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
@MappedSuperclass
public abstract class AbstractPersistable<PK extends Serializable> implements Persistable<PK> {
 
    private static final long serialVersionUID = -5554308939380869754L;
 
    @Id @GeneratedValue private PK id;
 
    /*
     * (non-Javadoc)
     * @see org.springframework.data.domain.Persistable#getId()
     */
    public PK getId() {
        return id;
    }
 
    /**
     * Sets the id of the entity.
     * 
     * @param id the id to set
     */
    protected void setId(final PK id) {
        this.id = id;
    }
 
    .. 중략
}
cs


결론적으로 이 AbstractPersistable 클래스에서 PK 를 정의하고 있기 때문에 각각의 도메인들은 더이상 id를 정의할 필요가 없어진다. 

그런데 내가 이걸 테스트 하다보니 문제가 생겼다. 

사용자 정보를 담은 도메인인 Account 에는 id를 String 으로 loginId 라는 이름으로 넣어놨다. AbstractPersistable  클래스에서 정의하는 ID 랑 다르기 때문에 @AttributeOverride  를 써서 재정의를 해보았는데 이상한 현상이 발생했다. DB 테이블에 loginId 말고 id라는 컬럼이 따로 생기고 각각의 필드들도 id랑 loginId랑 2개씩 생기는 현상이 발견됐다. createById, createByLoginId 이런 형태로 2개씩 생겼다. 

그리고 두번째로 ID를 set을 할 수가 없다. 그렇다 보니 ID를 정의할 수 없어서 결국 domain의 Id는 @generateValue 가 가능한 Long 형태로 변경했다. 혹시라도 다시 방법을 찾게 된다면 다시 포스팅을 하겠다..


1
2
3
4
5
6
7
8
9
@EnableJpaAuditing
@Configuration
public class AuditingConfig {
 
    @Bean
    CustomAuditorAware auditorAware() {
        return new CustomAuditorAware();
    }
}
cs

두번째로 해야할 일은 Config 등록이다. @EnableJpaAuditing을 선언해주고 JavaConfig로 등록해준다. 그런데 이때에 createDate, modifyDate는 상관이 없는데 createBy, modifiedBy 같은 경우는 대상이 필요하기 때문에 재정의가 필요하다. 


1
2
3
4
5
6
7
8
9
10
public class CustomAuditorAware implements AuditorAware<Account>{
 
    @Override
    public Account getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (null == authentication || !authentication.isAuthenticated()) {
            return null;
        }
        return (Account) authentication.getPrincipal(); }
}
cs


그래서 이렇게 getCurrentAuditor method를 재정의 해준다. 지금 작성하고 있는 프로젝트는 spring security 가 적용된 프로젝트이기 때문에 SecurityConextHolder에서 정보를 가져와서 보여준다. 


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
35
36
37
38
(SpringRunner.class)
@SpringBootTest(classes = StudySpringApplication.class)
public class AccountServiceTest {
 
    private static Logger logger = LoggerFactory.getLogger(AccountServiceTest.class);
 
 
    @MockBean
    private CustomAuditorAware customAuditorAware;
 
    @Autowired
    private AccountService accountService;
 
    @Autowired
    private RoleService roleService;
 
    @Before
    public void setup(){
        Account sessionAccount = accountService.get("admin");
        Mockito.when(customAuditorAware.getCurrentAuditor()).thenReturn(sessionAccount);
    }
 
    @Test
    public void createAccount(){
        Account newAccount = new Account();
        newAccount.setLoginId("admin1");
        newAccount.setUsername("admin user");
        newAccount.setPassword("admin1!");
        newAccount.setEmail("admin@spring.com");
        newAccount.setEnable(true);
 
        accountService.create(newAccount, null);
 
        Account account = accountService.get("admin1");
        Assert.assertEquals("admin1", account.getLoginId());
        Assert.assertNotNull(account.getCreatedBy());
    }
}
cs

결과를 확인해보기 위해 Test 코드를 돌려보면 잘 적용이 되는것을 확인 할수 있다.

728x90
반응형
반응형

기존 소스에 동시 로그인을 제어하기 위해서 Maxsession 설정을 넣어보았다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class ResourceSecurityConfiguration extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/""/login/**","/browser/**""/error/**").permitAll()
                .antMatchers("/private/**").authenticated()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new LoginSuccessHandler())
                .failureHandler(new LoginFailureHandler())
                .and()
                .logout().permitAll()
                .and()
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true);
    }
}
cs


위 설정주에 보면 sessionManagement가 추가되어있다. 그리고 maximunSessions가 1로 설정되어있고 maxSessionPreventsLogin 이 true 로 설정되어있다. 

maximunSessions : Session 허용 개수

maxSessionPreventsLogin : true 일 경우 기존에 동일한 사용자가 로그인한 경우에는 login 이 안된다. false 일경우는 로그인이 되고 기존 접속된 사용자는 Session이 종료된다. false 가 기본이다.

이렇게 설정하고 나서 동시로그인을 해보았다.



처음 로그인 한 경우에는 잘 된다.



두번째 동일한 계정으로 접속을 할 경우에는 이런 메세지가 나온다. 

(물론 이 화면은 내가 LoginFailureHandler에서 페이지를 설정해놓았기 때문에 저런 화면이 나온다.)



실제로 Log를 보면 org.springframework.security.web.authentication.session.SessionAuthenticationException: Maximum sessions of 1 for this principal exceeded 라는 Exception 이 발생한 것을 볼수 있다.




실제로 어디에서 Exception이 발생하는지 궁금해서 Debug를 찍어보았다. 여러면 돌려보면서 확인 한 결과 AbstractAuthenticationProcessingFilter 에서 발생을 했다. 이 Filter에 보면 sessionStrategy.onAuthentication 이라는 메소드를 호출한다. 이 메소드를 따라가 보면 CompositeSessionAuthenticationStrategy 클래스에서 다시 ConcurrentSessionControlAuthenticationStrategy 클래스로 다시 Delegating 된다. ConcurrentSessionControlAuthenticationStrategy 의 onAuthentication 메소드는 아래와 같이 구현되어있다.


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
35
36
37
public void onAuthentication(Authentication authentication,
            HttpServletRequest request, HttpServletResponse response) {
 
        final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
                authentication.getPrincipal(), false);
 
        int sessionCount = sessions.size();
        int allowedSessions = getMaximumSessionsForThisUser(authentication);
 
        if (sessionCount < allowedSessions) {
            // They haven't got too many login sessions running at present
            return;
        }
 
        if (allowedSessions == -1) {
            // We permit unlimited logins
            return;
        }
 
        if (sessionCount == allowedSessions) {
            HttpSession session = request.getSession(false);
 
            if (session != null) {
                // Only permit it though if this request is associated with one of the
                // already registered sessions
                for (SessionInformation si : sessions) {
                    if (si.getSessionId().equals(session.getId())) {
                        return;
                    }
                }
            }
            // If the session is null, a new one will be created by the parent class,
            // exceeding the allowed number
        }
 
        allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);
}
cs


4번 라인 : sessionRegistry 에서 현재 인증된 Principal과 동일한 session을 모두 가져오게 된다.  

7번 라인 : 먼저 로그인했던 session 이 있기 때문에 sessionCount 는 1이 된다.

8번라인 : allowedSessions 는 sessionManagement의 maxSession이 1로 설정했기 때문에 1이다. 


그래서 20번 라인의 if문을 통과해서 for문을 수행하게 되는데 sessionId가 다르기 때문에 그냥 빠져나오고 36 라인의 allowableSessionsExceeded가 실행된다.


allowableSessionsExceeded 메소드 안에는 아래와 같은 구문이 있는데 결론적으로 아래 조건문이 만족하게 되고 Exception을 throw 하게 된다.


1
2
3
4
5
6
if (exceptionIfMaximumExceeded || (sessions == null)) {
    throw new SessionAuthenticationException(messages.getMessage(
            "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
            new Object[] { Integer.valueOf(allowableSessions) },
            "Maximum sessions of {0} for this principal exceeded"));
}
cs



728x90
반응형
반응형

Spring Boot에서 RabbitMQ 를 연결을 해보자.

원래는 AWS에 있는 Ubuntu 서버에 설치를 한후 연결을 하려고 했다. 그런데 설치까지는 문제가 없이 잘 진행 됐는데 이상하게 SpringBoot에서 연결 하려고 하면 Connection Refuse가 계속 나와 몇일을 보냈다. port를 안연것도 아니고, management 콘솔은 15672포트로 잘만 접속 되는데 5672포트로 연결 하려고만 하면 안됐다. 오늘까지 하다가 안되면 GG 치려고 했다. 실제로 GG 치고 로컬에 깔고 진행을 했다.

그래서 정상 작동하는거 보고 혹시나 해서 다시 AWS에 있는 Rabbit MQ에 접속을 시도해 봤는데 됐다... 왜 되는거지???? --_--;;


대체 이것때문에 쌩쑈를 몇일을 했는데. 어쨌든 되니깐 좋다. 하아..


Rabbitmq 설치 방법은 홈페이지를 찾아보거나 MAC일 경우 내가 작성한 포스트를 보면 된다.


2017/10/27 - [Development/DataBase] - [RabbitMQ] MacOS에 RabbitMQ 설치


application.properties 파일에 접속 정보를 추가해준다.

1
2
3
4
5
myqueue=spring-study
spring.rabbitmq.host=localhost
spring.rabbitmq.username=rabbitmq
spring.rabbitmq.password=rabbitmq1!
spring.rabbitmq.port=5672

cs


Consumer.java

1
2
3
4
5
6
7
8
9
@Component
public class Consumer {
    private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
 
    @RabbitListener(queues = "${myqueue}")
    public void handler(String message){
        logger.info("consumer....> " + message);
    }
}


cs


Consumer는 RabbitListener에 정의 되어있는 Queue를 감시하다가 메시지가 들어오면 handler로 전해준다.


Producer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class Producer {
    private static final Logger logger = LoggerFactory.getLogger(Producer.class);
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    public void sendTo(String routingKey, String message){
        logger.info("send message....");
 
        this.rabbitTemplate.convertAndSend(routingKey, message);
    }
 
}

cs


Producer에서는 routingKey 와 message 를 받는다. routingKey 가  Queue 이름이 된다. 메세지는 Exchange로 전송되며 Exchange는 Queue로 실어나른다.



StudyApplication.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Value("${myqueue}")
String queue;
 
@Bean
Queue queue(){
    return new Queue(queue, false);
}
 
@Autowired
Producer producer;
 
@Bean
CommandLineRunner sender(Producer producer){
       return args -> {
           producer.sendTo(queue, "Hello !!!");
    };
}
 
@Scheduled(fixedDelay = 500L)
public void sendScheduleMessage(){
    producer.sendTo(queue, "Message Delevery : " + new Date());
}
cs


RabbitMQ Management 에 들어가서 Connection, Channel, Queue를 확인 하면 아래처럼 나온다.





이거 하나 테스트 해보려고 대체 몇일을 날린줄 모르겠다. 여기에서는 간단히 설명했지만 RabbitMQ 도 Queue와 Exchange에 대한 내용들에 대한 정의를 알아야할 부분이 많다. 시간내서 한번 개념을 확실히 잡고 가야겠다.


소스 : https://github.com/blusky10/study_spring


(예제 내용은 "실전스프링워크북" 에 있는 내용을 실습한 내용입니다.) 


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
반응형
반응형

Oauth 2.0 에 대해서 공부를 하다가 용어에 대한 명확한 이해가 필요해서 정리를 했다.


Access Token : 보호된 리소스에 일정 기간동안 접근할 수 있는 권한을 가진 문자열

- Scope : Token 소유자가 접근할수 있는 보호된 리소스 셋


Client : Resource를 이용하려고 하는 Web 이나 App. 

Resource Sever : 실제 정보를 가지고 있는 대상. 

Resource Owner : Resource 에 대한 소유자.


Access Token 을 얻는 절차는 아래처럼 설명할 수 있다.


Resource Owner 

 Client

 Resource Server

 1. Client 에게 정보 요청


 

 

 2. Resource Server 에 있는 Resource Owner의 정보를 접근 할수 있는 권한 요청

 

3. Resource Server 에 로그인

(Client 에서 Resource 서버에 로그인할수 있는 페이지를 바로 연결해줌)

 


 4. Resource Server에 있는 Resource Owner의 특정 정보에 Client가 접근하는것을 허용하는지 여부 확인 (Yes/No)

 

 

 

 

 5. Resource Owner 가 Client의 접근을 허용할 경우 Client 에게 Code를 보냄

 

 6. Code를 받아서 Client_id, secret를 code와 함께 Resource Server로 보냄

 
  

 7. Client가 보낸 client_id, secret, code를 확인한 후 access tockent을 발급

 

 8. 발급받은 access token으로 Resource Server 에 있는 owner 의 정보에 접근.

 


위 표에서 설명한 것은 Access Token을 얻는 방법중 한가지에 대해서 설명한 것이다. 꼭 이것과 동일하지 않을 수 있다. 그렇지만 저런 한가지 흐름을 알아 두면 다른것을 이해하기에 도움이 될거라 생각이 된다. 


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
반응형
반응형

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

실제 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