SpringSecurity/졸과

jwt 토큰을 이용한 다중서버 로그인 유지

브리오 2024. 7. 16. 14:01

AuthServer-CorsConfig

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //새로운 UrlBasedCorsConfigurationSource 객체를 생성합니다. 이 객체는 URL 패턴과 함께 CORS 설정을 등록하는 데 사용됩니다.
        CorsConfiguration config = new CorsConfiguration();
        //새로운 CorsConfiguration 객체를 생성합니다. 이 객체는 CORS 설정을 정의하는 데 사용됩니다.
        config.setAllowCredentials(true);
        //이 줄은 CORS 요청에서 자격 증명(쿠키, 인증헤더 등)을 사용할 수 있도록 설정합니다.
        config.addAllowedOrigin("*");
        //이 줄은 모든 도메인에서의 CORS 요청을 허용합니다. (주의: 실제로는 필요한 도메인만 허용하는 것이 보안상 좋습니다.)
        //클라이언트가 어떤게 되도 상관없다
        config.addAllowedHeader("*");
        //이 줄은 모든 헤더에 대해 CORS 요청을 허용합니다.
        config.addAllowedMethod("*");
		//이 줄은 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)에 대해 CORS 요청을 허용합니다.
        source.registerCorsConfiguration("/api/**", config);
        //이 줄은 /api/** URL 패턴에 대해 앞서 정의한 CorsConfiguration 을 등록합니다.
        return new CorsFilter(source);
        //새로운 CorsFilter 객체를 생성하고 반환합니다. 이 필터는 앞서 설정한 CORS 설정을 사용합니다.
    }
}

@configuration : 설정 클래스 -> 빈정의, 외부파일 설정 로딩

@Bean : IoC(Inversion of Control) 컨테이너가 관리하는 객체->DI(dependency injection) 되는것들

사용할려면 직접 new 한뒤에 사용해야하지만 스프링에서 띄우기 때문에 편하게 사용가능

CORS(Cross-Origin Resource Sharing) : 보안상의 이유로 다른 출처의 자원에서 접근을 막source.registerCorsConfiguration("/api/**", config);를 통해 /api/로 시작하는 모든 요청에 대해

위에서 구성한 CORS 설정이 적용됩니다. 나머지는 접근 불가

AuthServer-SecurityConfig

@EnableWebSecurity
//스프링 시큐리티를 활성화해서 웹 보안 구성을 할 수 있도록 해줍니다
@Configuration
@RequiredArgsConstructor
//롬복에서 제공하는 어노테이션으로 final 필드를 가지는 생성자를 자동으로 생성합니다
//없으면 아래처럼 직접 생성자를 만들어야 한다
/*
    public SecurityConfig(TokenProvider tokenProvider, CorsFilter corsFilter, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtAccessDeniedHandler jwtAccessDeniedHandler) {
        this.tokenProvider = tokenProvider;
        this.corsFilter = corsFilter;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
    }
*/
public class SecurityConfig {
    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    //TokenProvider, CorsFilter, JwtAuthenticationEntryPoint, JwtAccessDeniedHandler 주입받기

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
	//BCryptPasswordEncoder 인스턴스를 반환하는 Bean을 정의, 비밀번호 인코딩 및 매칭을 처리하는 데 사용
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    //filterChain 메서드는 SecurityFilterChain 객체를 생성하는 Bean을 정의하며, HTTP 보안 구성을 설정합니다.
        http
                // CSRF 설정을 기본으로 Configuration을 통해 비활성화
                .csrf(csrf -> csrf.disable())

                // 로그인 인증필터 보다 CORS 필터가 더 앞에 있어야 한다
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)

                // 인증 실패 시->JwtAuthenticationEntryPoint
                // 접근 거부 경우->JwtAccessDeniedHandler
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling
                                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                                .accessDeniedHandler(jwtAccessDeniedHandler)
                )
                //X-Frame-Options : 보안강화
                .headers(headers ->
                        headers.frameOptions(frameOptions -> frameOptions.sameOrigin())
                )

                // STATELESS -> JWT쓸거니까
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                // /auth는 기본적으로 허용해야한다
                // 나머지는 인증되야만 사용가능
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/auth/**").permitAll()
                                .anyRequest().authenticated()
                );

        // JwtFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
        http.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
        //보안 구성 끝
        return http.build();
    }
}

-현재 필터 순서 cors->jwt->usernamepassword

AuthServer-AuthController

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;

    @PostMapping("/signup")
    //request받는거랑 response받는거랑 형식이 다르다
    public ResponseEntity<MemberResponseDto> signup(@RequestBody MemberRequestDto memberRequestDto) {
        //authService.signup()호출해서 성공하면 200리턴
        return ResponseEntity.ok(authService.signup(memberRequestDto));
    }

    @PostMapping("/login")
    public ResponseEntity<TokenDto> login(@RequestBody MemberRequestDto memberRequestDto) {
        return ResponseEntity.ok(authService.login(memberRequestDto));
    }

    @PostMapping("/reissue")
    public ResponseEntity<TokenDto> reissue(@RequestBody TokenRequestDto tokenRequestDto) {
        return ResponseEntity.ok(authService.reissue(tokenRequestDto));
    }
}

-ResponseEntity : 사용이유

  1. HTTP 응답 형태 제어: ResponseEntity를 사용하면 HTTP 응답 코드, 헤더, 본문 데이터 등을 세밀하게 제어할 수 있습니다.
  2. 유연한 응답: ResponseEntity는 다양한 상황에서 유연한 응답 생성을 제공하여, API 요청에 따라 다른 형태의 응답을 제공할 수 있습니다.
  3. HTTP 응답 코드 설정: ResponseEntity를 통해 HTTP 응답 코드를 설정하여 성공, 실패 또는 다른 상태를 클라이언트에게 전달할 수 있습니다.

AuthServer-MemberRequestDto

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MemberRequestDto {

    private String email;
    private String password;
    
    //service에서 회원가입할때 필요
    public Member toMember(PasswordEncoder passwordEncoder) {
        return Member.builder()
                .email(email)
                .password(passwordEncoder.encode(password))
                //기본적으로 역할은 USER
                .authority(Authority.ROLE_USER)
                .build();
    }
	
    //service에서 login할때 사용 -> emial,password를 받아서 auth토큰 객체 형성
    //service에서 login을 하면 사용자정보가 일치한지 확인하고 jwt토큰을 발행하는데
	//사용자 정보가 일치한지 확인할때 사용된다
    public UsernamePasswordAuthenticationToken toAuthentication() {
        return new UsernamePasswordAuthenticationToken(email, password);
    }
}

- UsernamePasswordAuthenticationToken : SpringSecurity에서 사용되는 기본 인증 

https://brio-sw.tistory.com/98

AuthServer-MemberResponseDto

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MemberResponseDto {
    private String email;
	
    //멤버를 받아서 responseDto로 변환 해주는 of메소드
    public static MemberResponseDto of(Member member) {
        return new MemberResponseDto(member.getEmail());
    }
}

-of : 특정객체를 다른객체 형태로 변환할때 사용

AuthServer-Member

@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String email;

    private String password;

    @Enumerated(EnumType.STRING)
    private Authority authority;

    @Builder
    //다른 클래스에서멤버객체를 사용할때 필요
    public Member(String email, String password, Authority authority) {
        this.email = email;
        this.password = password;
        this.authority = authority;
    }
}

-@Builder : 다른 클래스에서 멤버 객체를 사용할때 필요하다, 생성자에 적용한

AuthServer-RefreshToken

@Getter
@NoArgsConstructor
@Entity
public class RefreshToken {

    @Id
    @Column(name = "rt_key")
    private String key;

    @Column(name = "rt_value")
    private String value;

    @Builder
    public RefreshToken(String key, String value) {
        this.key = key;
        this.value = value;
    }
	//service에서 토큰 재발행 할때 사용된다
    public RefreshToken updateValue(String token) {
        this.value = token;
        return this;
    }
}

AuthServer-JwtFilter

@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

	//헤더 작업
    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";

    private final TokenProvider tokenProvider;

    // 실제 필터링 로직은 doFilterInternal 에 들어감
    // JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext 에 저장하는 역할 수행
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        // 1. Request Header 에서 토큰을 꺼냄
        String jwt = resolveToken(request);

        // 2. validateToken 으로 토큰 유효성 검사
        // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
		
        //서블릿으로 넘기기전에 다음 필터로 전달한다 : 다음필터는 매개변수인 filterchain에의 해 결정된다
        filterChain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보를 꺼내오기
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.split(" ")[1].trim();
        }
        return null;
    }
}

- @RequiredArgsConstructor : final에 대해서 자동으로 생성자 생성

https://brio-sw.tistory.com/99

jwt토큰을 헤더에 담아서 들고오는 클라이언트의 요청을 여기서 토큰이 유효한지 검사하고 스레드의 context에 저장한다

그리고 서블릿에 넘기는게 아니라 다음 필터로 넘긴

https://brio-sw.tistory.com/100

 

AuthServer-TokenProvider

@Slf4j
//로거 자동 생성
@Component
//컴포넌트로 등록
public class TokenProvider {

	//토큰에 포함되는 정보 중 키값을 정의
    private static final String AUTHORITIES_KEY = "auth";
    private static final String BEARER_TYPE = "Bearer";
    private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;            // 30분
    private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;  // 7일
	
    //jwt토큰 서명에 사용되는 키 
    //토큰을 검증할 때, 서버는 토큰의 Header와 Payload 부분을 추출하고, 동일한 비밀 키로 서명을 생성한다
    private final Key key;
	
    //jwt토큰은 base64로 디코딩 후 HMAC-SHA 알고리즘에 사용후 키 생성
    public TokenProvider(@Value("${jwt.secret}") String secretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

	//controller(memberdto)->service(authentication=인증확인)->여기
    public TokenDto generateTokenDto(Authentication authentication) {
        // getAuthorities()로 가져온 컬렉션을 stream() 스트림으로 변환
        String authorities = authentication.getAuthorities().stream()
        		//사용자 권한 확인 ROE_USER
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();

        // Access Token 생성
        Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
        //JwtBuilder 인스턴스 생성
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())       // payload "sub": "name"
                .claim(AUTHORITIES_KEY, authorities)        // payload "auth": "ROLE_USER"
                .setExpiration(accessTokenExpiresIn)        // payload "exp": 151621022 (ex)
                .signWith(key, SignatureAlgorithm.HS512)    // header "alg": "HS512" ,JWT서명 설정
                .compact();									// MAKE
		
        // Header: { "alg": "HS512", "typ": "JWT" }
		// Payload: { "sub": "name", "auth": "ROLE_USER", "exp": 151621022 }
        
        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME))
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();

        return TokenDto.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .accessTokenExpiresIn(accessTokenExpiresIn.getTime())
                .refreshToken(refreshToken)
                .build();
    }

    public Authentication getAuthentication(String accessToken) {
        //클레임 추출
        Claims claims = parseClaims(accessToken);
		//빈 토큰
        if (claims.get(AUTHORITIES_KEY) == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        // 클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                //스트림의 각 요소를 SimpleGrantedAuthority 객체로 변환하고 
                //SimpleGrantedAuthority는 GrantedAuthority의 구현체로, 권한 정보를 담는 역할을 한다
                //스트림의 각 권한 문자열("ROLE_USER", "ROLE_ADMIN")에 대해 
                //SimpleGrantedAuthority 생성자를 호출해서 객체를 만듭니다        
                        .map(SimpleGrantedAuthority::new)
                //SimpleGrantedAuthority 객체들이 리스트(List<SimpleGrantedAuthority>)로 변환
                        .collect(Collectors.toList());

        // UserDetails 객체를 만들어서 Authentication 리턴
        // 시큐리티 자체적으로 UserDetails의 구현체인 User를 사용한다
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    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 Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

 

-컴포넌트 : @Bean은 사용자가 직접 객체를 생성하고 반환한다면, @Component는 스프링이 대신 해준다고 생각하자

https://brio-sw.tistory.com/100

 

 

제목1

제목1

제목1