실전 프로젝트 - 회원 인증 시스템

enxnong·2024년 7월 31일
0

스프링 시큐리티

목록 보기
12/13

정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.

사용자정의 보안설정 및 기본 사용자구성

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -> auth
                        // "/" 경로 인증 없이 접근 허용
                        .requestMatchers("/").permitAll()
                        // 나머지는 인증 받도록 설정
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                ;

        return http.build(); // securityFilterChain 빈 생성
    }

    // 사용자 계정 생성
    @Bean
    public UserDetailsService userDetailsService(){
        UserDetails user = User.withUsername("user").password("{noop}1111").roles("USER").build();
        return new InMemoryUserDetailsManager(user);
    }

}
  • UserDetailsService

    • 특정 사용자의 정보를 로드하는 핵심 인터페이스
    • 메서드 : loadUserByUsername(String username)
      - 사용자 이름을 기반으로 사용자의 세부 정보를 로드
      - 인증 과정에서 사용
  • InMemoryUserDetailsManager

    • 메모리 내에서 사용자 정보를 관리하는 클래스
    • 주로 개발 테스트에서 활용

로그인 페이지 만들기

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // csrf 기능 활성화
                .authorizeHttpRequests(auth -> auth
                        // 정적 자원 모두 인증 없이 접근 허용 설정
                        .requestMatchers("/css/**","/images/**","/js/**","/favicon.*","/*/icon-*").permitAll()
                        // "/" root 인증 없이 접근 허용 설정
                        .requestMatchers("/").permitAll()
                        // 나머지는 인증 받도록 설정
                        .anyRequest().authenticated()
                )
                // 로그인 페이지 설정
                // 로그인 페이지를 커스텀하면 로그아웃 페이지도 커스텀 해야됨!
                .formLogin(form -> form.loginPage("/login").permitAll())
                ;

        return http.build();
    }
  • POST 방식의 요청은 csrf 기능이 켜져있으면 반드시 csrd 토큰이 서버로 전달되어야한다
  • thymeleaf에서는 form 태그를 사용하면 자동으로 csrf 토큰을 생성해준다
<form th:action="@{/login}" method="post">
	...
</form>
  • 자동으로 hidden 태그로 csrf 토큰이 생성 된 것을 볼 수 있다

회원가입 / PasswordEncoder

PasswordEncoder

  • PasswordEncoder 인터페이스는 비밀번호를 안전하게 저장하기 위해 비밀번호의 단방향 변환을 수행하는 데 사용된다
  • 일반적으로 사용자의 비밀번호를 암호화하여 저장하거나 인증 시 검증을 위해 입력한 비밀번호와 암호화 되어 저장된 비밀번호를 서로 비교해야 할 때 사용된다
    • encode() : 비밀번호를 지정된 암호화 방식으로 인코딩
    • matches() : 인코딩된 비밀번호화 입력된 비밀번호를 인코딩하여 일치하는지 검사
    • upgradeEncoding() : 보안을 위해 한 번 더 인코딩을 해야하는 경우데 사용(기본 : false 반환)

DelegatingPasswordEncoder

  • PasswordEncoder 구현 객체를 생성해주는 컴포넌트로써 DelegatingPasswordEncoder를 통해 애플리케이션에서 사용할 PasswordEncoder를 결정하고, 결정된 PasswordEncoder로 사용자가 입력한 패스워드를 단방향으로 암호화 해준다.
    • ex) {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

💡 결정된 PasswordEncoder 확인 방법
→ {id} 형식의 접두사로 어떤 방식으로 인코딩 되었는지 확인할 있다

  • 다양한 방식의 암호화 알고리즘을 적용할 수 있으며, 지정하지 않을 경우 {bcrypt}로 적용된다
    @PostMapping(value="/signup")
    public String signup(AccountDto accountDto) {
        // AccountDto에 입력된 정보를 다른 객체에 복사시켜줌
        // 이를 통해 각 속성 간 매핑이 됨
        ModelMapper mapper = new ModelMapper();
//        Account account = mapper.map(원본객체, 원본의 값을 복사해서 받을 클래스 타입);
        Account account = mapper.map(accountDto, Account.class);
        // account를 통해 db에 데이터 저장
        account.setPassword(passwordEncoder.encode(accountDto.getPassword()));
        // db에 저장하기 위해 service => respository로 전달
        userService.createUser(account);
        // 회원 가입 진행 후 root 페이지로 이동
        return "redirect:/";
    }
  • 회원가입 실행

  • 입력된 비밀번호 encoding

  • 회원가입 성공

커스텀 UserDetailService 구현하기

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Account account = userRepository.findByUsername(username);
        if(account == null){
            // account가 인증받지 못한 경우, 예외 발생 시켜서 접근 차단
            throw new UsernameNotFoundException("No user found with username" + username);
        }

        // 권한 정보 설정
        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(account.getRoles()));
        // AccountDto에 매핑시키기
        ModelMapper mapper = new ModelMapper();
        AccountDto accountDto = mapper.map(account, AccountDto.class);

        return new AccountContext(accountDto,authorities);
    }
  • 로그인 요청 유저 정보 확인

  • 유저 권한 확인

  • UserDetail 정보 확인

커스텀 AuthenticationProvider 구현하기

  • userDetailsService가 AuthenticationProvider에서 서비스를 참조하여 사용하기 때문에 AuthenticationProvider를 커스텀하면 userDetailsService을 추가적으로 하지 않아도 된다
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String loginId = authentication.getName();
        String password = (String) authentication.getCredentials();

        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(loginId);

        if(!passwordEncoder.matches(password, accountContext.getPassword())){
            // 입력한 비밀번호와 저장된(인코딩된) 비밀번호 일치 여부 확인
            throw new BadCredentialsException("유효하지 않은 비밀번호");
        }

        // 새로운 인증 객체 만들어서 반환
        return new UsernamePasswordAuthenticationToken(accountContext.getAccountDto(),null,accountContext.getAuthorities());
    }
  • 로그인 요청 유저 정보 확인

커스텀 로그아웃

  • 로그아웃 기능은 POST 방식인 경우에만 수행한다
    • CSRF 기능이 활성화된 상태에서 POST방식의 요청이 들어오면 해당 요청에는 CSRF 기능이 담겨서 와야되기 때문이다
    @GetMapping(value="/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response){
        Authentication authentication = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication();
        if(authentication != null){
            new SecurityContextLogoutHandler().logout(request,response,authentication);
        }
        return "redirect:/login";
    }

커스텀 인증상세 구현하기

WebAuthenticationDetails

  • 인증 세부 정보를 포함하는 클래스로 사용자의 IP주소와 세션ID와 같은 정보를 가지고 온다

AuthenticationDetailsSource

  • 인증 과정 중 Authentication 객체에 세부 정보(WebAuthenticationDetails 클래스)를 제공하는 소스 역할을 한다
  • WebAuthenticationDetails 객체를 생성하는 데 사용되며 인증 필터에서 참조한다
    public FormAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // request 값을 전달받아 클라이언트에 넘긴 정보를 시큐리티에 저장시킴
        this.secretKey = request.getParameter("secret_key");
    }

profile
높은 곳을 향해서

0개의 댓글