스프링부트 쇼핑몰 프로젝트 | 4 Spring Security (1)

Yunny.Log ·2022년 5월 8일
0

Spring Boot

목록 보기
46/80
post-thumbnail

Security

인가 & 인증의 중요성

  • 인증 후 인가가 일어남

1. 설정

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

1) dependencies 안에 넣어주기

  • 스프링 시큐리티를 추가했다면, 이제 모든 요청은 인증을 필요로 한다.
  • 의존성을 추가하는 것만으로도 모든 요청에 인증을 요구하게 된다.

2. SecurityConfig 소스 작성

  • 의존성에 security 추가 시에, 모든 요청에 인증을 요구하지만 SecurityConfig.java의 configure의 메소드에 설정을 추가하지 않으면 인증을 요구하지 않음


@RequiredArgsConstructor

@EnableWebSecurity

// WebSecurityConfigurerAdapter 을 상속받는 클래스에 이 어노테이션을 붙이면, SpringSecurityFilterChain이 자동 포함

public class SecurityConfig extends WebSecurityConfigurerAdapter {

//WebSecurityConfigurerAdapter 을 상속 받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징 가능

    private final TokenService tokenService;
    private final CustomUserDetailsService userDetailsService;
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    //http 요청에 대한 보안을 설정, 페이지 권한 설정 / 로그인 페이지 설정 / 로그아웃 메소드 등에 대한 설정 작성

    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        // 비밀번호를 db 그대로 저장 경우, db 해킹당하면 고객 정보 그대로 노출, 따라서 비밀번호 암호화 저장하는 함수를 통해 비밀번호 저장
        //이를 빈으로 등록해 사용할 것
    }

    }

3. Member


    public static Member toEntity(
            SignUpRequest req,
            Role role,
            PasswordEncoder encoder
    ) {

        if(!req.password.equals(req.passwordcheck)){
            throw new PasswordNotSameException();
        }

        return new Member
                (
                        req.email,
                        encoder.encode(req.password),
                        req.username,
                        req.department,
                        req.contact ,
                        List.of(role),

                        new ProfileImage(
                                req.profileImage.
                                        getOriginalFilename()
                        )

                );
    }

MemberRepository.java

public interface MemberRepository extends JpaRepository<Member, Long> , CustomMemberRepository{

    Optional<Member> findByEmail(String email);

    boolean existsByEmail(String email);
    

SignService.java

    private void validateSignUpInfo(SignUpRequest req) {
        if(memberRepository.existsByEmail(req.getEmail()))
            throw new MemberEmailAlreadyExistsException(req.getEmail());
    }

    private void validatePassword(SignInRequest req, Member member) {
        if(!passwordEncoder.matches(req.getPassword(), member.getPassword())) {
            throw new PasswordNotValidateException();
        }
    }
  • 비즈니스 로직을 담당하는 서비스 계층 클래스에 @Transactional 어노테이션 선언
  • 로직 처리하다가 에러 발생 ? => 변경된 데이터를 로직 수행하기 이전 상태로 콜백
  • 빈을 주입하는 방법
    1) @Autowired 어노테이션 이용
    2) 필드 주입(setter 주입)
    3) 생성자 주입

(+) javax validation 어노테이션

SignController.java

public class SignController {
    private final SignService signService;

    Logger logger = LoggerFactory.getLogger(SignController.class);

    @PostMapping("sign-up")
    @ResponseStatus(HttpStatus.CREATED)

//@Valid 로 request에서 annotation 조건에 안 맞는 애 있는지 점검
    public Response signUp(@Valid SignUpRequest req) {
        signService.signUp(req);
        return success();
    }

4. 로그인 / 로그아웃

1) UserDetailService

  • UserDetailService 인터페이스는 데이터베이스에서 회원 정보를 가져오는 역할을 담당
  • loadUserByUsername 메소드가 존재하며, 회원 정보를 조회해 사용자 정보와 권한을 갖는 UserDetails 인터페이스 반환

2) UserDetail

  • 스프링 시큐리티에서 회원 정보를 담기 위해서 사용하는 인터페이스는 UserDetails
  • 인터페이스 구현하거나 스프링 시큐리티에서 제공하는 User 클래스 사용
  • User 클래스는 UserDetails 인터페이스를 구현하는 클래스

3) 로그인 / 로그아웃 구현


CustomUserDetailService

  • UserDetailService 인터페이스는 데이터베이스에서 회원 정보를 가져오는 역할을 담당

  • loadUserByUsername 메소드가 존재하며, 회원 정보를 조회해 사용자 정보와 권한을 갖는 UserDetails 인터페이스 반환

  • 얻은 Authentication 객체를 SecurityContext에 저장 (얘는 authenticated 되었다고 이 context에 저장시켜주는 것)


@Component
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    /**
     * 인증된 사용자의 정보를 CustomUserDetails로 반환
     */
    private final MemberRepository memberRepository;

    @Override
    /**
     * 유저의 Role, RoleType 확인
     */
    public CustomUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {

        /**
         * 사용자의 id 값으로 사용자 정보 조회
         */
        Member member = memberRepository.findById(Long.valueOf(userId))
                .orElseGet(() -> new Member(null, null, null, null, null ,List.of(), null));
        return new CustomUserDetails( //3)
                String.valueOf(member.getId()),
                member.getRoles().stream().map(memberRole -> memberRole.getRole())
                        .map(role -> role.getRoleType())
                        .map(roleType -> roleType.toString())
                        //권한 등급은 String 인식, Enum 타입 RoleType을 String 변환
                        .map(SimpleGrantedAuthority::new).collect(Collectors.toSet())
                //권한 등급을 GrantedAuthority 인터페이스로 받음
        );
    }
}
  • UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩
    => 로그인할 유저의 아이디를 넘겨준다.

  • 3) : userdetail 구현하는 user 객체 반환, user 객체 생성 위해서 생성자로 파라미터 넘겨줌


CustomUserDetails.java

  • 스프링 시큐리티에서 회원 정보를 담기 위해서 사용하는 인터페이스는 UserDetails
  • 인터페이스 구현하거나 스프링 시큐리티에서 제공하는 User 클래스 사용
  • User 클래스는 UserDetails 인터페이스를 구현하는 클래스

@Getter
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
    /**
     * 인증된 사용자의 정보와 권한 가짐
     * userId, 권한 등급 메소드만 사용
     * 나머지 메소드 호출 시 예외 발생
     */
    private final String userId;
    private final Set<GrantedAuthority> authorities;

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

    @Override
    public String getUsername() {
        return userId;
    }

    @Override
    public String getPassword() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isAccountNonExpired() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isAccountNonLocked() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isEnabled() {
        throw new UnsupportedOperationException();
    }
}

SecurityConfig.java

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 로그인, 회원가입은 누구나
         * 회원정보 가져오는 것은 누구나
         * 멤버 삭제는 관리자 혹은 해당 멤버만
         */
        http
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .authorizeRequests()
                //.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()//added
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/sign-in", "/sign-up", "/refresh-token").permitAll()



                .anyRequest().hasAnyRole("ADMIN")//멤버의 역할이 관리자인 경우에는 모든 것을 허용

                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .and()//인증되지 않은 사용자의 접근이 거부
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                .and()//인증된 사용자가 권한 부족 등의 사유로 인해 접근이 거부
                .addFilterBefore(new JwtAuthenticationFilter(tokenService, userDetailsService), UsernamePasswordAuthenticationFilter.class);

        http.headers().frameOptions().sameOrigin();
    }

CustomAuthenticationEntryPoint

  • 로그인 성공 x
  • 인증되진 않은 자가 리소스 요청한다면, Unauthorized 에러 발생시키도록 authentication Entrypoint 구현
/**
 * 리프레쉬 토큰 만료 시 핸들러
 인증되지 않은 사용자가 요청 시 작동 핸들러
 */
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        /**
         * 토큰이 만료된 경우 예외처리
         */
        response.setStatus(SC_UNAUTHORIZED);
        response.sendRedirect("/exception/entry-point");
    }
    // 스프링 도달 전이라서 직접 상황에 맞게 응답 방식 작성 가능하나 response로 응답하도록 설정
}

CustomAccessDeniedHandler

  • 로그인은 됐지만 권한 없음

/**
 * 인증은 되었지만,
 * 사용자가 접근 권한이 없을 시 작동 핸들러
 */
@NoArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler{

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//

        response.sendRedirect("/exception/access-denied");
    }

}
  • 시큐리티에서 필터는 controller에 닿기 전에 실행된다.
  • 따라서 내가 정의했던 ExceptionAdvice에 가기 힘듦
  • 따라서 따로 ExceptionController을 필터 외부에 만들어줘서 리다리엑트 되게 추가 설정

ExceptionController

/**
 * 예외 사항 발생 시 "/exception/{예외}"로 리다이렉트
 */

    private final TokenService tokenService;
    @CrossOrigin(origins = "https://localhost:3000")
    @GetMapping("/exception/entry-point")
    public void entryPoint(@RequestHeader(value = "Authorization") String accessToken) {
        /**
         * 액세스 만료
         */
        if (!tokenService.validateAccessToken(accessToken)) {
            System.out.println("액세스가 만료");
            throw new AccessExpiredException();
        }
        System.out.println("액세스 만료가 아니라 리프레시 에러야 이거는  ");
        throw new AuthenticationEntryPointException();
    }
    @CrossOrigin(origins = "https://localhost:3000")
    @GetMapping("/exception/access-expired")
    public void accessExpired() {
        throw new AccessExpiredException();
    }


    @CrossOrigin(origins = "https://localhost:3000")
    @GetMapping("/exception/access-denied")
    public void accessDenied() {

        throw new AccessDeniedException();
    }

    }

0개의 댓글