반응형

백기선님의 Spring Boot 레퍼런스 동영상을 보다 보니 레퍼런스도 한번 쭉~~ 살펴볼 필요가 있겠다 싶은 생각이 들었다. 그럼 뭘 볼까? 라고 생각 하다가 Spring Security 를 보기로 결정 했다. 기간을 가지고 보는것은 아니고 그냥 처음부터 차근차근 정리 하면서 읽어보자라고 생각을 했다. 그런데 보다보니 약간 이상한게 있었다.



위 내용은 레퍼런스에 나오는 5.2장 HttpSecurity 부분이다. 이상하다고 생각한 부분은 "However ~~~~~" 단락이다. 대체 뭘 어떻게 한다는 의미인지 알수가 없었다. 대체 커스텀 로그인 페이지를 만들때 뭘 조심하라는건지. Url이 RESTFul 한데 뭘 어쩌라는건지. 그리고 정보 유출을 막기위해 Spring security 를 사용하는것이 뭐가 분명하지 않다는것인지 이해가 가지 않았다. 커스텀 로그인 페이지를 만들면 spring security가 해줄수 있는 영역이 분명치 않다는 의미인지.. 그리고 마지막에 "For example" 이 있는데 왜 정작 그 다음에는 예제에 대한 내용은 없는 건지 알수가 없었다. 


그래서 내용이 궁금하기도 해서 spring security github issue 페이지에 issue를 등록했다. 



이슈 링크: https://github.com/spring-projects/spring-security/issues/5242#event-1584275921


그랬더니 issue가 처리 되었다고 메일이 왔다. 그래서 뭐가 어떻게 되었는지 확인을 해보았다. 일단 현재 spring security 5.0.4 Release의 reference는 변경사항이 없다. 그래서 git 을 확인해봤다. 뭐 이런 저런 내용이 있었다. 



처음에는 내용을 보니 첫 문단에 뭘 drop했다는 건지는 잘 모르겠다. 그런데 다시 넣었고 적절한 방법은 "For example" 을 지우는 거라고 코멘트가 써있었다.

그리고 그 다음에 또 다른 내용이 있었다.

내용을 살펴보면 이렇다. 

그 단락은 Spring Security가 왜 예전의 default URL 과 parameter에서 새로운 것으로 변경했는지 설명하기 위해서 작성한 것이다. 그러데 XML과 Java Config가 동일해서 지금은 더이상 적절하지 않다.

그래서 결론적으로 

Restore Example Default URLs and Parameters List to Remove Default URLs and Parameters Commentary

그 단락을 삭제했다.


그런데 삭제 했다는데 Reference 에는 아직 존재를 한다. 그래서 다시 Spring Security Github 를 뒤졌다. 


Commit 로그가 저렇게 있었다. 그래서 세부 내역을 확인해 보았다.



히스토리를 보니 저렇게 그 부분이 삭제되어 있다. 아마도 다음 레퍼런스에 반영이 되려나 보다. 레퍼런스를 읽다 보니 이런 일도 생기는구나 라는 생각을 했다. 앞으로 차근차근 더 읽어보면서 또 의문나는것이 있으면 자주 이용을 해야겠다.



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

+ Recent posts