스프링 시큐리티

dalBeen·2023년 10월 7일
0

스프링

목록 보기
8/14

스프링 시큐리티?

스프링 시큐리트는 스프링 기반의 애플리케이션 보안을 담당하는 스프링 하위 프레임 워크

인증

사용자의 신원을 입증하는 과정. 쉽게말해 어떤 사이트에 아이디와 비밀번호를 입력하고 로그인하는 과정


인가

사용자의 권한을 확인하는 과정. 어떤 파일을 확인할 수 있는 권한인지 확인


스프링 시큐리티

애플리케이션의 보안을 담당하는 스프링 하위 프레임 워크. 보안 관련 옵션을 만이 제공해주고 복잡한 로직없이 어노테이션으로 설정할 수 있다.
기본적으로 스프링 시큐리티는 세션 기반인증을 제공한다


필터

스프링 시큐리티는 필터 기반으로 동작한다

인증과 인가와 관련된 다양한 필터들이 존재한다

  1. SecurityContextPersistenceFilter: SecurityContext를 HttpSession에 저장하거나 복원하는 작업을 담당합니다. 이를 통해 사용자의 보안 컨텍스트가 요청 간에 유지됩니다.

  2. UsernamePasswordAuthenticationFilter: 폼 기반 인증을 처리합니다. 사용자가 아이디와 비밀번호를 입력하여 로그인을 시도할 때, 이 필터가 해당 요청을 처리합니다.

  3. BasicAuthenticationFilter: HTTP 기본 인증을 처리합니다.

  4. RememberMeAuthenticationFilter: "Remember Me" 인증을 처리합니다. 사용자가 웹 사이트에 자동 로그인할 수 있도록 해주는 기능입니다.

  5. AnonymousAuthenticationFilter: 인증되지 않은 사용자를 위한 '익명' 인증을 처리합니다.

  6. ExceptionTranslationFilter: 인증 및 권한부여와 관련된 예외를 처리하고, 필요한 경우 인증 프로세스를 시작합니다.

  7. FilterSecurityInterceptor: 권한부여 처리를 합니다. HTTP 리소스에 대한 접근 권한을 결정합니다.

  8. LogoutFilter: 로그아웃 요청을 처리하며, 로그아웃 성공 후 후속 작업을 수행합니다.

  9. CsrfFilter: CSRF 공격을 방지하기 위한 필터입니다.

  10. ConcurrentSessionFilter: 동시 세션 제어를 위한 필터입니다. 예를 들어, 동일한 사용자 아이디로 동시에 여러 곳에서 로그인하는 것을 제한할 수 있습니다.



스프링 시큐리티 인증 처리 과정

로그인한다고 생각했을때

  1. HttpServletRequest에 아이디, 비밀번호 정보가 전달됨
    -> AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성을 검사함.

  2. 유효성 검사후 UsernamePasswordAuthenticationToken을 만들어 넘겨준다

  3. UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달

  4. UsernamePasswordAuthenticationToken을 AuthenticationProvider에게 전달

  5. 사용자 아이디를 UserDetailService로 보낸다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetails객체로 만들어 AuthenticationProvider에게 전달

  6. DB에 있는 사용자 정보를 가져옴

  7. 입력정보와 UserDetails의 정보를 비교해 실제 인증처리 진행

  8. 인증까지 완료되면 SecurityContextHolder에 Authentication을 저장. 인증 성공여부에따라 성공시 AuthenticationSuccessHandler, 실패시 AuthenticationFailureHandler핸들러를 실행



스프링 부트 3.X에 스프링 시큐리티 적용

의존성 추가

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

회원 도메인 생성

UserDetails 클래스를 상속하는 Member클래스 생성

@Entity(name = "MEMBER")
@AllArgsConstructor
@Getter
@Setter
@Builder
public class MemberEntity implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles;

    public MemberEntity() {

    }

	//사용자가 가지고 있는 권한 목록 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

	//계정이 만료되었는지 확인
    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

	//계정이 잠금이 되었는지 확인
    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

	//사용자의 인증 자격증명(비밀번호)이 만료되었는지
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

	//계정이 사용가능한지 확인
    @Override
    public boolean isEnabled() {
        return false;
    }
}

UserDetails 클래스는 스프링 시큐리티에서 사용자의 인증정보를 담아 두는 인터페이스


Repository생성

@Repository
public interface MemberRepository extends JpaRepository<MemberEntity,Long> {

    Optional<MemberEntity> findByUsername(String username);
    boolean existsByUsername(String username);
}

Service 생성

@Slf4j
@Service
@AllArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemberEntity member = this.memberRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("couldn't find user-> " + username));
        return member;
    }

    public MemberEntity register(Auth.SignUp member){
        boolean exist = this.memberRepository.existsByUsername(member.getUsername());
        if(exist){
            throw new AlreadyExistUserException();
        }

        member.setPassword(this.passwordEncoder.encode(member.getPassword()));
        MemberEntity result = memberRepository.save(member.toEntity());
        return result;
    }

    public MemberEntity authenticate(Auth.SignIn member){
        System.out.println(member.getUsername());
        MemberEntity user = memberRepository.findByUsername(member.getUsername())
                .orElseThrow(() -> new RuntimeException("존재하지 않는 ID입니다"));

        if(!passwordEncoder.matches(member.getPassword(),user.getPassword())){
            throw new RuntimeException("비밀번호가 일치하지 않습니다");
        }

        return user;
    }
}

SecurityConfiguration

@Slf4j
@Configuration
//스프링 시큐리티 설정 활성화
@EnableWebSecurity
// 메서드 수준의 보안을 활성화함
//-> @PreAuthorize / @PostAuthorize어노테이션 사용가능
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	//JWT를 사용한 인증필터
    private final JwtAuthenticationFilter authenticationFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                    .authorizeRequests()
                        .antMatchers("/**/signup","/**/signin").permitAll()
                .and()
                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
    

	//스프링 시큐리티의 모든 기능을 사용하지 않게 설정
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/h2-console/**");
    }
    
    //기본 AuthenticationManager Bean을 오버라이드하며, 
    //이를 다른 곳에서 주입받아 사용할 수 있게 해줍니다.
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}
  1. 첫번째 configure()메서드
    -> Http 기본인증을 비활성화
    -> Crsf보호를 비활성화
    -> 세션을 사용하지 않는 무상태 모드로 설정
    -> 해당 경로에 대한 인증없이 접근이 허용하도록 설정
    ->UsernamePasswordAuthenticationFilter.class 전에 authenticationFilter 실행하도록 추가

Controller

@RestController
@Slf4j
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

    private final MemberService memberService;
    private final TokenProvider tokenProvider;

    @PostMapping("/signup")
    public ResponseEntity<?> signup(@RequestBody Auth.SignUp request){
        MemberEntity register = memberService.register(request);
        return ResponseEntity.ok(register);
    }

    @PostMapping("/signin")
    public ResponseEntity<?> signin(@RequestBody Auth.SignIn request){
        System.out.println(request);
        MemberEntity member = memberService.authenticate(request);
        String token = tokenProvider.generateToken(member.getUsername(), member.getRoles());
        log.info("user login -> "+member.getUsername());
        return ResponseEntity.ok(token);
    }
}

TokenProvider -> JwtAuthenticationFilter -> SecurityConfiguration

SecurityConfiguration에 JwtAuthenticationFilter등록해서 작동하는데 TokenProvider로 토큰을 생성하고 파싱하기도 함

TokenProvider

@Component
@RequiredArgsConstructor
public class TokenProvider {

    @Value("${spring.jwt.secret}")
    private String secretKey;
    private static final long TOKEN_EXPIRE_TIME=1000*60*60;
    private static final String KEY_ROLE="roles";
    private final MemberService memberService;

    //토근생성
    public String generateToken(String username, List<String> roles){
        Claims claims= Jwts.claims().setSubject(username);
        claims.put(KEY_ROLE,roles);

        Date now=new Date();
        Date expiredDate = new Date(now.getTime() + TOKEN_EXPIRE_TIME);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiredDate)
                .signWith(SignatureAlgorithm.HS512,secretKey)
                .compact();
    }

    public String getUsername(String token){
        return this.parseClaims(token).getSubject();
    }

    public boolean validateToken(String token){
        if(!StringUtils.hasText(token)) return false;

        Claims claims = parseClaims(token);
        return claims.getExpiration().before(new Date());
    }
    //토근 파스
    private Claims parseClaims(String token){
        try{
            return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
        }catch (ExpiredJwtException e){
            return e.getClaims();
        }
    }

    public Authentication getAuthentication(String jwt){
        UserDetails userDetails = memberService.loadUserByUsername(getUsername(jwt));
        return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
    }
}

JwtAuthenticationFilter

package com.zerobase.stock.security;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    public static final String TOKEN_HEADER="Authorization";
    public static final String TOKEN_PREFIX="Bearer";
    private final TokenProvider tokenProvider;

	//토큰확인
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token=resolveTokenFromRequest(request);
        if(StringUtils.hasText(token)&&tokenProvider.validateToken(token)){
            Authentication auth = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);

            log.info(String.format("[%s]-> %s",tokenProvider.getUsername(token),request.getRequestURI()));
        }

        filterChain.doFilter(request,response);
    }

    private String resolveTokenFromRequest(HttpServletRequest request){
        String token=request.getHeader(TOKEN_HEADER);

        if(!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)){
            return token.substring(TOKEN_PREFIX.length());
        }

        return null;
    }
}

스프링 시큐리티 내용 보충

profile
깊게 공부해보자

0개의 댓글