#들어가는 말

기본적이 예제의 설명은 제외했다 시큐리티에 설명은 " 스프링 시큐리티이란? " 을 클릭 인증과 인가를 제공해주는 스프링 시큐리티는 많은 필터들이 있고 일반적으론 모놀로틱 구조는 세션방식을 선택을 하며 프론트엔드 나누기 시작하면 최소 api토큰  큰서비스는 oauth토큰까지 확대 된다고 한다.

 

GitHub - whitewise95/TIL: Today I Learned

Today I Learned. Contribute to whitewise95/TIL development by creating an account on GitHub.

github.com

 

GitHub - whitewise95/TIL: Today I Learned

Today I Learned. Contribute to whitewise95/TIL development by creating an account on GitHub.

github.com

#예제

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
                

        http.addFilterBefore(formLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers("/user/signup").permitAll()
                .antMatchers("/user/idCheck/**").permitAll()
                .antMatchers("/user/nicknameCheck/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().disable(); // 이 예제는 front는 다른 서버에 배포 되므로 disable

    }

    @Bean
    public LoginFilter formLoginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter(authenticationManager());
        loginFilter.setFilterProcessesUrl("/user/login");
        loginFilter.afterPropertiesSet();
        return loginFilter;
    }

    @Bean
    public FormLoginAuthProvider formLoginAuthProvider() {
        return new FormLoginAuthProvider(encodePwd());
    }
    
}

LoginFilter

/*
 * 첫번째
 * UsernamePasswordAuthenticationFilter 는 사용자가 보낸 아이디와 비밀번호를 인터셉트한다.
 * */
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    final private ObjectMapper objectMapper;

    public LoginFilter(final AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
        objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /*
     *
     * UsernamePasswordAuthenticationToken 객체는 클라이언트에서 가져온 정보를 진짜 인증을 담당할
     * AuthenticationManager(ProviderManager) 인터페이스에게
     * 인증용 객체(UsernamePasswordAuthenticationToken)로 만들어 주는 로직이다.
     * */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        UsernamePasswordAuthenticationToken authRequest;
        try {
            JsonNode requestBody = objectMapper.readTree(request.getInputStream());
            String username = requestBody.get("username").asText();
            String password = requestBody.get("password").asText();
            authRequest = new UsernamePasswordAuthenticationToken(username, password);
        } catch (Exception e) {
            throw new RuntimeException("username, password 입력이 필요합니다. (JSON)");
        }

        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

FormLoginAuthProvider

public class FormLoginAuthProvider implements AuthenticationProvider {

    @Resource(name = "principalDetailsService")
    private UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder passwordEncoder;

    public FormLoginAuthProvider(BCryptPasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    /*
     *  DB에서 사용자 인증 저보를 가져올 UserDetailsService 객체에게 사용자 아이디를 넘겨준다.
     *  리턴값으로 UserDetails(인증용 객체)를 받고,
     *  인증용 객체와 도메인 객체를 분리하지 않기 위해서 실제 사용되는 도메인 객체에 UserDetails를 상속하기도 한다
     * */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        // FormLoginFilter 에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
        String username = token.getName();
        String password = (String) token.getCredentials();

        // PrincipalDetails 를 통해 DB에서 username 으로 사용자 조회
        PrincipalDetails userDetails = (PrincipalDetails) userDetailsService.loadUserByUsername(username);
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            return new UsernamePasswordAuthenticationToken(null, null, userDetails.getAuthorities());
        }

        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }

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

}

PrincipalDetailsService

@Service
public class PrincipalDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Autowired
    public PrincipalDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username).orElseThrow(
                () -> new UsernameNotFoundException("해당 유저가 없습니다."));

        return new PrincipalDetails(userEntity);
    }
}

PrincipalDetails

@Getter
public class PrincipalDetails implements UserDetails {
    //Session(Authentication(UserDetails,OAuth2USer)):

    private final User user;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}



#테스트

1) 예제에 있는 그대로 테스트

로그인 후 JSESSIONID 를 받아온걸 확인할 수 있었다.

JSESSIONID를 설정하지 않고 antMatchers().permitAll() 설정해주지 않은 API를

찔러 보았지만 인증이 되지 않은것을 확인

쿠키에 JSESSIONID를 설정하고 다시 같은 api를 이용해보았다.

 

2) formlogin.disable() 안하고 테스트

나는 처음에 formlogin이 Request로 JSON데이터와 Form 데이터의 기준이라고 생각하지 않았다 하지만 증명하지는 못했고 왜 그런지도 설명하지 못했다. 이번 기회에 테스트를 진행 해보았고 결과는 JSON으로 보내야지만 받을 수 있었다. 근데 어쩔 땐 form 데이터타입으로 보내야만 받아질 때가 있었는데 그것은 다음 테스트에서 설명하겠다.

 

3) Provider, Filter 제거 후 테스트

3-1) Provider, Filter를 안쓰도록 주석처리를 하고 진행했다.

3-2) 근데 갑작이 username 과 password가 받아 지지 않는다.

  • formlogin().disable()를 제거하고 .formLogin().loginProcessingUrl("/user/login") 로 변경하고 form데이터로 요청했더니 로그인에 성공했다.
    즉, Provider, Filter를 안쓰면 formlogin을 사용해야하며 form데이터로 받아야한다.

3-3) 로그인 후 JSESSIONID 를 받아온걸 확인할 수 있었다.

3-4) 하지만 인증해줄 provider가 없으므로 아무리 쿠키에 세션이 있어도 인증이 되지 못했다.

4) 결과 정리

  • formlogin은 request 타입 json, form 데이터와 관계가 없다.
  • formlogin().disable 하고 provider와 Filter를 안쓰면 데이터가 받아지지 않음

form과 json만 테스트함

  • provider와 Filter가 없더라도 formlogin을 disable 안하면 데이터는 form데이터로 받아짐 JSETIONID도 받아짐

provider가 없으면 인증해줄 객체가 사라지므로 아무리 JSESSIONID를 쿠키에 담아도 인증과 인가가 안되어 .permitAll()안해준 API는 접근 불가

  • 질문을 통해 formlogin을 disable한다는 건 모놀로식이 아니고 프론트를 나누었다면 formlogin을 disable를 하는게 맞다는 답변을 얻음
복사했습니다!