본문으로 바로가기

스프링-시큐리티 인증 절차 정리

category SPRING/스프링-시큐리티 2021. 6. 30. 04:15
728x90
반응형
SMALL
본 글은 스프링 시큐리티의 인증 절차와 시큐리티의 클래스들의 역할에 대해 정리 하는 글이다

 

1. 시큐리티에서 인증과 권한이란?

인증 절차를 거친후 권한 절차를 진행한다.

1) 인증: 사용자가 본인이 맞는지를 확인하는 절차

2) 권한: 인증된 사용자가 요청된 자원에 접근권한 여부를 판단하는 절차

 

2. 시큐리티 인증 방식

credential 기반 인증: 아이디+비밀번호 를 이용하여 인증

-principal: 아이디

-credential: 비밀번호

 

3. 시큐리티 주요 모듈

1) SecurityContextHolder, SecurityContext, Authentication

이 세가지 클래스는 시큐리티의 주요 컴포넌트로서

시큐리티에서 방금담은 Authentication을

1.SecurityContext 에 보관한다.

2.SecurityContext 을 SecurityContextHolder에 담아 보관한다

 

2) 위의 해당과정을 시큐리티에서 인증 처리코드로 본다면

1. username(아이디)과 password를 조합해서 UsernamePasswordAuthenticationToken 인스턴스를 만듬

2. 만들어진 인스턴스는 검증을 위해 AuthenticationManager 인스턴스로 전달됨

3. AuthenticationManager 의 인증이 성공하면 Authentication 인스턴스를 리턴(AuthenticationProvider 로)

4. Authentication은 SecurityContextHolder.getContext().setAuthentication(~~)을 set으로 넣어줌

AuthenticatoinManger의 추상메소드 authenticate의 반환값이 Authentication 인걸 확인 할수 있으며

Authentication을 확장하고 있는 AbstractAuthenticationToken 을 확인하면 인증 과정을 확인할 수 있다.

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
 
package org.springframework.security.authentication;
 
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
 
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
 
 
 
public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
 
    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;
 
 
    public AbstractAuthenticationToken(Collection<extends GrantedAuthority> authorities) {
        if (authorities == null) {
            this.authorities = AuthorityUtils.NO_AUTHORITIES;
            return;
        }
 
        for (GrantedAuthority a : authorities) {
            if (a == null) {
                throw new IllegalArgumentException(
                        "Authorities collection cannot contain any null elements");
            }
        }
        ArrayList<GrantedAuthority> temp = new ArrayList<>(
                authorities.size());
        temp.addAll(authorities);
        this.authorities = Collections.unmodifiableList(temp);
    }
 
 
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }
 
    public String getName() {
        if (this.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) this.getPrincipal()).getUsername();
        }
        if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
            return ((AuthenticatedPrincipal) this.getPrincipal()).getName();
        }
        if (this.getPrincipal() instanceof Principal) {
            return ((Principal) this.getPrincipal()).getName();
        }
 
        return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
    }
 
    public boolean isAuthenticated() {
        return authenticated;
    }
 
    public void setAuthenticated(boolean authenticated) {
        this.authenticated = authenticated;
    }
 
    public Object getDetails() {
        return details;
    }
 
    public void setDetails(Object details) {
        this.details = details;
    }
 
 
    public void eraseCredentials() {
        eraseSecret(getCredentials());
        eraseSecret(getPrincipal());
        eraseSecret(details);
    }
 
    private void eraseSecret(Object secret) {
        if (secret instanceof CredentialsContainer) {
            ((CredentialsContainer) secret).eraseCredentials();
        }
    }
 
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof AbstractAuthenticationToken)) {
            return false;
        }
 
        AbstractAuthenticationToken test = (AbstractAuthenticationToken) obj;
 
        if (!authorities.equals(test.authorities)) {
            return false;
        }
 
        if ((this.details == null&& (test.getDetails() != null)) {
            return false;
        }
 
        if ((this.details != null&& (test.getDetails() == null)) {
            return false;
        }
 
        if ((this.details != null&& (!this.details.equals(test.getDetails()))) {
            return false;
        }
 
        if ((this.getCredentials() == null&& (test.getCredentials() != null)) {
            return false;
        }
 
        if ((this.getCredentials() != null)
                && !this.getCredentials().equals(test.getCredentials())) {
            return false;
        }
 
        if (this.getPrincipal() == null && test.getPrincipal() != null) {
            return false;
        }
 
        if (this.getPrincipal() != null
                && !this.getPrincipal().equals(test.getPrincipal())) {
            return false;
        }
 
        return this.isAuthenticated() == test.isAuthenticated();
    }
 
    @Override
    public int hashCode() {
        int code = 31;
 
        for (GrantedAuthority authority : authorities) {
            code ^= authority.hashCode();
        }
 
        if (this.getPrincipal() != null) {
            code ^= this.getPrincipal().hashCode();
        }
 
        if (this.getCredentials() != null) {
            code ^= this.getCredentials().hashCode();
        }
 
        if (this.getDetails() != null) {
            code ^= this.getDetails().hashCode();
        }
 
        if (this.isAuthenticated()) {
            code ^= -37;
        }
 
        return code;
    }
 
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append(": ");
        sb.append("Principal: ").append(this.getPrincipal()).append("; ");
        sb.append("Credentials: [PROTECTED]; ");
        sb.append("Authenticated: ").append(this.isAuthenticated()).append("; ");
        sb.append("Details: ").append(this.getDetails()).append("; ");
 
        if (!authorities.isEmpty()) {
            sb.append("Granted Authorities: ");
 
            int i = 0;
            for (GrantedAuthority authority : authorities) {
                if (i++ > 0) {
                    sb.append(", ");
                }
 
                sb.append(authority);
            }
        } else {
            sb.append("Not granted any authorities");
        }
 
        return sb.toString();
    }
}
 
 
cs

 

3)로그인한 사용자의 정보 얻기

인증정보를 SecurityContext에 넣어줫기때문에 사용자의 정보를 이런식으로 얻을수 있게된다.

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

SecurityContextHolder 의 메소드들을 쭉 따라가보면 Authentication 인터페이스에 getPrincipal() 추상 메소드가 있는것을 확인 할 수 있다.

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
 
 
package org.springframework.security.core;
 
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
 
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
 
 
public interface Authentication extends Principal, Serializable {
     been authenticated. Never null.
         Collection<extends GrantedAuthority> getAuthorities();
 
    Object getCredentials();
    
    Object getDetails();
 
    Object getPrincipal();
 
    
    boolean isAuthenticated();
 
 
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
 
cs

 

4)Spring Security Filter Chain

DelegatingFilterProxy

시큐리티를 적용하기 위해선 처음으로 web.xml 에 filter 설정을 등록한다.

1
2
3
4
5
6
7
8
9
   <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
 
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
cs

모든 요청(/*)을 가로채서 시큐리티 필터를 적용 시킨다(보안적용) 는 뜻으로 이해하자

Spring Security Filter는 A -> B -> C 의 Chain 형태이다. 

아래 그림과 같이 요청에 대해 다음과 같인 filter들이 순차적으로 일을 수행한다.

보통 form 형식을 사용하여 로그인이 시작되면서 시큐리티가 시작된다.

form 형식이 사용되면 UsernamePasswordAuthenticationFilter 클래스에서 인증절차가 시작된다. 해당 클래스 안에 doFilter 메소드를 참고하자

이 과정을 커스텀 하게되면 /member/customLogin 경로에서 입력된 유저정보들은 시큐리티를 타게되고 인증을 위해서 provider 에 선언된 커스텀 클래스의 customUserDetailsService 로 가게된다.

<bean id="customUserDetailsService" class="org.yoon.security.CustomUserDetailsService" /

<security:http>
	<security:form-login login-page="/member/customLogin"/>
</security:http>


<!-- authentication-manager:사용자의 인증처리 -->
<security:authentication-manager>
	<security:authentication-provider user-service-ref="customUserDetailsService">
		<security:password-encoder ref="bcryptPasswordEncoder "/>
	</security:authentication-provider>
</security:authentication-manager>

 

해당 구조도를 보면서 파악하면 좀더 쉬울것이다

 

 

직접 구현한 customUserDetailsService 의 내용을 보면 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
//CustomUser 에서 변환된 값을 리턴
@Log4j
 
public class CustomUserDetailsService implements UserDetailsService {
 
    @Setter(onMethod_=@Autowired )
    private MemberMapper memberMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //시큐리티에서 username은 실제 유저 아이디로 인식
        log.warn("유저정보 로드: "+username);
        MemberVO vo=memberMapper.read(username);
 
        log.warn(vo);
        return vo==null ? null : new CustomUser(vo);
    }
 
}
 
cs

 

5) UserDetails

인증절차(Authentication)이 성공하면 UserDetails는 인증정보를 SecurityContextHolder에 저장하기 위한 객체를 만든다.  

그리고 접근권한인 authorities를 가져오는 메소드도 확인할 수 있다.

 

6) UserDetailsService

위에 커스텀된 클래스를 다시 참고해서 보면

사이트 내에 정보가 없다면 인증객체를 만들어주는 과정을 볼수있다.

인증객체의 존재여부를 통해 다시 커스텀된 CustomUser 로 반환되는것을 알수 있는데

User를 상속하고있는 직접 커스텀 한 클래스에서의 인증객체 생성 인스턴스를 확인 할 수 있다.

7) GrantedAuthority

principal 에 주어진 권한이다. 보통 role 이라고 하며 GrantedAuthority 객체는 UserDetailsService에 의해 로드된다.

8) 권한에 의한 핸들링 처리

UserDetailsService를 상속하는 직접 커스텀한 CustomDetailsService의 반환타입인 CustomUser 를 확인하면 MemberVO를 파라미터로 받아 인증객체를 생성하고있으며 MemberVO 안에는 AuthVO 로 접근권한을 설정할 수있는 필드값을 확인할 수 있다.

실제 DB의 auth 접근권한 컬럼값에는 값이 들어가있는것을 확인할 수있는데

만약 값이 null이거나 조건문을 통해서 권한에 따라 핸들링 할수 있게된다.

xml설정

<bean id="customAccessDenied" class="org.yoon.security.CustomAccessDeniedHandler" />
<bean id="customLoginSuccess" class="org.yoon.security.CustomLoginSuccessHandler" />
<bean id="customUserDetailsService" class="org.yoon.security.CustomUserDetailsService" />
<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />

<security:http>
	<security:form-login login-page="/member/customLogin"/>
	<security:access-denied-handler ref="customAccessDenied" />
	<!-- remember-me 체크박스 선택하면 로그인 기억 기능 -->
	<security:remember-me data-source-ref="dataSource" token-validity-seconds="604800" />
	<!-- 로그아웃 처리와 LogoutSuccessHandler post 방식으로만 작동함  로그아웃시 자동로그인쿠기,was 발행 쿠기 삭제-->
	<security:logout logout-url="/member/customLogout"
	invalidate-session="true" delete-cookies="remember-me, JSESSION_ID" />
</security:http>


<!-- authentication-manager:사용자의 인증처리 -->
<security:authentication-manager>
	<security:authentication-provider user-service-ref="customUserDetailsService">
		<security:password-encoder ref="bcryptPasswordEncoder "/>
	</security:authentication-provider>
</security:authentication-manager>

</beans>

AuthenticationSuccessHandler 를 직접 구현하고 있으며

직접 DB의 AUTH 값에 대한 조건문을 작성하여 권한처리를 직접 핸들링을 할수 있게된다.

CustomUser에서 getAuth 값이 null이라면 AccessDeniedHandler 를 구현하여 시큐리티가 알아서 에러페이지를 핸들링 할수 있는 커스텀 까지 가능하다

 

참고:https://flyburi.com/584 https://sjh836.tistory.com/165

728x90
반응형
LIST