Game login 구현하기 [2] access_token / refresh_token

최준호·2022년 4월 23일
0
post-thumbnail

🔨access_token / refresh_token 발급하기

@Slf4j
@RequiredArgsConstructor
public class GameAuthFilter extends UsernamePasswordAuthenticationFilter {
    
    ...

    //로그인 성공
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        GameUserVo user = gameUserService.getUserDetailByUserId(((User) authResult.getPrincipal()).getUsername());
        log.info("로그인 성공 = {}",user.getUserId());

        String access_token = Jwts.builder()
                .setSubject(user.getUserId())
                .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) //파기일
                .claim("roles", user.getRoles().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))    //암호화 알고리즘과 암호화 키값
                .compact();

        String refresh_token = Jwts.builder()
                .setSubject(user.getUserId())
                .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) //파기일
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))    //암호화 알고리즘과 암호화 키값
                .compact();

        /*
         * 헤더로 보낼 경우
         */
//        response.setHeader("access_token", access_token);
//        response.setHeader("refresh_token", refresh_token);

        Map<String, String> map = new HashMap<>();
        map.put("access_token",access_token);
        map.put("refresh_token",refresh_token);
        new ObjectMapper().writeValue(response.getOutputStream(), map);

    }
}

다음과 같이 두개의 토큰을 생성해서 json으로 반환하도록 만들었다. header에 전송하고 싶다면 주석 코드로 하면 된다.

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class GameRole implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Override
    public String getAuthority() {
        return name;
    }
}

그리고 GameRole에 GrantedAuthority를 상속시켜 다음과 같이 구현해준다.

그럼 이제 로그인을 했을 때 2개의 토큰을 발급 받을 수 있고

https://jwt.io/ 페이지에서

우리가 jwt에 넣었던 내용을 확인할 수 있게되었다.

✅token 유효성 체크

@Component
@Slf4j
public class TokenAuthJwtFilter extends AbstractGatewayFilterFactory<TokenAuthJwtFilter.Config> {
    private Environment env;

    public TokenAuthJwtFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    public static class Config{
        //설정에 필요한 내용 정의
    }

    //인증 요청시 확인
    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain)->{
            ServerHttpRequest request = exchange.getRequest();

            if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)){
                //헤더에 AUTHORIZATION key 자체가 존재하지 않을 경우
                return onError(exchange, "no AUTHORIZATION header", HttpStatus.UNAUTHORIZED);   //401 반환
            }

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);   //AUTHORIZATION key 값으로 value 가져옴
            String jwt = authorizationHeader.replace("Bearer ", ""); //JWT, OAuth는 Bearer로 붙여서 전송하기로 약속함
            try{
                Claims claims = Jwts.parser().setSigningKey(env.getProperty("token.secret"))  //secret key 값을 통해 parse
                        .parseClaimsJws(jwt).getBody();//token의 내용을 가져옴
                String userId = claims.getSubject();   //userId
                ArrayList<String> roles = (ArrayList<String>) claims.get("roles");
                //String[] roles = (String[]) claims.get("roles");    //권한

                log.info("subject = {}", userId);


            }catch (Exception e){
                //token이 유효하지 않을 경우
                return onError(exchange, e.getMessage(), HttpStatus.UNAUTHORIZED);
            }


            return chain.filter(exchange);
        });
    }

    //에러 발생시 에러 값을 response
    //Mono, Flux -> Spring WebFlux 개념 / 데이터 단위 단일=Mono, 복수=Flux
    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();  //Mono 데이터 return
    }
}

그 후 다음과 같이 TokenAuthJwtFilter class를 생성하여 토큰을 검사하도록 한다.

...
spring:
  application:  #gateway service 이름름
    name: gateway-service
  cloud:
    gateway:  #gateway 설정
      routes:
        # game service
        - id: game-service
          uri: lb://GAME-SERVICE
          predicates:
            - Path=/game-service/**
            - Method=GET, POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/game-service(?<segment>.*), /$\{segment}
            - TokenAuthJwtFilter
            ...

gateway에서도 game service로 요청이 왔을 땐 다음과 같이 filter가 걸리도록 했다.

실제로 로그인을 요청했을 때 다음과 같이 jwt의 내용에서 권한과 userId를 가져오며 없다면 onError를 통해 에러 메세지를 뿜어낸다.

동일한 url로 토큰을 임의로 수정해서 보내면 401이 떨어지며

에러 로그는 다음과 같이 찍히는 걸 확인할 수 있다.

🔨refresh token으로 access token 발급

controller

@RestController
@RequestMapping("/game")
@RequiredArgsConstructor
@Slf4j
public class GameUserController {
    private final GameUserService gameUserService;

    ...

    /*
     * refresh token 발급
     */
    @PostMapping("/token/refresh")
    public ResponseEntity<Token> tokenRefresh(HttpServletRequest req){
        Token token = gameUserService.refreshToken(req);
        return ResponseEntity.ok().body(token);
    }
}

login-service에 token을 재발급 하기 위한 url을 적어놨고

@Getter
@AllArgsConstructor
public class Token {
    private String access_token;
    private String refresh_token;
}

Token 객체는 이렇게만 정의했다.

기존의 CommonApi를 사용하여 데이터를 반환하지 않는 이유는 로그인 할때 반환되는 token의 데이터가
{
access_toke : ~~,
refresh_token : ~~
}
의 형태로 반환하기 때문에 형태를 동일하게 하기 위함이다.

service

public interface GameUserService extends UserDetailsService {

    ...
    //token 재발급
    Token refreshToken(HttpServletRequest req);
}
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class GameUserServiceImpl implements GameUserService{
    private final BCryptPasswordEncoder passwordEncoder;
    private final GameUserRepository gameUserRepository;
    private final GameRoleRepository gameRoleRepository;
    private final Environment env;

	...
    
    @Override
    public Token refreshToken(HttpServletRequest req) {
        log.info("user token refresh");
        String authorization = req.getHeader(AUTHORIZATION);

        Token token = null;
        if(authorization != null && authorization.startsWith("Bearer ")){
            String refreshToken = authorization.replace("Bearer ", "");

            try {
                //refresh token parse
                Claims claims = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
                        .parseClaimsJws(refreshToken).getBody();

                //refresh token에는 userId와 만기일만 담겨 있음
                String userId = claims.getSubject();
                GameUserEntity user = gameUserRepository.findByUserId(userId);
                
                //access token 재발급
                String accessToken = Jwts.builder()
                        .setSubject(user.getUserId())
                        .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) //파기일
                        .claim("roles", user.getRoles().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
                        .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))    //암호화 알고리즘과 암호화 키값
                        .compact();
                token = new Token(accessToken, refreshToken);
            }catch (Exception e){
                //token이 유효하지 않을 경우 ex) refresh token의 정보가 잘못 되었거나 유효기간을 초과함
                log.error("user token refresh error : {}", e.getMessage());
                throw new UserException("401", "token이 유효하지 않습니다.", HttpStatus.UNAUTHORIZED);
            }
        }else{
            // 요청이 잘못됨
            String msg = "token 요청이 잘못 되었습니다.";
            log.error("user token refresh error : {}", msg);
            throw new UserException("404", msg, HttpStatus.BAD_REQUEST);
        }
        return token;
    }
}

refresh_token의 정보를 통해 user의 정보를 가져와 다시 access_token을 만들고 새롭게 반환하는 데이터는 새로 만든 access_token을 넣고 요청할 때 사용된 refresh_token을 넣어서 반환해준다.

그 외 header의 요청이 잘못 되었을 경우와 token을 파싱하며 에러가 발생했을 때 예외 처리를 해두었다.

로그인시 다음과 같이 토큰을 얻을 수 있었고

refresh를 통해 token을 재발급 받을 수 있다.

일부러 token 내용을 조작하여 틀린 token을 전송하면 다음과 같이 반환한다.

이제 로그인시에는 해당 토큰을 발급 받아 해당 어플리케이션 db에 refresh token을 저장시키고 access token은 재발급 받아서 사용할 수 있게 되었다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글