반응형

내 github 에 있는 프로젝트들중 예전에 만들었던 건데 내용좀 다시 볼겸 수정을 하고 있었다. 

사실, 간단한거 빼고는 잘 작동을 안하는것 같다.

 

그래서 이번에 라이브러리 버전 업데이트 좀 할겸 소스를 수정을 했다.

 

Spring Boot 버전도 최신 버전으로 수정을 하고 Security 버전도 수정을 했다. 그리고 나서 간단히 로그인을 해보려고 하니 다음과 같은 에러가 발생했다.

 

There is no PasswordEncoder mapped for the id "null"

 

음.. ?

찾아보니 Spring Security 버전이 올라가면서 PasswordEncoder가 변경되면서 발생한 에러였다. 

The general format for a password is:

{id}encodedPassword

Such that id is an identifier used to look up which PasswordEncoder should be used and encodedPassword is the original encoded password for the selected PasswordEncoder. The id must be at the beginning of the password, start with { and end with }. If the id cannot be found, the id will be null. For example, the following might be a list of passwords encoded using different id. All of the original passwords are "password".

(출처 : https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released)

 

그래서 방법은 다음과 같다. (내가 가지고 있는 소스 기준이다. )

 

1. DB 에 password 값을 변경한다. 

 

생성되는 password 값에 "{noop}" 을 붙여서 저장을 한다. 

 

2. UserDetail 서비스를 새로 구현 하였다면 password 부분을 수정해준다. 

    public CustomUserDetails(Account account) {
        this.username = account.getLoginId();
        this.password = "{noop}" + account.getPassword();
    }

 

1번이나 2번이나 결과적으로 password 값 앞에 "{id}" 값을 넣어주면 된다는 의미이다.

 

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

기존 소스에 동시 로그인을 제어하기 위해서 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
반응형

+ Recent posts