public class User {
...
@Enumerated(EnumType.STRING)
private UserRole role;
}
🔹 @Enumerated(EnumType.STRING)
: enum 이름을 DB에 저장
public enum UserRole {
ADMIN, USER;
}
🔹 enum으로 ADMIN, USER 2가지 Role 생성
SecurityConfig
...
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.token.secret}")
private String secretKey;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
.antMatchers(HttpMethod.POST, "/api/v1/**").authenticated() // 접근 요청 막기
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
.and()
.addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.build();
}
}
🔹 .antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll()
: 2개의 uri로 오는 요청은 모두 허용
🔹 .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated()
: 이후에 오는 /api/v1/**
형태의 POST 요청은 모두 막기
🔹 .addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
: UsernamePasswordAuthenticationFilter 적용하기 전에 JWTTokenFilter 적용
JwtTokenFilter
package com.hospitalreview.configuration;
import com.hospitalreview.domain.User;
import com.hospitalreview.service.UserService;
import com.hospitalreview.utils.JwtTokenUtil;
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 javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 권한이 닫힌 상태에서 조건에 따라 권한을 줄지 말지 선택
// Header의 Authorization 가져오기
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization:{}", authorizationHeader);
// 토큰이 없거나 Bearer로 시작하지 않는 경우
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// token 분리
String token;
try {
token = authorizationHeader.split(" ")[1];
} catch (Exception e) {
log.error("token 추출에 실패했습니다.");
filterChain.doFilter(request, response);
return;
}
// 토큰 유효 기간이 만료되었을 경우
if(JwtTokenUtil.isExpired(token, secretKey)) {
filterChain.doFilter(request, response);
return;
}
// token의 Claim에서 userName 꺼내기
String userName = JwtTokenUtil.getUserName(token, secretKey);
log.info("userName:{}", userName);
// UserDetail 가져오기
User user = userService.getUserByUserName(userName);
log.info("userRole:{}", user.getRole());
// 권한 부여, Role 바인딩
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 권한 부여
filterChain.doFilter(request, response);
}
}
🔹 extends OncePerRequestFilter
: 토큰을 가지고 요청할때마다 Check
🔹 request.getHeader(HttpHeaders.AUTHORIZATION);
: Header의 AUTHORIZATION의 값을 가져옴
🔵 예외 경우
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer "))
: 토큰이 없거나, Bearer
로 시작하지 않으면 예외 처리Bearer
: 토큰의 하나의 형태token = authorizationHeader.split(" ")[1];
: 공백으로 분리한 후 인덱스가 1인 값만 가져오는 이유(앞에 붙은 Bearer를 제거하기 위해 ➡ 토큰만 가져오기 위해)if(JwtTokenUtil.isExpired(token, secretKey))
: 해당 토큰의 사용기간이 유효한지 확인(해당 메소드는 JwtTokenUtil 클래스에서 다시 설명)🔴 filterChain.doFilter(request, response)
: 다음 필터를 가르킴 (권한 부여와 상관없음 ➡ 예외 처리에 사용하면 권한을 주지않고 다음 필터로 넘긴 것)
🔵 예외가 발생하지 않은 경우
userName 가져오기
JwtTokenUtil.getUserName(token, secretKey)
: token의 claim에서 userName 가져오기
UserDetail 가져오기
가져온 userName으로 DB에서 해당 정보 가져오기
Role 바인딩 및 권한 부여
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())));
: userName으로 해당 role을 주기
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
: 권한 부여
이후 다음 필터로 보내기
...
public class JwtTokenUtil {
// secret를 사용하여 token을 parser
private static Claims extractClaims(String token, String key) {
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
}
public static String getUserName(String token, String key) {
return extractClaims(token, key).get("userName", String.class);
}
// token 사용기간이 유효한지 Check
public static boolean isExpired(String token, String key) {
Date expiredDate = extractClaims(token, key).getExpiration(); // expire timestamp를 return함
return expiredDate.before(new Date()); // 현재보다 전인지 check를 합니다.
}
...
}
🔹 extractClaims
: token을 secretKey로 열고 정보 꺼내기
🔹 getUserName
: token에서 userName을 String 형태로 가져오기
🔹 isExpired
: 리턴이 boolean형태로 만료 시간을 가져온 후, 유효하다면 true, 아니라면 false 리턴
@RestController
@RequestMapping("/api/v1/reviews")
@Slf4j
public class ReviewController {
@PostMapping
public String write(@RequestBody ReviewCreateRequest dto, Authentication authentication) {
log.info("Controller user:{}", authentication.getName());
return "리뷰 등록에 성공했습니다.";
}
}
🔴 최종 정리
/api/v1/users/login
에 접근할 때, userName과 password로 토큰 받기ReviewController에서 Authentication
: 권한을 부여받은 계정에 대한 정보를 가져올 수도 있음