Jin's Dev Story

[Spring Security] Jwt Token 사용 본문

Web & Android/Spring Security

[Spring Security] Jwt Token 사용

woojin._. 2023. 10. 17. 15:58

Jwt Token 로그인

💡 Use Case Specification (명세서)

  1. 우선 login, join을 제외한 페이지를 전부 막는다.
  2. 사용자가 login하면 id, pw를 검증하고 Token을 생성하여 발급한다.
  3. 발급 받은 Token 권한에 따라 해당 페이지를 접근할 수 있다.
💡 [build.gradle]
implementation 'io.jsonwebtoken:jjwt:0.9.1' implementation 'javax.xml.bind:jaxb-api:2.3.0' implementation 'org.springframework.boot:spring-boot-starter-security'

SourceCode & Explanation

configuration

AuthenticationConfig

  • Security 사용을 선언 및 설정
  • 모든 요청을 받아 필터링하는 securityFilterChain을 세팅하여 등록함
import com.example.walkingmate_back.login.service.LoginService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@Configuration // 설정파일 선언
@EnableWebSecurity // security사용 선언
@RequiredArgsConstructor
public class AuthenticationConfig {

    // @EnableWebSecurity를 선언함으로 써 모든 api 요청을 security가 관리하게 됨.
    private final LoginService loginService;

    @Value("${jwt.secret}")
    private String secretKey;

    // api 요청이 들어오면 검사하는 security의 FilterChain설정.
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(request -> request
                        .requestMatchers("/api/login", "/api/join", "/buyHistory/**", "/board/**", "/user/**", "/battle/**", "/checkList/**", "/run/**", "/team/**").permitAll() // 인증 필요없음
                        .requestMatchers(HttpMethod.POST, "/api/**").authenticated() // 인증 있어야함
                )
//                .requestMatchers(HttpMethod.POST, "/api/v1/home/user").hasRole("USER") // USER 권한 있어야함
//                .requestMatchers(HttpMethod.POST, "/api/v1/home/admin").hasRole("ADMIN") // ADMIN 권한 있어야함
                .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(new JwtFilter(loginService, secretKey), UsernamePasswordAuthenticationFilter.class); // FilterChain 앞에 JwtFilter 추가

        return httpSecurity.build();
    }
}

JwtFilter

  • securityFilterChain 앞에서 처리하는 Custom Filter
  • Token 확인, 접근제한, 권한부여, Detail추가 등의 선처리 역할
package com.example.test.configuration;
import com.example.test.service.UserService;
import com.example.test.utils.JwtUtil;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
    private final UserService userService;
    private final String secretKey;
    // 인증받기 위한 내부Filter - 여기를 통해야 들어갈 수 있다.
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // request에서 토큰 추출
        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization : {}", authorization);
        // Token이 없다면 그냥 반환시킴
        if(authorization == null){
            log.error("[접근불가] authorization이 없습니다.");
            filterChain.doFilter(request, response);
            return;
        }
        // Token 꺼내기
        String token = authorization;
        // Token Expired 되었는지 여부
        if(JwtUtil.isExpired(token, secretKey)){
            log.error("token이 만료 되었습니다.");
            filterChain.doFilter(request, response);
            return;
        }
        // UserName Token에서 꺼내기
        String userName = JwtUtil.getUserName(token, secretKey);
        log.info("userName:{}", userName);
        // 권한 부여
        UsernamePasswordAuthenticationToken authenticationToken;
        if(userName.equals("admin")) {
            // id가 admin이면 관리가(ADMIN)권한 부여
            authenticationToken = new UsernamePasswordAuthenticationToken
                    (userName, null, List.of(new SimpleGrantedAuthority("ADMIN")));
        }else {
            // 아니라면 일반 사용자(USER)권한 부여
            authenticationToken = new UsernamePasswordAuthenticationToken
                    (userName, null, List.of(new SimpleGrantedAuthority("USER")));
        }
        log.info("Role : {}", authenticationToken.getAuthorities());
        // Detail을 넣어줍니다.
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));     SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}
import com.example.walkingmate_back.login.service.LoginService;
import com.example.walkingmate_back.login.utils.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
    private final LoginService loginService;
    private final String secretKey;

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

        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization : {}", authorization);

        if(authorization == null){
            log.error("[접근불가] authorization이 없습니다.");
            filterChain.doFilter(request, response);
            return;
        }
        String token = authorization;

        // TODO
        //
//        if(JwtUtil.isExpired(token, secretKey)){
//            log.error("token이 만료 되었습니다.");
//            filterChain.doFilter(request, response);
//            return;
//        }

        // Token에서 UserName추출
        String userName = JwtUtil.getUserName(token, secretKey);
        log.info("userName:{}", userName);

        UsernamePasswordAuthenticationToken authenticationToken;
        if(userName.equals("admin")) {
            // 관리자(ADMIN)권한 부여
            authenticationToken = new UsernamePasswordAuthenticationToken
                    (userName, null, List.of(new SimpleGrantedAuthority("ADMIN")));
        }else {
            // 일반 사용자(USER)권한 부여
            authenticationToken = new UsernamePasswordAuthenticationToken
                    (userName, null, List.of(new SimpleGrantedAuthority("USER")));
        }
        log.info("Role : {}", authenticationToken.getAuthorities());

        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));     SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

controller

UserController

  • login 페이지 mapping, 요청 값 리턴
package com.example.test.controller;
import com.example.test.domain.LoginRequest;
import com.example.test.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
    // login 버튼 클릭 시 id, pw를 받으며 호출됨.
    // dto로 (id,pw)값을 태워서 service의 login메서드 호출
    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest dto) {
        return ResponseEntity.ok().body(userService.login(dto.getUserName(), dto.getPassword()));
    }
    @PostMapping("/join")
    public ResponseEntity<String> join() {
        return ResponseEntity.ok().body("회원가입 완료");
    }
}

HomeController

  • user, admin 페이지 mapping, 요청 값 리턴
  • USER, ADMIN 페이지 권한 설정
package com.example.test.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/home")
public class HomeController {
    @PostMapping("/user")
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public String userPage() {
        return "userPage";
    }
    // ADMIN 권한을 가져야 접근 가능
    @PostMapping("/admin")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String adminPage() {
        return "adminPage";
    }
}

ReviewController

  • reviews 페이지 mapping, 요청 값 리턴
package com.example.test.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/reviews")
public class ReviewController {
    @PostMapping
    public ResponseEntity<String> writeReview(Authentication authentication) {
        return ResponseEntity.ok().body(authentication.getName() + "님의 리뷰 등록이 완료되었습니다.");
    }
}

domain

LoginRequest

  • login 매핑 시 Request값을 받는 dto
package com.example.test.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class LoginRequest {
    private String userName;
    private String password;
}

 


service

UserService

  • UserController에서 service작업을 처리하기 위해 호출하는 클래스
  • login메서드로 값을 전달받아 JwtUtil을 호출하여 Token을 생성해옴
import com.example.walkingmate_back.login.domain.JoinRequest;
import com.example.walkingmate_back.login.domain.JoinResponseDTO;
import com.example.walkingmate_back.login.domain.LoginRequest;
import com.example.walkingmate_back.login.domain.LoginResponse;
import com.example.walkingmate_back.login.utils.JwtUtil;
import com.example.walkingmate_back.user.entity.UserBody;
import com.example.walkingmate_back.user.entity.UserEntity;
import com.example.walkingmate_back.user.repository.UserBodyRepository;
import com.example.walkingmate_back.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

/**
 *    로그인, 회원가입
 *    - 서비스 로직
 *
 *   @version          1.00 / 2023.09.10
 *   @author           전우진, 이인범
 */

@Service
@Slf4j
@Transactional
public class LoginService {

    @Value("${jwt.secret}")
    private String secretKey;
    private Long expiredMs = 1000 * 60 * 60l;

    private final UserRepository userRepository;
    private final UserBodyRepository userBodyRepository;

    private LoginResponse loginResponse;
    private JoinResponseDTO joinResponseDTO;

    @Autowired
    public LoginService(UserRepository userRepository, UserBodyRepository userBodyRepository) {
        this.userRepository = userRepository;
        this.userBodyRepository = userBodyRepository;
    }

    /**
     * 회원가입
     * - 전우진, 이인범 2023.09.10
     */
    public JoinResponseDTO join(JoinRequest joinRequest) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

        // 문자열 -> LocalDate
        LocalDate date = LocalDate.parse(joinRequest.getBirth(), formatter);

        joinResponseDTO = new JoinResponseDTO();

        if(userRepository.existsById(joinRequest.getId()) == false) {
                userRepository.save(UserEntity.builder()
                        .id(joinRequest.getId())
                        .pw(joinRequest.getPw())
                        .name(joinRequest.getName())
                        .phone(joinRequest.getPhone())
                        .birth(date)
                        .build());

                // 신체정보 저장
                UserBody userBody = new UserBody();
                userBody.setUserId(joinRequest.getId());
                userBody.setHeight(joinRequest.getHeight());
                userBody.setWeight(joinRequest.getWeight());
                userBodyRepository.save(userBody);

                joinResponseDTO.data.code = joinResponseDTO.success;
                joinResponseDTO.data.message = "회원가입 성공";
                return joinResponseDTO;
            } else {
                joinResponseDTO.data.code = joinResponseDTO.fail;
                joinResponseDTO.data.message = "중복된 아이디";
                return joinResponseDTO;
            }
    }

    /**
     * 로그인
     * - 이인범
     */
    public LoginResponse login(LoginRequest loginRequest) {
        String userId = loginRequest.getUserId();
        String password = loginRequest.getPassword();
        log.info("userName:{}, password:{}", userId, password);

        loginResponse = new LoginResponse();

        // 인증 과정
        Optional<UserEntity> user = userRepository.findById(userId);
        if (user.isPresent()) {
            loginResponse.data.userId = user.get().getId();

            if((user.get().getPw()).equals(password)) {
                loginResponse.data.jwt = JwtUtil.createJwt(userId, secretKey, expiredMs);
                loginResponse.data.code = loginResponse.success;
                loginResponse.data.message = "generate token";
                return loginResponse;
            }
            loginResponse.data.message = "잘못된 비밀번호";
            loginResponse.data.code = loginResponse.fail;
            return loginResponse;
        }
        loginResponse.data.message = "존재하지 않는 사용자";
        loginResponse.data.code = loginResponse.fail;

        return loginResponse;
    }
}

utils

JwtUtil

  • Jwt을 사용할때 필요한 부가 작업 메서드로 구현
  • Token 생성, 만료, 값 추출 등의 작업
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {

    public static String getUserName(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().get("userName", String.class);
    }

    // TODO
    // Token의 만료 여부를 체크하는 isExpired 메서드
    // Error: .parseClaimsJws(token)에서 token parsing 과정에서 Json타입 에러 발생.
    public static boolean isExpired(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }

    public static String createJwt(String userName, String secretKey, Long expiredMs) {
        Claims claims = Jwts.claims(); // username을 저장할 map?
        claims.put("userName", userName);
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }
}
총정리 : configuration에서 security세팅 및 적용하고, controller에서 service의 함수 호출하면, service가 utils에서 토큰 만들어와서 다시 controller로 return해줌.