@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에 넣었던 내용을 확인할 수 있게되었다.
@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이 떨어지며
에러 로그는 다음과 같이 찍히는 걸 확인할 수 있다.
@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 : ~~
}
의 형태로 반환하기 때문에 형태를 동일하게 하기 위함이다.
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은 재발급 받아서 사용할 수 있게 되었다.