SpringSecurity - 인증 처리 구현

박민수·2023년 11월 14일
0

Spring

목록 보기
34/46
post-thumbnail

SpringSecurity - 인증 처리 구현

SpringSecurity를 이용하여 인증을 처리할 때, customUserDetailsService를 구현하여 직접 사용자를 DB에 저장하고 DB로부터 사용자를 직접 조회하여 그 조회한 사용자를 통해서 인증처리가 이루어질 수 있도록 할 수 있다.

의존성 추가 및 기본 설정

dependency 추가

implementation 'org.springframework.boot:spring-boot-starter-security'
SecurityConfig.java

이전에는 스프링 시큐리티의 인증을 담당하는 AuthenticationManager에 authenticationManagerBuilder를 이용해서 userDetailsServicepasswordEncode를 직접 설정해주어야 했지만, 현재는 AuthenticationManager 빈 생성 시 자동으로 UserSecurityServicePasswordEncoder가 설정된다.

@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfig {

    @Autowired UserDetailsService userDetailsService;

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() throws Exception {
        return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/","/users").permitAll()
                .antMatchers("/mypage").hasRole("USER")
                .antMatchers("/messages").hasRole("MANAGER")
                .antMatchers("/config").hasRole("ADMIN")
                .anyRequest().authenticated();
                
        http.formLogin()
                .loginPage("/login").permitAll();
                
        http.logout()
        		.logoutSuccessUrl("/");

        return http.build();
    }
}

UserAccount.class

  • User는 UserDetails(인터페이스)를 구현한 객체이다.
  • UserDetails는 Spring Security에서 사용자의 정보를 담는 인터페이스이다.
@Getter
public class UserAccount extends User {

    private final Account account;
    
    public UserAccount(Account account, Collection<? extends GrantedAuthority> authorities) {
        super(account.getUsername(), account.getPassword(), authorities);
        this.account = account;
    }
    
//    public UserAccount(Account account) {
//        super(account.getUsername(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
//        this.account = account;
//    }
}

CustomUserDetailsService.java

  • UserDetailsService는 유저의 정보를 가져오는 인터페이스이며, 기본적으로 loadUserByUsername 메소드를 오버라이드 해야한다.
  • loadUserByUsername 메소드의 리턴 타입은 UserDetails이며, 유저의 정보를 불러와서 UserDetails 타입으로 리턴한다.
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = userRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException("UsernameNotFoundException");
        }

        List<GrantedAuthority> roles = new ArrayList<>();
        roles.add(new SimpleGrantedAuthority(account.getRole()));

        return new UserAccount(account, roles);
    }
}

customAuthenticationProvider.java

@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        UserAccount userAccount = (UserAccount) userDetailsService.loadUserByUsername(username);

        if (!passwordEncoder.matches(password, accountContext.getAccount().getPassword())) {
            throw new BadCredentialsException("아이디 또는 비밀번호가 일치하지 않습니다.");
        }

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userAccount.getAccount(), null, userAccount.getAuthorities());

        return authenticationToken;
    }

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

로그인 처리 구현

AccountController.class

@GetMapping("/sign-up")
public String signUpForm(Model model) {
    model.addAttribute("signUpForm", new SignUpForm());
    return "account/sign-up";
}

@PostMapping("/sign-up")
public String signUpSubmit(@Valid @ModelAttribute SignUpForm signUpForm, Errors errors, Model model) {
    if (errors.hasErrors()) {
        return "account/sign-up";
    }

    Account account = accountService.processNewAccount(signUpForm);
    accountService.login(account);
    return "redirect:/";
}

AccountService.class

public void login(Account account) {
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
            new UserAccount(account), // UserAccount 객체가 Principal로 간주됨.
            account.getPassword(),
            List.of(new SimpleGrantedAuthority("ROLE_USER")));
    SecurityContextHolder.getContext().setAuthentication(token);
}

UserAccount.class

@Getter
public class UserAccount extends User {

    private Account account;

    public UserAccount(Account account) {
        super(account.getNickname(), account.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
        this.account = account;
    }
}

CurrentUser (annotation)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
// 인증된 객체가 아니면 스프링 시큐리티가 UserAccount(principal)에 anonymousUser 라는 값을 넣음.
// UserAccount 객체가 anonymousUser면 null, 그게 아니라면 UserAccount 객체안에 account 프로퍼티를 가져옴.
public @interface CurrentUser {
}

InMemoryUserDetailsManager

SecurityConfig.class

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){

        UserDetails user = User.builder()
                .username("user")
                .password(passwordEncoder().encode("user"))
                .roles("USER")
                .build();

        UserDetails sys = User.builder()
                .username("sys")
                .password(passwordEncoder().encode("sys"))
                .roles("SYS", "USER")
                .build();

        UserDetails admin = User.builder()
                .username("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN", "SYS", "USER")
                .build();

        return new InMemoryUserDetailsManager(user, sys, admin);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user").hasRole("USER")
                .antMatchers("/admin/pay").hasRole("ADMIN")
                .antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
                .anyRequest().authenticated();
        http
                .formLogin();

        return http.build();
    }

}

ExceptionTranslationFilter

  • new AuthenticationSuccessHandler() : 인증 성공 시 처리
  • .authenticationEntryPoint() : 인증 실패 시 처리
  • .accessDeniedHandler() : 인가 실패 시 처리

SecurityConfig.class

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/user").hasRole("USER")
            .antMatchers("/admin/pay").hasRole("ADMIN")
            .antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
            .anyRequest().authenticated();
    http
            .formLogin()
            .successHandler(new AuthenticationSuccessHandler() { // 인증 성공 시 처리
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    RequestCache requestCache = new HttpSessionRequestCache();
                    SavedRequest savedRequest = requestCache.getRequest(request, response);
                    String redirectUrl = savedRequest.getRedirectUrl();
                    response.sendRedirect(redirectUrl);
                }
            });
    http
            .exceptionHandling() // 예외 처리 기능 작동
            .authenticationEntryPoint(new AuthenticationEntryPoint() { // 인증 실패 시 처리
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                    response.sendRedirect("/login");
                }
            }) 
            .accessDeniedHandler(new AccessDeniedHandler() { // 인가 실패 시 처리
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                    response.sendRedirect("/denied");
                }
            });

    return http.build();
}
profile
안녕하세요 백엔드 개발자입니다.

0개의 댓글