Spring + JWT

문진영·2022년 9월 14일
1

파이널 프로젝트

목록 보기
3/9

JWT 시나리오

  1. 클라이언트가 로그인 정보와 함께 로그인 요청을 보낸다.
  2. 서버는 가입한 유저인지 유저정보로 확인한다.
  3. 유저가 확인이 된다면, 토큰(access,refresh)을 생성하고 발급한다.
  4. 클라이언트는 발급받은 토큰으로 로그인 유지, 유저 정보 불러오기, 유저인증 수단으로 활용한다.
  5. 만약 access 토큰이 만료 될 경우 클라이언트는 refresh토큰과 함께 재발급 요청을 보낸다.
  6. 클라이언트로부터 받은 refreshtoken이 유효할 경우 서버는 다시 토큰(access, refresh)을 갱신하여 재발급한다.
  • 토큰이 유효할 경우 서비스를 이용하고
  • 토큰이 만료되거나 조작 시에 프론트와 약속된 예외 처리를 해야 한다.

application.yml의 일부

jwt :
  secret : PUT SECRET_KEY

이곳에서 secret-key를 보관한다

UserController.java의 일부

@RestController // @Controller + @ResponseBody
@RequiredArgsConstructor //생성자 주입
@RequestMapping("/user")//아래에 있는 모든 mapping은 문자열/api를 포함해야한다.
public class UserController {
    private final TokenServiceImpl tokenService;
    private final TokenProvider tokenProvider;

    Cookie cookie = new Cookie("Cookie","forSecure");

    @PostMapping(value = "/signin")
    //ResponseEntity는  httpentity를 상속받는 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스이고, 응답으로 변환될 정보를 모두 담은 요소들을 객체로 사용 된다.
    public ResponseEntity login(@RequestBody  UserDTO userDTO, HttpServletResponse response){
        try {
             tokenService.loginMethod(userDTO, response);

            cookie.setMaxAge(7*24*60*60);
            cookie.setHttpOnly(true); //token 쿠키 저장 방식의 csrf 취약 문제 방지 위해  httponly true 설정
            cookie.setSecure(true); //security : true
            cookie.setPath("/");

            response.addCookie(cookie);

            return ResponseEntity.ok().body("SignIn Success");

        } catch (Exception e) {
            ResponseDTO responseDTO = ResponseDTO.builder().error(e.getMessage()).build();
            return ResponseEntity
                    .badRequest()
                    .body(responseDTO);
        }
    }
    
    @GetMapping(value = "/check")
    public ResponseEntity checkUser(HttpServletRequest request) {

        UserDTO result = tokenService.decodeJWT(request);

        return ResponseEntity.ok().body(result);
    }
    
    @GetMapping("/token/refresh")
    public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String refresh_token = request.getHeader(AUTHORIZATION);
        if(refresh_token != null && refresh_token.startsWith("Bearer ")) {
            try {

                UserDTO user = tokenService.decodeJWT(request);

                tokenService.refreshToken(user, response);

                Map<String, String> message = new HashMap<>();
                message.put("message","Refresh The Token");

                new ObjectMapper().writeValue(response.getOutputStream(), message); //토큰 전송

            }catch (Exception exception) {

                if (exception.getMessage().startsWith("The Token's Signature")) { //조작된 토큰 일때
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", "Incorrect Token Do Re-Login");
                }

                else if (exception.getMessage().startsWith("The Token has expired")) {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", "Token Has Expired Do Refresh");
                }

                else {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", "Unexpected Error...");
                }

                response.setStatus(FORBIDDEN.value()); //forbidden error code로 보낸다.

                //response.sendError(FORBIDDEN.value());

                Map<String, String> error = new HashMap<>();

                error.put("error_message", "Make User Re-Login");

                response.setContentType(APPLICATION_JSON_VALUE);

                new ObjectMapper().writeValue(response.getOutputStream(), error);
            }
        } else {
            throw new RuntimeException("Access token is missing");
        }
    }
}

login :

  • tokenservice의 loginMethod로 유저 정보를 보낸다.
  • XSS방지 cookie설정

checkUser :

  • token이 담긴 request를 tokenservice의 decodeJWT로 보낸다
  • 클라이언트의 로그인 유지와 정보 교환에 사용된다

refreshToken :

  • 받은 refresh-token이 존재하면 tokenService의 decodeJWT로 검사를 하고 유저 정보를 불러온다.
  • 불러온 유저 정보로 tokenService의 refreshToken으로 토큰 생성 및 재발급을 진행한다.
  • 오류 종류에 따른 예외 처리를 진행 한다.

TokenServiceImpl.java

@Service
@RequiredArgsConstructor
public class TokenServiceImpl implements TokenService{

    private final UserRepository userRepository;

    private final PasswordEncoder passwordEncoder;

    private final TokenProvider tokenProvider;

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

    @Override
    public void loginMethod(UserDTO userDTO, HttpServletResponse response) {

        String email = userDTO.getEmail();

        UserEntity info = userRepository.findByEmail(email);

        if(info == null){
            throw new UsernameNotFoundException("User not found in the database");
        }

        else {
            String password = info.getPassword();
            boolean verify = passwordEncoder.matches(userDTO.getPassword(), password);

            if(verify){
                tokenProvider.createToken(info.toDTO(), response);
            }

            else {
                throw new RuntimeException("WrongPassword");
            }
        }
    }

    @Override
    public void refreshToken(UserDTO user, HttpServletResponse response) {
        tokenProvider.createToken(user, response);
    }

    @Override
    public UserDTO decodeJWT(HttpServletRequest request) {



        String access_token = request.getHeader(AUTHORIZATION);

        if(access_token != null && access_token.startsWith("Bearer ")) {
            try {
                String token = access_token.substring("Bearer ".length());

                Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY.getBytes());

                JWTVerifier verifier = JWT.require(algorithm).build();

                DecodedJWT decodedJWT = verifier.verify(token);

                String email = decodedJWT.getSubject();
                String name = decodedJWT.getIssuer();
                String role = decodedJWT.getClaim("role").toString().replace("\"", "");

                UserDTO userDTO = UserDTO.builder()
                        .email(email)
                        .name(name)
                        .role(role)
                        .build();

                return userDTO;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        else {
            throw new RuntimeException("No Token");
        }
    }
}

loginMethod : login시 유저 확인과 유저일경우 token을 생성 하여 발급한다.
refreshToken : 다시 access,refresh token을 생성하여 재발급한다.
decodeJWT : token을 decode하여 유저정보를 return한다.

TokenProvider.java

@Component
public class TokenProvider {

    @Value("${jwt.secret}")//application.yml 파일에 secret-key 보관
    private String SECRET_KEY;

    public void createToken(UserDTO user, HttpServletResponse response) {

        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY.getBytes()); //token 생성 알고리즘

        String access_token = JWT.create() //access token 생성
                .withSubject(user.getEmail())//이름을 유일한 유저 정보로 하여 토큰의 중복 방지
                .withExpiresAt(new Date(System.currentTimeMillis() + 10 * 60 *1000)) //기간 설정 -> 지금으로 부터 + ???
                .withIssuer(user.getName())
                .withClaim("role", user.getRole())
                .sign(algorithm);

        String refresh_token = JWT.create() //refresh token 생성
                .withSubject(user.getEmail())
                .withExpiresAt(new Date(System.currentTimeMillis() + 300 * 60 *1000))
                .withIssuer(user.getName())
                .withClaim("role", user.getRole())
                .sign(algorithm);

        response.setHeader("access_token", access_token);//header에 담아서 보낸다
        response.setHeader("refresh_token", refresh_token);
    }
}

유저 정보로 토큰을 생성합니다.

profile
개발 하는 게 좋은 사람입니다.

0개의 댓글