[Spring Security] Authentication 메커니즘 및 Custom Filter 실습

WOOK JONG KIM·2022년 11월 29일
0

패캠_java&Spring

목록 보기
75/103
post-thumbnail

Authentication

Authentication 는 인증된 결과만 저장하는 것이 아니고, 인증을 하기 위한 정보와 인증을 받기 위한 정보가 하나의 객체에 동시에 들어 있다.

-> 인증을 제공해줄 제공자(AuthenticationProvider)가 어떤 Authentication에 대해서 허가를 내줄 것인지 판단하기 위해서는 직접 입력된 Authentication을 보고 허가된 Authentication을 내주는 방식이기 때문

-> AuthenticationProvider 는 처리 가능한 Authentication에 대해 알려주는 support 메소드를 지원, authenticate() 에서 Authentication을 입력값과 동시에 출력값으로도 사용

Authorities에는 '어디를 갈 수 있는지', '어떤 역할을 할 수 있는지'에 대한 권한 정보로 이들이 구현(implement)한 GrantedAuthority 인터페이스에 관한 정보들이 저장되어있다

  • Authentication에 들어가 있는 Token들은 각각의 Filter들을 통해 발행된 Token들이다.
  • Credentials은 인증을 받기 위해 필요한 정보들로 대표적인 예시로는 비밀번호가 있다.
  • Principal은 인증된 결과에 대한 정보이다. credentials를 이용해 인증을 받고 그 결과
  • Details은 위의 정보 외의 인증에 관여된 주변 정보들을 갖고 있다.

Authentication Provider

  • Authentication을 제공하는 것을 Authentication Provider
  • Authentication Provider는 credentials나 Principal이 있는 Authentication을 받아서 인증을 하고 인증된 결과를 다시 Authentication객체로 전달하는 역할
  • 이때 Authentication Provider는 어떤 인증에 대해 승인을 해줄지 Authentication Manager에게 알려줘야 하기 때문에 support()라는 메서드를 제공
  • 인증 제공자(Provider)도 여러개 올 수 있다 -> 인증 대상과 방식이 다양할 수 있음

커스텀 필터

  • StudentAuthenticationToken 과 TeacherAuthenticationToken 을 각각 Authentication 으로 한다.
  • StudentManager와 TeacherManager 를 각각 AuthenticationProvider 로 구현

문제점

  • UsernamePasswordAuthenticationFilter 가 해주던 일을 직접 구현해야 함

  • CustomLoginFilter 를 쓸 경우 successHandler와 failureHandler 를 별도로 구현

  • default 페이지와 caching 된 request 페이지로 redirect 하는 기능도 직접 구현
    -> 현재로서는 CustomLoginFilter 와 UsernamePasswordAuthenticationFilter 를 동시에 사용하는 것이 가장 현명한 대안

  • 가능한 UsernamePasswordAuthenticationFilter 를 재활용하는 방안을 강구하는 것이 바람직

코드 예시

Controller

@Controller
public class HomeController {

    @GetMapping("/")
    public String index(){
        return "index";
    }

    @GetMapping("/login")
    public String login(){
        return "loginForm";
    }

    @GetMapping("/login-error")
    public String loginError(Model model){
        model.addAttribute("loginError", true);
        return "loginForm";
    }

    @GetMapping("/access-denied")
    public String accessDenied(){
        return "accessDenied";
    }

    @ResponseBody
    @GetMapping("/auth")
    public Authentication auth(){
        return SecurityContextHolder.getContext().getAuthentication();
    }
}
@Controller
@RequestMapping("/student")
public class StudentController {

    @PreAuthorize("hasAnyAuthority('ROLE_STUDENT')")
    @GetMapping("/main")
    public String main(){
        return "StudentMain";
    }

}
@Controller
@RequestMapping("/teacher")
public class TeacherController {

    @PreAuthorize("hasAnyAuthority('ROLE_TEACHER')")
    @GetMapping("/main")
    public String main(){
        return "TeacherMain";
    }
}

Student

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
// principal 즉 인증 대상을 만들기
public class Student {

    private String id;
    private String username;

    //Authentication 인증을 하려면 GrantedAuthority 필요
    private Set<GrantedAuthority> role;
}

StudentAuthenticationToken

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
// 사이트에 들어올 학생이 가지게 되는 일종의 통행증( 인증 토큰), Authentication의 구현체
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();
    }
}

StudentManager(ProductManager)

// 통행증을 발급할 매니저
// component이기에 initalizingBean 사용 가능
// StuentManager가 AuthenticationProvider가 되어 Hong이라는 ID의 사용자가 오면 Student를 담은 Authentication Token
// 즉 통행증을 발행해 주겠다
// Authentication의 Manager에 등록하기 위해 SecurityConfig에 코드 작성
@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {

    // 매니저가 가지는 학생 리스트
    private HashMap<String, Student> studentDB = new HashMap<>();


    // token을 StudentAuthenticationToken으로 발급하겠다
    // 인자로는 UserNameFilter를 거친 Authentication이 도착하고
    // 이를 StudentAuthenticationToken(Authenciation 구현체) 형태로 return
    @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; // 내가 처리할 수 없는 Authentication, false로는 X
    }

    // UsernamePasswordAuthenticationFilter 형태의 토큰을 처리할수 있다고 Authentication Manager에게 알려줌!!
    // 검증을 해주는 Provider 역할을 하겠다
    // 이후 커스터마이징한 토큰 처리하게 하였음
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == StudentAuthenticationToken.class;
    }

    // 빈이 초기화 되었을 때 StudentDb에 저장할 학생 목록
    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
                new Student("kang", "강아지", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
                new Student("rang", "호랑이", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")))
        ).forEach(s ->
                studentDB.put(s.getId(),s)
        );
    }

}

Teacher은 Student와 구조 비슷

Security Config

@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final StudentManager studentManager;

    private final TeacherManager teacherManager;


    public SecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
        this.studentManager = studentManager;
        this.teacherManager = teacherManager;
    }


    // Authentication Provider 등록
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
        auth.authenticationProvider(teacherManager);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());

        http
                .authorizeRequests(request->
                        request.antMatchers("/", "/login").permitAll()
                                .anyRequest().authenticated()
                )
//                .formLogin(
//                        // login페이지를 통해 UsernameAuthenticationFilter가 동작하도록
//                        login->login.loginPage("/login").permitAll()
//                                .defaultSuccessUrl("/", false)
//                                .failureUrl("/login-error")
//                )
                // 커스터 마이징한 필터 위치 추가
                // 만약 위 코드 주석 처리를 풀면 두개가 동시에 동작, 밑에 코드만으로는 에러및 성공 핸들링 불가
                // 주석 풀고 동작시 에러 및 성공 Handling이 가능
                .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class )
                .logout(logout->logout.logoutSuccessUrl("/"))
                .exceptionHandling(e -> e.accessDeniedPage("/access-denied"))
                ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                ;
    }
}

Custom Login Filter

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 : "";

        // 선생인지 학생인지
        String type = request.getParameter("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);
        }
    }
}

Authentication Manager

  • 인증 제공자들을 관리하는 인터페이스가 AuthenticationManager (인증 관리자)이고, 이 인증 관리자를 구현한 객체가 ProviderManager

  • ProviderManager 도 복수개 존재 가능

  • 개발자가 직접 AuthenticationManager를 정의해서 제공하지 않는다면, AuthenticationManager 를 만드는 AuthenticationManagerFactoryBean 에서 DaoAuthenticationProvider 를 기본 인증제공자로 등록한 AuthenticationManage를 만듬

  • DaoAuthenticationProvider 는 반드시 1개의 UserDetailsService 를 발견할 수 있어야 한다. 만약 없으면 InmemoryUserDetailsManager 에 [username=user, password=(서버가 생성한 패스워드)]인 사용자가 등록되어 제공

실제 개발시에는 Provider Manager 개발하는 경우 X

  • UserDetails 및 UserDetailsService를 만들어서 Bean으로 제공 해주면 DaoAuthenticationProvider라는 Default Provider가 Details Service를 가져다가 인증 제공을 위임하여 진행 함
profile
Journey for Backend Developer

0개의 댓글