본문 바로가기
Spring/Spring Security

authentication 메커니즘

코동이 2021. 10. 8.

 

* 인증 ( Authentication )

 

 

 Authentication은 사이트에서 "통행증" 역할을 한다. Authentication을 구현하는 객체들은 대체로 메서드 이름 맨 뒤에 AuthenticationToken이 붙는다. 그래서 구현체들을 인증토큰이라고 부른다. 

 

Credentials : 인증을 위해 필요한 정보(input, 비밀번호)

Principal : 인증된 결과, 인증대상(output)

Details : 기타 정보, 인증 관련 주변 정보들(ip, session 관련)

Authorities : 권한 정보들

(권한을 담당하는 GrantedAuthority를 구현한 객체들을 Authorities에 넣어둔다. )

 

Authentication 객체는 SecurityContextHolder를 통해 세션이 있건 없건 언제든 접근할 수 있도록 필터체인에서 보장해준다.

 

 Authentication에는 인증 결과를 저장할 뿐만 아니라, 인증을 위한 정보도 들어있다. 왜냐하면, 인증 제공자(AuthenticationProvider)에게 Authentication이 넘겨져 인증 절차를 거쳐야 하기 때문이다. AuthenticationProvider는 인증을 검사 할 Authentication대상을 알려주는 supports()를 지원한다. supports()에 자신이 담당하는 인증토큰 방식을 정의하여 내가 담당한 인증토큰을 찾는다. authenticate()에서는 Authentication을 입력값과 출력값으로 사용하여 검증한다.

 

 

* 인증제공자 ( AuthenticationProvider )

 

 

 인증 제공자(AuthenticationProvider)는 supports()로 자신이 인증 검사 할 인증 토큰을 정한다. 실제 검증 시에는, Authentication을 받아서 credentials을 검증하고 Principal Authentication을 리턴한다. input, output 모두 Authentication인데 일반적으로 인증을 위해 input되는 것은 credentials이고 인증 후 output 되는 것은 Principal이다. (혹은 input으로 Principal이 들어올 수도 있으며 확인하고 리턴한다.)

 

 인증 제공자는 어떤 인증을 확인할지 인증 관리자(AuthenticationManager)에게 알려줘야 한다. supports() 메서드로 검사한다. 인증 대상과 방식이 다양할 수 있기 때문에 인증 제공자도 여러개 존재한다.

 

 

* 인증 관리자 ( Authentication Manager )

 

 

인증 제공자를 관리하는 인터페이스가 인증관리자(AuthenticationManger) 인터페이스이고, 인증관리자를 구현한 객체가 ProviderManager이다. ( 복수개 존재 가능 )

 

개발자가 인증관리자(AuthenticationManager)를 직접 구현하지 않는다면, AuthenticationManager를 만드는 AuthenticationManagerFactoryBean에서 DaoAuthenticationProvider를 기본 인증제공자로 등록하여 AuthenticationManager를 만든다.

 

DaoAuthenticationProvider는 반드시 1개의 UserDetailsService를 가져야 한다. 만약 없으면 InmemoryUserDetailsManager에 [username = user, password=(서버가 생성한 패스워드)]인 사용자가 제공된다.

 

일반적으로 실무에서 AuthenticationManager를 직접 정의하는 경우는 없다. UserDetails를 구현한 객체를 만들고 UserDeatilsService에 제공하면 DaoAuthenticationProvider가 인증 제공자에 위임해서 처리하기 때문이다.

 

 

 

* Authentication 구현하는 인증토큰 만들기

 

@Data
@Builder
public class StudentAuthenticationToken implements Authentication {
    private Student principal;
    private String credentials;
    private String details;
    private boolean authenticated;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return principal == null ? new HashSet<>() : principal.getRole();
    }

    @Override
    public String getName() {
        return principal == null ? "" : principal.getUsername();
    }
}

 

 

* 인증 제공자의 인증

 

@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {
    private final Map<String, Student> studentDB = new HashMap<>();

 

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == UsernamePasswordAuthenticationToken.class;
    }

 

 

UsernamePasswordAuthenticationToken 인증토큰을 발급받기 위해서 AuthenticationManager는 인증을 처리 할 AuthenticationProvider을 찾는다. 현재 정의된 supports()의 의미는 UsernamePasswordAuthenticationToken 를 내가 처리하겠다는 의미이다. 인증제공자에게 권한을 위임하도록 boolean으로 검사해서 true이면 해당 인증토큰을 진행한다.

 

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        if(studentDB.containsKey(token.getName())) {
            Student student = studentDB.get(token.getName());
            return StudentAuthenticationToken.builder()
                    .principal(student)
                    .credentials(null)
                    .details(student.getUsername())
                    .authenticated(true)
                    .build();
        }

        return null;
    }

 

AuthenticationManager는 성공적으로 authenticate()가 완료되면 StudentAuthenticationToken 인증토큰을 발급한다.

 

    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_USER"))),
                new Student("gang", "강호동", Set.of(new SimpleGrantedAuthority("ROLE_USER"))),
                new Student("bong", "봉태호", Set.of(new SimpleGrantedAuthority("ROLE_USER")))
        ).forEach(s ->
                studentDB.put(s.getId(), s));
    }

 

InitializingBean를 구현하여 afterPropertiesSet()으로 계정을 만든다.

 

 

* 인증 만들기

 

    private final StudentManager studentManager;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
    }

 

로그인 시, UsernamePasswordAuthenticationToken 타입의 토큰인증을 발급한다.

 

{
  "principal": {
    "id": "hong",
    "username": "홍길동",
    "role": [
      {
        "authority": "ROLE_USER"
      }
    ]
  },
  "credentials": null,
  "details": "홍길동",
  "authenticated": true,
  "name": "홍길동",
  "authorities": [
    {
      "authority": "ROLE_USER"
    }
  ]
}

 

*TeacherManager를 로그인 계정에 추가하기

 

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
        auth.authenticationProvider(teacherManager);
    }

 

 

 인증매니저가 계정을 쌓아두고 있기 때문에, 여러개를 추가해도 된다. 검사 방식은 쌓아둔 계정을 비교해본다. 선생님 아이디로 로그인을 하면, StudentManager의  authenticate()를 갔다가 null을 반환하고, TeacherManager의 authenticate에서 계정을 확인하고 로그인 정보를 리턴한다. 

 

* formLogin으로 UsernamePasswordAuthenticationFilter 필터를 사용하고 있는데, UsernamePasswordAuthenticationToken를 커스터마이징하고 싶다면?

 

 

UsernamePasswordAuthenticationFilter 을 상속해서 내부 구현을 바꿔주면, 인증방식을 커스터마이징 할 수 있다. attemptAuthentication이 토큰을 만드는 메서드이기 때문에 해당 메서드를 재정의해서 원하는대로 변경한다.

 

public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {
    public CustomLoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

 

로그인 시도 할 때, UsernamePasswordAuthenticationFilter가 인증을 거치는 과정 중에 핵심을 가져와서 인증토큰을 만드는 부분이다. (참고로 생성자에 AuthenticationManager를 추가해주는 것을 잊지 않는다.!)

 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());

        http
                .authorizeRequests(request->
                        request
                                .antMatchers("/", "/login").permitAll()
                                .anyRequest().authenticated()
                )
//                .formLogin(
//                        login -> login.loginPage("/login")
//                                .permitAll()
//                                .defaultSuccessUrl("/", false)
//                                .failureUrl("/login-error")
//                )
                .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
                .logout(logout -> logout.logoutSuccessUrl("/"))
                .exceptionHandling(e -> e.accessDeniedPage("/access-denied"));
    }

 

 기존의 formLogin 대신에 커스터마이징한 filter를 통해서 로그인을 제어할 수 있다.  addFilterAt()CustomLoginFilter를 이용한다. ( 구현 내용을 보면 UsernamePasswordAuthenticationToken 방식을 이용하는 것은 그대로 한다.)  request에 "/" 뿐만 아니라, "/login"을 추가해서 루트 페이지와 로그인 페이지는 어떠한 권한 상태에서도 접속할 수 있게 해주어야 한다.

 

 

 *선생님, 학생 각 토큰 인증방식을 사용하기(필터 커스터마이징)

 

UsernamePasswordAuthenticationToken의 토큰 인증을 쓰지 않고 다른 토큰 인증방식을 사용할 수 있다. attemptAuthentication 메서드를 커스터마이징 하면 된다. 각 StudentManagerTeacherManager가 허용하는 인증토큰을 검사하는 supports() 함수를 그대로 같이 변경하면 된다. (기존에는 return authentication == UsernamePasswordAuthenticationToken.class; 를 사용하고 있는 상태였다.)

 

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";

        String type = request.getParameter("type");
        System.out.println(type);
        if(type == null || !type.equals("teacher")) {
            //student
            StudentAuthenticationToken token =
                    StudentAuthenticationToken.builder().credentials(username).build();

            return this.getAuthenticationManager().authenticate(token);
        } else {
            //teacher
            TeacherAuthenticationToken token =
                    TeacherAuthenticationToken.builder().credentials(username).build();

            return this.getAuthenticationManager().authenticate(token);
        }
    }

 

UsernamePasswordAuthenticationToken 실제 구현 내용을 이용해서 커스터마이징 한 메서드이다. type을 검사해서 teacher인 경우 TeacherAuthenticationToken을 만들고, 아닌 경우 StudentAuthenticationToken을 만든다.

 

 

* Student

 

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == StudentAuthenticationToken.class;
    }

 

StudentAuthenticationToken.class 의 인증토큰인 경우에 인증을 진행한다.

 

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        StudentAuthenticationToken token = (StudentAuthenticationToken) authentication;
        if(studentDB.containsKey(token.getCredentials())) {
            Student student = studentDB.get(token.getCredentials());
            return StudentAuthenticationToken.builder()
                    .principal(student)
                    .credentials(null)
                    .details(student.getUsername())
                    .authenticated(true)
                    .build();
        }

        return null;
    }

 

studentDB에 token의 credential 정보가 있는지 확인해서 있으면 StudentAuthenticationToken을 만들어 리턴한다.

 

 

*Teacher

 

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == TeacherAuthenticationToken.class;
    }

 

TeacherAuthenticationToken.class 의 인증토큰인 경우에 인증을 진행한다.

 

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        TeacherAuthenticationToken token = (TeacherAuthenticationToken) authentication;
        if(teacherDB.containsKey(token.getCredentials())) {
            Teacher teacher = teacherDB.get(token.getCredentials());
            return TeacherAuthenticationToken.builder()
                    .principal(teacher)
                    .credentials(null)
                    .details(teacher.getUsername())
                    .authenticated(true)
                    .build();
        }

        return null;
    }

 

teacherDB에 token의 credential 정보가 있는지 확인해서 있으면 TeacherAuthenticationToken을 만들어 리턴한다.

 

 

 문제는 기존에 있는 인증토큰을 사용하지 않고 커스터마이징한 인증토큰을 사용할 때 리다이렉트 주소와 로그인 실패 주소를 사용할 수 없다는 것이다. formLogin을 사용하면 로그인 성공 시 리다이렉트를 해주는 기능을 이용할 수 있다.

 

        http
                .authorizeRequests(request->
                        request
                                .antMatchers("/", "/login").permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(
                        login -> login.loginPage("/login")
                                .permitAll()
                                .defaultSuccessUrl("/", false)
                                .failureUrl("/login-error")
                )
                .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
                .logout(logout -> logout.logoutSuccessUrl("/"))
                .exceptionHandling(e -> e.accessDeniedPage("/access-denied"));
반응형

'Spring > Spring Security' 카테고리의 다른 글

UserDetailsService  (0) 2021.10.11
BaiscToken으로 웹/모바일 로그인 인증 구현하기  (0) 2021.10.11
BasicAuthentication 인증  (0) 2021.10.11
basic login 과정  (0) 2021.10.08
간단한 로그인 페이지 만들기  (0) 2021.10.07