본문 바로가기
Spring

[8] 소셜 로그인 구현 2

by 민지기il 2025. 3. 18.

implementation: 강의 버전을 따라서 implement 해줬다.

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

 

[1] JWTUtil

@Log4j2
public class JWTUtil {
    private static String key = "123456789910Thisisthekeythatiusetoencryptthepassword";
    public static String generateToken(Map<String, Object> valueMap, int min) {
        SecretKey key = null;
        try {
            key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        String jwtStr = Jwts.builder()
                .setHeader(Map.of("typ", "JWT"))
                .setClaims(valueMap)
                .setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
                .setExpiration(Date.from(ZonedDateTime.now().plusMinutes(min).toInstant()))
                .signWith(key)
                .compact();

        return jwtStr;
    }
    public static Map<String, Object> validateToken(String token) {
        Map<String, Object> claim = null;

        try {
            SecretKey key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));

            claim = Jwts.parserBuilder()
                    .setSigningKey(key)  // 서명 키 설정
                    .build()
                    .parseClaimsJws(token)  // 토큰 검증 및 파싱
                    .getBody();
        } catch (MalformedJwtException malformedJwtException) {
            throw new CustomJWTException("MalFormed");  // 잘못된 형식의 토큰
        } catch (ExpiredJwtException expiredJwtException) {
            throw new CustomJWTException("Expired");  // 만료된 토큰
        } catch (InvalidClaimException invalidClaimException) {
            throw new CustomJWTException("Invalid");  // 유효하지 않은 클레임
        } catch (JwtException jwtException) {
            throw new CustomJWTException("JWTError");  // 기타 JWT 관련 오류
        } catch (Exception e) {
            throw new CustomJWTException("Error");  // 기타 예외 발생
        }

        return claim;
    }

}

 

[2] ApiLoginSuccessHandler 추가 -- 토큰 생성 코드 추가

@Log4j2
public class ApiLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //access & refresh claims에 넣기
        MemberDTO memberDTO = (MemberDTO) authentication.getPrincipal();

        Map<String, Object> claims = memberDTO.getClaims();
	// +) token 생성하는 부분 추가하기 !
        String accessToken = JWTUtil.generateToken(claims, 10);
        String refreshToken = JWTUtil.generateToken(claims, 60*24);

        claims.put("accessToken", "");
        claims.put("refreshToken", "");

        Gson gson = new Gson();
        String jsonStr = gson.toJson(claims);

        response.setContentType("applicaion/json; charset=UTF-8"); //한글 처리
        PrintWriter printWriter = response.getWriter();
        printWriter.println(jsonStr);
        printWriter.close();

    }
}

 

[3] CustomSecurityConfig 추가

public class CustomSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        log.info("--------security config-----");
        http.cors(httpSecurityCorsConfigurer -> {
            httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource());
        });
        // session 제거
        http.sessionManagement(httpSecuritySessionManagementConfigurer -> {
            httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.NEVER);
        });

        http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());
        //로그인 성공, 실패 관리
        http.formLogin(formLogin -> formLogin
                .loginPage("/api/member/login")
                .successHandler(new ApiLoginSuccessHandler())
                .failureHandler(new ApiLoginFailHandler())
        );
	//+) JWTCheckFilter 추가하기
        http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

사용자가 로그인하여 Access Token을 발급받은 후 API 요청을 보낼 때, 해당 요청이 이미 인증된 사용자인지 JWT를 먼저 확인하고,

인증되지 않은 경우에만 UsernamePasswordAuthenticationFilter가 실행되도록 하는 구조이다.

 

[4] JWTCheckFilter

@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String path =  request.getRequestURI();

        return false; //check함
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
    String authHeaderStr = request.getHeader("Authorization");
    //Bearer: 공백 포함 7개, 뒤에가 jwt
        try {
            String accessToken = authHeaderStr.substring(7);
            Map<String, Object> claims = JWTUtil.validateToken(accessToken); //검사
            filterChain.doFilter(request, response);
        }catch (Exception e){
            Gson gson = new Gson();
            String msg = gson.toJson(Map.of("error", "ERROR_ACCESS_TOKEN"));
            response.setContentType("application/json");
            PrintWriter printWriter = response.getWriter();
            printWriter.println(msg);
            printWriter.close();
        }
    //도착지
    filterChain.doFilter(request, response);
    }
}

 

[5] CustomSecurityConfig 설정 추가

@EnableMethodSecurity
public class CustomSecurityConfig {
...
}

[6] JWTCheckFilter에 하단 내용 추가

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
    String authHeaderStr = request.getHeader("Authorization");
    //Bearer: 공백 포함 7개, 뒤에가 jwt
        try {
            String accessToken = authHeaderStr.substring(7);
            Map<String, Object> claims = JWTUtil.validateToken(accessToken); //검사

            String email = (String) claims.get("email");
            String pw = (String) claims.get("pw");
            String nickname = (String) claims.get("nickname");
            Boolean social = (Boolean) claims.get("social");
            List<String> roleNames = (List<String>) claims.get("roleNames");

            MemberDTO memberDTO = new MemberDTO(email, pw, nickname, social.booleanValue(), roleNames);
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(memberDTO, pw, memberDTO.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            filterChain.doFilter(request, response);
        }catch (Exception e){
            Gson gson = new Gson();
            String msg = gson.toJson(Map.of("error", "ERROR_ACCESS_TOKEN"));
            response.setContentType("application/json");
            PrintWriter printWriter = response.getWriter();
            printWriter.println(msg);
            printWriter.close();
        }
    //도착지
    filterChain.doFilter(request, response);
    }

roleNames: 사용자의 권한 목록(ROLE_USER, ROLE_ADMIN 등)이 들어있음.

MemberDTO 객체 생성 → 사용자 정보를 담고 있음.

UsernamePasswordAuthenticationToken 객체 생성 → Spring Security에서 인증 객체로 사용.

SecurityContextHolder.getContext().setAuthentication(authenticationToken); → Spring Security가 이 사용자를 인증된 상태로 설정함!, @PreAuthorize가 사용자 권한을 체크할 수 있음.

[7] ProductController

@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
@GetMapping("/list")
public PageResponseDTO<ProductDTO> list(PageRequestDTO pageRequestDTO){
    return productService.getList(pageRequestDTO);
}

이 메서드는 ROLE_USER 또는 ROLE_ADMIN 권한이 있는 사용자만 호출이 가능함

@PreAuthorize를 사용하면, 메서드 실행 전에 Spring Security가 권한을 검사해서 허용된 사용자만 접근할 수 있음.

[8] login 페이지는 filter하면 안 됨

/api/member/로 시작하면 filter 안 함

@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String path =  request.getRequestURI();
        if(path.startsWith("/api/member/")){
            return true;
        }

        return false; //check함
    }

 

[9] Gson 이란?

: Java 객체(Map, DTO 등)를 JSON으로 쉽게 변환하는 기술임

        Map<String, Object> data = new HashMap<>();
        data.put("name", "Alice");
        data.put("age", 25);
        data.put("isStudent", false);

String json = gson.toJson(data);

System.out.println(json); 하면 {"name":"Alice","age":25,"isStudent":false} 이렇게 나온다.

 

[10] CustomAccessDeniedHandler

특정 권한을 가진 유저만 접근이 가능함

@Log4j2
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Gson gson = new Gson();
        String jsonStr = gson.toJson(Map.of("error", "ERROR_ACCESSDENIED"));

        response.setContentType("application/json");
        response.setStatus(HttpStatus.FORBIDDEN.value());
        PrintWriter printWriter = response.getWriter();
        printWriter.println(jsonStr);
        printWriter.close();
    }
}

[11] CustomSecurityConfig

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
		...
        //로그인 성공, 실패 관리
        http.formLogin(formLogin -> formLogin
                .loginPage("/api/member/login")
                .successHandler(new ApiLoginSuccessHandler())
                .failureHandler(new ApiLoginFailHandler())
        );
// 이 부분 추가하기
        http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
        http.exceptionHandling(config -> {
            config.accessDeniedHandler(new CustomAccessDeniedHandler());
        })
        return http.build();
    }

[12] ApirefreshController -  refresh token을 이용한 토큰 갱신

@RestController
@RequiredArgsConstructor
@Log4j2
public class ApiRefreshController {

    @RequestMapping("/api/member/refresh")
    public Map<String, Object> refresh(
            @RequestHeader("Authorization") String authHeader,
            String refreshToken) {

        if (refreshToken == null) {
            throw new CustomJWTException("NULL_REFRESH");
        }
        if (authHeader == null || authHeader.length() < 7) {
            throw new CustomJWTException("INVALID_STRING");
        }

        String accessToken = authHeader.substring(7);

        // Access Token이 아직 만료되지 않았다면 그대로 반환
        if (!checkExpiredToken(accessToken)) {
            return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
        }

        // Refresh Token을 검증하고 새로운 Access Token 생성
        Map<String, Object> claims = JWTUtil.validateToken(refreshToken);
        String newAccessToken = JWTUtil.generateToken(claims, 10);
        String newRefreshToken = checkTime((Integer) claims.get("exp")) 
                ? JWTUtil.generateToken(claims, 60 * 24) 
                : refreshToken;

        return Map.of("accessToken", newAccessToken, "refreshToken", newRefreshToken);
    }

    // Refresh Token의 남은 유효 시간 확인
    private boolean checkTime(Integer exp) {
        java.util.Date expDate = new java.util.Date((long) exp * 1000);
        long gap = expDate.getTime() - System.currentTimeMillis();
        long leftMin = gap / (1000 * 60);
        return leftMin < 60;  // 남은 시간이 60분 미만이면 새로운 Refresh Token 발급
    }

    // Access Token 만료 여부 확인
    private boolean checkExpiredToken(String token) {
        try {
            JWTUtil.validateToken(token);
        } catch (CustomJWTException ex) {
            if ("Expired".equals(ex.getMessage())) {
                return true;
            }
        }
        return false;
    }
}

 

'Spring' 카테고리의 다른 글

[2] 자바 중급 문법 2  (0) 2025.03.21
[1] 자바 중급 문법 1  (0) 2025.03.19
[9] 상품 생성  (0) 2025.03.18
[7] 소셜 로그인 구현  (0) 2025.03.12
[6] 이미지 파일 업로드  (0) 2025.03.07