[Spring Security] Custom Token and custom Filter

spring_security

학생과 선생님 로그인 구현 #


map

문제점 #

※ 본 코드는 깃헙 페이지에서 확인할 수 있습니다.


Custom Token #

* Principle #

Student에게 토큰을 발행하기 위하여 principle를 만든다.

login-custom-filter.src.main.java

package com.sp.fc.web.student;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String id;
    private String username;
    private Set<GrantedAuthority> role; // GrantedAuthority 구현
}

* AuthenticationToken #

그리고 같은 디렉토리에 학생들이 로그인하여 받을 통행증인 StudentAuthenticationToken 객체를 구현한다.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StudentAuthenticationToken implements Authentication {

    private Student principal;
    private String credentials;
    private String details;
    private boolean authenticated; // 인증도장

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

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

* Authentication Provider #

@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {

    private HashMap<String, Student> studentDB = new HashMap<>();

    @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)
                .details(student.getUsername())
                .authenticated(true)
                .build();
        }
        return null;
    }

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

    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
            new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
            new Student("puppy", "강아지", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
            new Student("kitty", "고양이", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")))
        ).forEach(s ->
            studentDB.put(s.getId(), s)
        );
    }
}

* AuthenticationProvider를 AuthenticationManager에 등록하기 #

SecurityConfig로 넘어가서.

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

    private final StudentManager studentManager;

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request->
                request.antMatchers("/").permitAll() // root page permit all
                .anyRequest().authenticated() // any request authenticated
            )
        .formLogin(login -> 
            login.loginPage("/login") // 로그인 페이지 설정
            .permitAll() // redirection error
        )
            ;
    }
    ...
}

* Auth page 확인 #

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

principal로 만들었던 student가 동작하고있으며 authorities로 ROLE_USER가 동작하고 있음을 알 수 있다.

결과적으로, UsernamePasswordAuthFilter가 UsernamePasswordAuthToken 발행 -> AuthManager는 토큰을 처리 할 Provider를 찾는다 -> StudentManager의 supports 메소드를 보고 StudentManager 에게 토큰을 넘겨서 처리하게 된다.

* Teacher Authentication #

앞의 Student의 Auth를 만들었듯, Teahcer, TeacherAuthenticationToken, TeacherManager 객체를 생성한다.

SecurityConfig 에서 teacherManager를 Provider로 추가한다.

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

그러면 teacherManger을 통하여 토큰이 처리되서 로그인이 완료되는것을 알 수 있다.


Custom Filter #

config directory에 CustomLoginFilter 객체를 생성한다.

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);
    }
}

이제 SecurityConfig로 넘어가서

@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"))
    ;
}