스프링시큐리티 인 액션

Jieun·2023년 10월 26일
0

spring공부

목록 보기
2/3


1. 인증필터가 인증요청을 가로채 인증관리자에 위임 -> 응답을 바탕으로 보안컨텍스트 구성
2. 인증관리자는 인증공급자(인증논리)를 통해 인증을 처리
3. 인증공급자 : 인증논리 구현체 : 인증에 필요한 자격증명 제공 : AuthenticationProvider
- < 사용자 세부정보서비스 : 사용자 관리책임구현 > : UserDetailsService
- < 암호 인코더 : 암호관리 구현 > : PasswordEncoder
4. 보안 컨텍스트 : 인증데이터 저장


UserDetailsService

  • 재정의 방법 : 직접구현 / 스프링시큐리티의 구현이용

UserDetailsService Interface

  • UserDetails - loadUserByUsername(String) : 사용자이름으로 사용자 세부정보(UserDetails)를 얻는다

- UserDetails: 사용자 기술하기(사용자정의)



- 해당 코드에서는 User 클래스의 builder를 통해 UserDetails 인스턴스를 간단하게 생성함 : UserDetails를 따로 구현할 필요 X

  • User class -> UserDetails
    UserDetails는 사용자(User)를 기술한다
  • username / password / authorities
  • 인터페이스 : 이용권리 나타내는 메소드 가짐

- GrantdAuthority : 사용자 권한정의


PasswordEncoder

PasswordEncoder Interface

  • String encode(rawPW) : raw Password를 변환해 반환
  • boolean matches(rawPW, encodedPW) : 인코딩문자열 - 원시문자열 일치여부 확인

AuthenticationProvider

Authentication

: 접근을 요청한 엔티티의 세부정보 + 인증프로세스완료여부, 권한의 컬렉션, ...
:


Filter

OncePerRequestFilter(Filter) Interface

  • doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)
    - filterChain.doFilter() : CorsFilter / CsrfFilter / BasicAuthenticationFilter / CustomFilters
    : 필터체인 : 체인의 다음필터로 요청전달 -> 필요한 여러가지 필터 수행

- CustomFilter 적용하기


config class에서 http요청에

  • http.addFilterBefore(CustomFilter, custom 이후에 사용될 필터)
  • http.addFilterAfter(CustonFilter, custom 이전에 사용될 필터)



  • Authentication : 인증정보를 담은 객체
  1. Http 요청이 들어온다.

  2. 들어온 요청이 AuthenticationFilter에 걸리면, 요청의 payload로부터 ID/PW를 추출하는데 이를 User Credentials 즉, 사용자 자격 증명 정보라 한다.
    이 정보를 기반으로 해 Authentication의 구현체의 일종인 UsernamePasswordAuthenticationToken 객체를 생성한다.
    : HTTP요청 -> AuthenticationFilter : UserCredential 추출 -> UsernamePasswordAuthenticationToken 객체 생성(Authentication의 구현체)

UsernamePasswordAuthenticationToken은 추후 인증이 끝나고 SecurityContextHolder.getContext()에 등록될 Authentication 객체이다.

  1. AuthenticationManager : 각 필터들의 인증 절차를 정의한다. Interface이기 때문에 실질적인 일은 그 구현체인 ProviderManager가 한다.
    AuthenticationManager Interface는 authenticate(Authentication authentication)메소드만 정의되어 있다.
  • 파라미터 Authentication : UsernamePasswordAuthenticationToken 타입. 클라이언트가 입력한 ID(principal)와 PW(credentials) 정보만이 담겨 있는 상태
  • 리턴타입 Authentication : authenticate()를 실행하여 authenticationProvider객체를 통해 인증이 완료되면 인증된 Authentication객체를 반환
public interface AuthenticationManager {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}
  1. ProviderManager : AuthenticationManager의 실제 구현체
    AuthenticationProvider 리스트에 속한 각 Provider에 위임해서 인증을 진행

    AuthenticationProvider마다 인증에 필요한 Authentication 객체가 다르다 -> support()를 통해 Authentication객체에 맞는 AuthenticationProvider를 찾는다.
    이 과정이 AuthenticationManager 구현체인 ProviderManager에서 이루어진다.

5.AuthenticationProvider는 자격증명정보 등을 저장소로부터 받아와 실제 인증로직을 수행한다 : UserDetailsService를 통해 UserDetails 객체 형태로 받아온다.

DaoAuthenticationProvider

  • retrieveUser() : UserDetailsService객체를 통해 로그인 요청한 유저의 UserDetails 객체를 가져온다
  • additionalAuthenticationChecks() : 입력받은 정보(username, credetial)와 userDetails객체의 정보와 비교해 인증을 체크하는 메소드이다. 실질적으로 DB의 데이터와 id, 비밀번호를 입력한 값과 비교하는 곳
  1. 인증된 Authentication 객체 반환
    : UsernamePasswordAuthenticationFilter -> AbstractAuthenticationProcessingFilter의 doFilter()에서 실행된 attemptAuthentication()의 결과로 인증된 Authentication 객체가 반환된다.

  2. 인증이 성공적으로 되었다면,Authentication 객체를 SecurityContextHolder 내의 SecurityContext에 저장한다

  3. SecurityContext

  • 처음 로그인 하는 상태
    - UsernamePasswordAuthenticationFilter :
    attemptAuthentication() -> authenticate()를 호출, 인증을 진행하고-> 인증된 Authentication 객체를 SecurityContextHolder에 넣는다
  • 로그인이 완료된 상태 : authenticate() 메서드를 실행하지 않고 HTTP 세션으로 부터 Authentication 객체를 가져와서 진행
    - SecurityContextPersisenceFilter :
    loadContext() 메서드로 세션 상에 Authentication 객체를 갖고 있는 SecurityContext 가 있는지 확인하고, 없다면 UsernamePasswordAuthenticationFilter 로 인증을 진행, 있다면 가져온 SecurityContext를 이용

+ JWT

+ ( JWTFilter, JWTProvider )

- JWTProvider

: JWT 생성, 해석, 변환
+ AuthenticationProvider의 역할도 겸임

public class JwtProvider {

    private final Key key;

    private static final String AUTHORITIES_KEY = "auth";
    private static final String BEARER_TYPE = "bearer";
    private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 3; // 3시간
    private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;  // 7일

    public JwtProvider(@Value(("${jwt.secret}")) String secretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

	// Token 생성
    public String generateAccessToken(Authentication authentication, long now) {
        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, getAuthorities(authentication))
                .setExpiration(new Date(now + ACCESS_TOKEN_EXPIRE_TIME))
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    public TokenDto generateTokenDto(Authentication authentication) {

        long now = (new Date()).getTime();
        Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME);

        return TokenDto.builder()
                .grantType(BEARER_TYPE)
                .accessToken(generateAccessToken(authentication, now))
                .refreshToken(generateRefreshToken(refreshTokenExpiresIn))
                .refreshTokenExpiresIn(refreshTokenExpiresIn.getTime())
                .build();
    }

	// Token 기반 Authentication 구현체 생성 : AuthenticationProvider 역할
    public Authentication getAuthentication(String accessToken) {

        Claims claims = parseClaims(accessToken);

        if(claims.get(AUTHORITIES_KEY) == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        Collection<? extends GrantedAuthority> authorities = getAuthorities(claims);

        return new UsernamePasswordAuthenticationToken(
                new User(claims.getSubject(), "", authorities),
                "",
                authorities
        );
    }

	// Token 유효성 검사
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }

    private String generateRefreshToken(Date tokenExpiresIn) {
        return Jwts.builder()
                .setExpiration(tokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    private String getAuthorities(Authentication authentication) {
        return authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));
    }

    private Collection<? extends GrantedAuthority> getAuthorities(Claims claims) {
        return Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

- JWTFilter

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

		// 토큰 분리
        String jwt = resolveToken(request);

		// 토큰 유효성 검증
        if (!jwt.equals("nothing") && jwtProvider.validateToken(jwt)) {
			//SecurityContextHolder에 Authentication 저장           
           SecurityContextHolder.getContext().setAuthentication(jwtProvider.getAuthentication(jwt));
            // Authentication 받아옴
        }
		//filterChain
        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
    	// request HEADER에서
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
        	// 7 = PREFIX.Length
            // PREFIX 부분을 날리고 JWT만 token에 할당
            return bearerToken.substring(7);
        }

        return "nothing";
    }
}

0개의 댓글