반응형


영화를 보고 가슴이 너무 뭉클해서 눈물을 머금었던 적이 언제였지? 아마 가장 최근이었던게 작년에 "4월은 너의 거짓말" 이라는 만화를 우연히 보고 그랬던게 마지막이었던것 같다. 그런데 오늘 이 애니메이션을 보면서 마지막에 나도 모르게 코끝이 찡해왔다. 대사 하나하나와 장면 하나하나에서 나도 모르게 눈물을 흘릴뻔 했다. 그만큼 감동적이고 마음이 따뜻해져왔던 애니메이션이었다. 


미구엘은 뮤지션을 꿈꾸지만 음악 자체를 금지해오는 집안의 성격때문에 그 꿈을 펼칠수가 없었다. 뮤직페스티벌에 나가기위해 기타를 구하려고 전설적인 가수 에르네스토의 기타에 손을 댔다가 "죽은자들의 세상"에 들어가게 된다. 그곳에서 헥터라는 인물을 만나서 공연을 하게 된다. 자신감이 없었던 미구엘은 용기를 얻게 되고 멋진 공연을 펼친다. 하지만 미구엘은 죽은 사람이 아니기 때문에 다시 현실로 돌아가야 한다. 그곳에서 만난 고조할머니 이멜다가 미구엘이 돌아갈수 있도록 축복을 해주지만 노래를 하지 말라는 당부에 미구엘은 다시 "죽은자들의 세상"으로 오게 되고 미구엘은 또다른 축복을 받으려고 전설적인 가수인 에르네스토를 찾아가게 된다. 


좀더 자세히 이야기를 하게 되면 이 스포가 되기 때문에 혹시나 안본 사람이 이글을 읽을까봐 더이상 내용에 대해서는 언급을 안하겠다. 


"죽은자들의 세상"

우리가 명절에 차례를 지내는것 처럼 여기에서도 비슷한 행사가 1년에 한번 열린다. 조상들의 사진을 올려놓고 맛있는 음식들을 정성스럽게 준비해 놓으면 조상들의 영혼이 와서 먹고 간다고 생각을 한다. 미신으로만 생각했던 일들은 정말로 일어나고 있는 일이었다. 이미 세상을 떠난 영혼들은 "죽은자들의 세상" 에 머물고 있는데 1년에 딱 한번 그날만 죽은자들이 산자들이 사는 곳으로 가서 후손들이 차려놓은 음식을 먹을수 있었다. 단, 후손의 기억이나 생각속에 죽은자의 기억이 존재 해야한다. 그래서 "사진" 이 그만큼 중요하다. 산자들이 죽은자에 대해서 더이상 기억을 하지 못한다면 죽은자들은 "죽은자들의 세상" 에서도 사라지게 된다. 


"기억 그리고 추억"

"죽은자들의 세상" 과 관련해서 "기억" 이라는 요소는 아주 중요한 키워드이다. 누군가를 계속해서 기억한다는것은 "죽은자들의 세상" 에서 머무를 수 있는 전제조건이 되기 때문이다. 그리고 이것은 우리에게 살아오면서 소중한 "추억"이라는 것을 환기시켜준다. 아무리 세월이 지나도 가슴 속에 뚜렷하게 기억하고 있는 소중한 "추억"들. 코코에서는 이런 것들이 한대 어우러저 우리에게 감동을 전해준다. 


코코를 보기전에 스치듯 본 감상평에서는 음악이 너무 좋았다는 평이 많았다. 그래서 나 또한 기대를 했고 그 기대는 만족스러웠다. 하지만 분명 말하고 싶은것은 음악이 다인 애니메이션이 아니다라는 것이다. 살아있는 자와 죽은자들의 경계를 "죽은자들의 세상"이라는 하나의 세계로 표현을 했다. 그리고 그 둘을 이어주는 수단으로 "기억"이라는 것을 사용함으로써 우리에게 기발한 상상력에 대한 감탄과 감동을 동시에 주고 있다. 


마지막으로 한가지 아쉬웠던 것은 이 애니메이션을 자막으로 안보고 더빙으로 봤다는 것이다. 아무래도 지후하고 같이 보러 갔기 때문에 자막으로 본다는 것은 무리가 있었다. 그래서 나중에 다시 더빙이 아닌 자막판을 봐야겠다는 생각이 들었다. 



728x90
반응형
반응형

ClassPathResource resource = new ClassPathResource(resourcePath);

File file = resource.getFile();

 

위와 같이 코드를 작성해서 로컬에서 실행했을 때에 아무 문제 없이 동작을 하던 것이 war File 배포한 후에 동작을 시켰을 때에 에러가 발생을 했다.

 

class path resource [img/header.jpg] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/xxxxadmin/lib/xxxxxxxxx.war!/WEB-INF/classes!/img/header.jpg

java.io.FileNotFoundException: class path resource [img/header.jpg] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/xxxxadmin/lib/xxxxxxxxx.war!/WEB-INF/classes!/img/header.jpg

at org.springframework.util.ResourceUtils.getFile(ResourceUtils.java:215)

at org.springframework.core.io.AbstractFileResolvingResource.getFile(AbstractFileResolvingResource.java:52)

 

분명히 존재하는데 찾지를 못하는 거지???

 

https://sonegy.wordpress.com/2015/07/23/spring-boot-executable-jar%EC%97%90%EC%84%9C-file-resource-%EC%B2%98%EB%A6%AC/

 

문제는 소스코드에 있는 resource.getFile() 메소드 때문이었다.

 

war 파일이나 IDE로 application run as로 실행하였다면 실제 resource 파일인 file:// 프로토콜을 쓰기 때문에 File객체를 생성해 줄 수 있지만, executable jar로 실행 했다면 FileNotFoundException이 발생 하게 됩니다.


그래서 아래와 같이 변경해주면 해결이 된다.


InputStream inputStream = classPathResource.getInputStream();

File tempImage = File.createTempFile("temp", ".jpg");

try {

    FileUtils.copyInputStreamToFile(inputStream, tempImage );

} finally {

    IOUtils.closeQuietly(inputStream);

}


728x90
반응형
반응형

2017년 블로그 결산이란것을 해봤다. (http://www.tistory.com/thankyou/2017/tistory/825962)




많이 언급한 것이 "생각", "게임", "에일로이" 라니... ㅋㅋ 호라이즌을 많이 하긴 했나보데. 그런데 "레고"가 없는것은 좀 이상하다. 언급 기준이 태그 기준은 아닌것 같고 실제 글 내용 기준인것 같긴 한데.  생각해보니 2017년 작성한 글 기준이니 레고가 별로 없을 수 있을것 같긴 하다. 


총 65개의 글을 작성했고 2016년보다 31개 더 증가했다. 아마도 블로그에 애드센스 달고 나서부터 좀더 신경을 쓰고 글을 작성해서 그런것 같다. 그리고 공부한 내용들을 조금씩 적어둔것도 한몫 했다. 

글을 쓰는게 쉽지는 않지만 2018년에도 꾸준히 열심히 써봐야겠다.


728x90
반응형
반응형

지후의 크리스 마스 선물로 뭘 사줘야 할까 한참 고민을 했다. 뭘 갖고 싶냐고 물어봤었는데 터닝메카드 에반을 갖고 싶다고 했다. 그런데 이 터니밍메카드 에반이 구하기가 쉽지 않았다. RC 카였는데 특이하게 컨트롤러와 자동차를 따로 팔았다. 이마트에 터닝메카드 에반이 있긴 했는데 정작 무선 컨트롤러가 없어서 결국 포기 했다. 


그래서 선택한 것은 레고 소방차였다. 여러가지 자동차를 갖고 있긴 해서 차종류는 왠만하면 피하려고 했었는데 소방차는 없어서 이것을 사주기로 마음먹었다. 그런데 이 제품이 몰랐는데 나온지 좀 된 제품이었다. 인터넷에서는 찾아보니 해외배송이 많이 나와서 그냥 마트에서 구매를 했다.



소방차와 소방관 2명 그리고 화재현장(?) 으로 구성되어있다. 소방차니깐 사다리가 있는데 우측 그림처럼 높낮이가 조절이 된다. 3만원 정도에 이정도 가성비면 괜찮은 제품인것 같다. 그리고 왼쪽 박스를 자세히 보면 소방관이 뭔가를 불쪽으로 쏘고 있다. 이부분은 처음에는 별로 신경 안썼는데 굉장한(?) 것이 또 포함되어있었다. 그거에 대해서는 마지막에 설명하겠다. ^^



먼저 피규어 2개와 화재현장 모습니다. 왼쪽 소방관의 장비가 정말 인상적이었다. 오른쪽 피규어같이 저 하얀 모자는 전에도 많이 봐서 알았는데 왼쪽 처럼 소방관 헬맷은 처음 봤다. 이렇게 디테일이 살아있을 줄이야. 그리고 지금 가운데에 불이 나고 있는데 저건 고정이 아니다. 불 모양을 올려놓았는데 처음에는 왜 저걸 고정이 안되게 해놨지 라는 의문이 생겼었다. 그런데 거기에는 큰그림이 있었다. (위에 말한것과 관련이 있음)



이게 완성된 모습이다.  어디 자세히 살펴보도록 해보자. 우선 차 에는 사다리가 부착되어있다. 저 사다리는 360도 회전이 가능하다. 그리고 줄에 감겨있는 노란 총같이 생긴게 소방호스를 표현한 것이다. 차량 뒷부분에는 무전기를 부착할수 있다.



사다리를 세운 모습이다. 지금 화면에는 짧아보이는데 저기에서 사다리가 더 올라간다. 지금 사진에서는 사다리를 빼지 않은 상황이다. 생각보다 사다리가 길다. 



반대편에는 삽이 달려있다. 그리고 운전석 뒷부분에는 도끼와 전기텀 같은게 부착되어있다. 실제 소방차에 저런것들이 들어있는지는 잘 모르겠다. 그리고 공구함처럼 무언가를 담을 수 있는 트렁크같은 보관함도 있다.


이렇게 오늘도 소방관 아저씨들은 불을 끄러 출동을 한다. ^^




자 이제 위에서 말한 상자 앞부분을 떠올려보자. 저렇게 화재 현장에 도착한 소방관은 불길을 향해 소방호스로 물을 뿌린다. 그림을 보면 뭔가 발사되고 있다.



소방 호스에서 나가는 물을 이렇게 표현했다. 소방차의 저 상자를 열면 저렇게 파랗고 동그란 레고 부품을 보관할 수 있다. 저 부품을 소방호스 앞부분에 끼우고 위에 볼록 나와있는 부분을 누르면 발사가 된다. 



그래서 직접 동영상으로 찍어봤다. (한손으로 핸드폰 들고 찍으려니 힘들었다. 레고보다 내 손이 좀더 비중이 크게 나오긴 하지만 이해 하시길..) 위에서 내가 말했던 저 불모양이 고정되는게 아니라 저렇게 올려놓을수 있도록 만든 의도도 저렇게 소방호스에서 나오는 물을 발사해서 넘어뜨리도록 하라는 의도였던것 같다. (아닐수도 있지만..)


처음에는 그저 소방차여서 사줘야겠다라는 생각을 했었는데 지후랑 같이 조립하고 나니 이런저런 놀수있는 기능들이 많이 있어서 더 만족 스러웠다. 지후도 저 총이랑 사다리를 정말 맘에 들어했다. 몇번이나 계속해서 반복해서 쏘면서 아주 신나했다. 이 소방차는 나중에 내 모듈러랑 같이 올려놔도 잘 어울릴것 같다. (그게 언제될지 잘 모르겠지만.. ㅠㅠ)



728x90
반응형
반응형

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

거의 모든 테이블에 디폴트로 들어있는 컬럼이고 상당히 중요한(?) 정보이다. 그렇다 보니 도메인에도 항상 똑같은 컬럼이 존재하게 된다. 그런데 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
반응형
반응형


회사에서 최근 제품에 대한 검증을 받게 되었다. 그러다 보니 당연히 보안관련해서 모의해킹도 포함이 되었다. 개발하면서 해킹 관련해서 관심이 없었는데 막아야 하는 입장이 되다보니 이것저것 찾아보기 시작했다. 그래서 "나는 리뷰어다" 선정되어서 책을 고를때 이 책이 바로 맘에 들었다. 



책은 각각의 주제에 대해서 챕터 별로 나눠져 있다. 그리고 그 주제에 대해서 소개 또는 설명을 해주는 형태로 구성이 되어있다. 글 제목에서도 나와있듯이 실제 실무자로 활동한 저자분께서 직접 경험한 이야기와 방법들이 자세히 설명이 되어있다. 내가 이 분야를 많이 알고 있지는 않지만 모의 해킹이라는 내용속에 다양한 사상이 들어있다는 것을 처음 알게 되었다. 내가 생각을 했던 단순히 패킷을 가로채서 정보를 빼내는것 뿐만 아니라 그것을 하기 위해서 생각해야하는 시나리오, 그리고 기술, 또 윤리의식등 생각할게 정말 많은 분야였다. 



그리고 중간중간 이렇게 질문에 대한 답변 형식으로 Q&A 같은 항목이 있어서 책을 읽으면서 또는 관련된 업무를 하면서 나올수 있는 질문에 대한 답변도 들을 수 있다.



3, 4 챕터에서는 모의 해킹이라는 진로에 대해서 해볼수 있는 고민들에 대한 내용을 담아주었다. 이 부분은 모의해킹 관련 직업 뿐만 아니라 직장을 들어가려고 준비하는 취업준비생들, 그리고 이제 막 입사한 신입사원들에게 똑같이 도움이 될수 있을것 같다. 아무리 분야가 다르더라도 분명이 똑같은 고민을 하는 사람들이 많고 그 고민을 해결하기 위해서 수많은 사람들이 멘토를 찾는 것처럼 이 책이 그런 조언을 해줄 수 있을 거라 생각이 든다. 


책 내용 중 이런 내용이 있다.


솔루션 업체들이 "이것은 비현실적인 공격인데요?" 라는 질문을 던질 때 '취약점' 이라는 답을 하고 싶다면, 항상 공격자 입장에서 생각하고 영향도를 고민해봐야 한다.


정말 저 말은 내가 검증받을 때 많이 했던 생각이었다. 이런 비현실적 공격이 있을수 있나요 라는.. 하지만 생각해보면 공격자가 정상적인 경로로 공격할리는 없다는 것을 금방 깨달을 수 있다. 결론은 취약점이고 그로 인한 영향이 크고 작건간에 발생할 여지가 생긴다. 저부분을 읽고 어찌나 부끄럽던지 얼굴이 화끈 거렸다. 


이책은 일부 기술적인 내용이 포함이 되어있지만 모의 해킹에 대한 기술서적은 아니다. 하지만 모의 해킹에 대해서 알지 못하는 사람들에게 충분히 어떤 분야라는 것을 알려줄 수 있는 책이다. 나 또한 제대로 알지 못하는 사람중 하나였고 이 책 덕분에 좀더 잘 이해할 수 있었다. 



(<실무자가 말하는 모의해킹>의 자세한 내용은 한빛미디어 홈페이지에서 확인할 수 있다. )


728x90
반응형
반응형


페이스북에 가입되어있는 그룹중 "책읽는 프로그래머" 에서 "아이들과 함께하는 리뷰어모집"을 하길래 신청을 했는데 선정이 되었다. 보통 리뷰어 모집할때 책 종류부터 보는데 "아주큰 스케치북" 시리즈가마음에 들었다. 종류에 따라서 방법은 다르지만 그중 "아주큰 스케치북 오리기" 를 선택했다. 선택한 이유는 지후가 가위를 너무 투박하게 다루기 때문에 좀더 같이 연습을 해보려고 선택을 했다. 



이 책을 보기위해 준비물이 필요한데 그것은 바로 가위와 풀이다.



책에 반절은 왼쪽 사진처럼 그림을 붙일 수 있는 스케치북이 있고 나머지 뒷부분에는 스케치북에 오려 붙일 종이(?) 들로 구성이 되어있다. 

그래서 뒷장은 그냥 뜯어서 쓰는게 좋다. 그래야 가위로 오리기도 수월해진다. 오른족에 있는 그림들을 점선에 맞게 오려서 왼쪽에 있는 순서대로 풀로 붙이면 된다. 



그럼 이제 시작!! 가위질이 서툴기 때문에 삐뚤삐뚤 난리도 아니다. 하지만 자기가 해보겠다고 이리저리 돌려가면서 가위질을 하는 모습이 참 귀엽다. 약간 아쉬운 점은 저 페이지가 뜯기 쉽게 되어있었으면 좋았을 것 같았다. 책을 들고 오리기에는 불편하기 때문에 페이지를 뜯어야 하는데 그때 좀 아이가 뜯을수 있도록 처리를 했으면 더 좋았을것 같다.



이렇게 탄생한 연못속의 오리. 옆에서 가위로 오리는것을 지켜보고 풀칠하는 것을 약간씩 도와줬다. 첫장이라서 난이도는 가장 쉬웠다. 

잘라야 하는 것도 많지 않고 그림도 수월했다. 뒷장으로 갈수록 오려야 하는 것들이 조금씩 늘어난다. 





이렇게 오리와는 다르게 부품도 여러개가 있고 잘라야 하는것들, 붙여야 하는 곳들도 점점 많아진다. 그래도 아이가 질려하지 않고 잘 앉아서 즐겁게 책을 보니깐 대 만족이었다. 


맨 처음에 책이 생각보다 커서 놀랐다. 책 제목 처럼 거의 스케치북 크기와 비슷한것 같다. 그리고 종이도 두꺼워서 아이가 가위로 자르는데 좀더 쉽게 자를 수 있었다. 아직 끝까지 다 해보지는 못했지만 천천히 같이 해보면서 좋은 시간을 보낼수 있을것 같다.



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 와 연동하면서 삽질을 많이했는데 일단 기록은 해놔야 할것 같아서 메모를 한다.

MacOs에서 RabbitMQ 를 설치하는것은 참~~ 간단하다.


https://www.rabbitmq.com/install-standalone-mac.html


Rabbitmq 홈페이지에 install 가이드를 보면 따라하기 쉽게 되어있다. 설치를 위해서는 brew를 설치 해야한다.


https://brew.sh/index_ko.html


brew 홈페이지에 가보면 설치 방법이 나와있다. 홈페이지에 나온대로


1
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
cs


이렇게 실행을 하면 brew가 설치가 된다. brew는 apt-get  같은 패키지 관리/설치를 해주는 툴이라고 이해하면 될것 같다.


그리고 나서


1
brew install rabbitmq
cs


이렇게 하면 설치는 완료된다.  설치 위치는 /usr/local/sbin 위치에 설치가 된다.

저 디렉토리로 이동후에 ./rabbitmq-server 를 실행하면 정상적으로 작동한다.



그리고 나서 http://localhost:15672/ 로 접속을 하면 된다. 처음 계정은 guest/guest 로 접속을 하면 된다.


guest 로만 접속을 하면 이상하니깐 계정을 하나 더 만들었다.


1
2
./rabbitmqctl add_user rabbitmq rabbitmq1!
./rabbitmqctl set_user_tags rabbitmq administrator
cs


첫번째 명령어는 새로운 계정을 생성하는 명령어이고 두번째 명령어는 생성한 계정에 permission을 할당하는 명령어 이다.

실제 화면에 들어가서 확인해 보면 아래와 같이 나온다.




728x90
반응형

+ Recent posts