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ํ•ด์คŒ.