Spring-JWT(JSON Web Token) - 4. JWT 다루기

4. JWT 다루기

목차

JWT를 다루기 위한 Util Class만들기

JwtUtil 객체는 Jwts.builder를 이용해 JWT를 생성하고 Jwts.parserBuilder를 이용해 Token을 JWT로 변환시켜 데이터를 가져오도록 한다.

Jwts.builder(JwtBuilder)

  • setHeader
    • JWT의 Header 에 대한 설정을 위한 메소드
  • setSubject, setExpiration, setIssuer, setAudience, setNotBefore, setIssuedAt
    • Registed Payload는 각각에 해당하는 set 메소드들이 제공된다.
  • setClaims
    • JWT의 Claim 데이터 를 추가하기 위한 메소드
  • signWith
    • Header와 Payload를 갖고 Secret Key 로 서명한다.
    • 암호화 알고리즘으로는 보통 HMAC or RSA 알고리즘을 사용한다.

Jwts.parserBuilder(JwtParser)

  • setSigningKey
    • 데이터 위변조 확인을 위해 secretKey를 이용해 JWS에 대한 유효성 검증을 한다.
    • secretKey를 이용한 검증 실패시 해당 JWT는 사용하지 못한다.
  • parseClaimsJws
    • token을 JWS로 파싱해준다.

JwtUtil.java

@Component
public class JwtUtil {
private final String secretKey = "ThisIsA_SecretKeyForJwtExample";


public String generateToken(String username) {
String JSONWebToken = Jwts.builder()
.setHeader(createHeader())
.setClaims(createClaims(username))
.setExpiration(createExpireDateForOneYear())
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();

return JSONWebToken;
}

private Map<String, Object> createHeader() {
Map<String, Object> header = new HashMap<>();

header.put("typ", "JWT");
header.put("alg", "HS256");
header.put("regDate", System.currentTimeMillis());

return header;
}

private Map<String, Object> createClaims(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
}

private Date createExpireDateForOneYear() {
// 토큰 만료시간은 30일으로 설정
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 30);
return calendar.getTime();
}

private Claims getAllClaims(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}

public String getUsername(String token) {
Claims claims = getAllClaims(token);
String username = claims.get("username", String.class);
return username;
}

public Date getExpiration(String token) {
Claims claims = getAllClaims(token);
return claims.getExpiration();
}

public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

private Boolean isTokenExpired(String token) {
return getExpiration(token).before(new Date());
}
}

JWT를 이용한 인증을 위해 새로운 Filter정의하기

OncePerRequestFilter를 상속 받아 JWT로 인증을 진행하는 JwtAuthenticationFilter를 생성한다.
JwtAuthenticationFilter에서는 요청이 들어왔을 때 Token이 있는지 확인하고 해당 Token을 이용해 인증을 진행한 후 AuthenticationToken을 생성해 SecurityContextHolder에 저장한다.

  • UsernamePasswordAuthenticationToken
    • 사용자의 Username과 Password를 이용하여 AuthenticationToken을 생성한다.

JwtAuthenticationFilter.java

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

@Autowired
private JwtUtil jwtUtil;
@Autowired
private CustomUserDetailsService service;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Http 요청이 들어왔을 때 header를 검사해 Authorization 값을 가져온다.
String authorizationHeader = request.getHeader("Authorization");

String token = null;
String username = null;

// 인증 토큰을 받았을 경우
// Bearer 토큰인지 확인한다.
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(token);
}

// Bearer 토큰으로부터 추출한 username을 이용해
// 해당 유저가 등록된 유저인지 검증한다.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

UserDetails userDetails = service.loadUserByUsername(username);

if (jwtUtil.validateToken(token, userDetails)) {

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}

filterChain.doFilter(request, response);
}
}

로그인을 위한 Controller 추가

Username과 Password를 이용해 /api/user/login 경로로 인증을 시도하면 AuthenticationManager 객체를 이용해 인증을 진행하고 인증이 성공적으로 완료 되면 JWT를 생성해 Header에 JWT를 넣어 Status code : 200과 함께 반환한다.

JwtController.java

....
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;

@PostMapping("/login")
public ResponseEntity login(@RequestBody AuthRequest authRequest) throws Exception {
String username = authRequest.getUsername();
String password = authRequest.getPassword();

try {
// AuthnRequest로부터 받은 username과 password를 이용하여 UsernamePasswordAuthenticationToken을 생성한 후
// 생성된 토큰을 이용해 인증을 시도한다.
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception ex) {
throw new Exception("inavalid username/password");
}

String token = jwtUtil.generateToken(authRequest.getUsername());

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " +token);

// 인증이 정상적으로 이루어진 경우 username을 이용해 JWT를 생성해 반환한다.
return new ResponseEntity(httpHeaders, HttpStatus.OK);
}

권한을 갖고 접근하기 위한 Controller 추가

  • 반환받은 JWT를 갖고 /api/user/access 경로로 접근하면 Permission to Access 문구가 반환된다.

JwtController.java

@GetMapping("/access")
public String accessByToken(){
return "Permission to Access";
}

Security 설정 추가하기

AuthenticationManagerBuilder객체의 userDetailsService 메소드를 이용해 사용자가 정의한 CustomUserDetailsService 객체를 사용하도록 등록한다.

SecurityConfig.java

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 사용자가 정의한 UserDetailsService를 사용한다.
auth.userDetailsService(customUserDetailsService);
}

회원가입을 위한 /api/user/signup 경로와 로그인을 위한 /api/user/login 경로는 인증전에 접근하기 위한 경로이므로 권한 없이 접근 가능해야 한다. 그외 모든 경로는 인증된 사용자만 접근 할 수 있도록 설정한다.

사용자 정의 Filter인 JwtAuthenticationFilter를 Spring Filter에 추가한다.

SecurityConfig.java

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/user/login").permitAll()
.antMatchers("/api/user/signup").permitAll()
.anyRequest().authenticated()
.and()
// 토큰을 활용하면 세션이 필요 없으므로 STATELESS로 설정하여 Session을 사용하지 않는다.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// form 기반의 로그인에 대해 비활성화 한다.
.formLogin().disable()
// 새롭게 정의한 Filter를 등록한다.
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
Share