SecurityConfig
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request ->
request
.requestMatchers(jwtUtil.allowedUrls).permitAll()
.anyRequest().authenticated())
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session을 사용하지 않음
)
.addFilterBefore(jwtFilter, RequestCacheAwareFilter.class)
.build();
}
}
csrf :
Cross-Site Request Forgery의 약어
"사이트 간 요청 위조"를 의미
공격자가 사용자가 로그인한 웹사이트를 통해 사용자가 모르는 상황에서 원치 않는 동작을 수행하게 하는 보안 취약점에 대한 보안 설정
authorizeRequests : 요청에 의한 보안검사 설정
- .requestMatchers(jwtUtil.allowedUrls).permitAll() // 특정 url이 매칭되면 검사하지 않고 넘긴다.
- .anyRequest().authenticated() / /어떤 요청에도 보안검사를 한다.
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) :
// session을 사용하지 않음
.addFilterBefore(jwtFilter, RequestCacheAwareFilter.class) : RequestCacheAwareFilter 전에 jwtFilter 적용
@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final JwtProvider jwtProvider;
private AuthenticationManager authenticationManager;
@PostConstruct
public void init() {
// 인증 객체를 생성하는 provider 를 주입하는 메서드
this.authenticationManager = new ProviderManager(jwtProvider);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
//해당 URL에 속하면 필터를 수행하지 않음
return Arrays.stream(jwtUtil.allowedUrls).anyMatch(item -> item.equalsIgnoreCase(request.getServletPath())); // true면 fileter 안 탐
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// Jwt 토큰을 가져오는 로직
String token = jwtUtil.extractJwtFromRequest(request);
// 토큰이 존재하고 토큰의 유효성이 통과되면 수행하는 로직
if (token != null && jwtUtil.validateToken(token)) {
//인증 객체를 생성
MyTokenAuthentication authenticateRequest = new MyTokenAuthentication(token);
Authentication authenticationResult = authenticationManager.authenticate(authenticateRequest);
if (!authenticateRequest.isAuthenticated()) {
throw new BadCredentialsException("인증이 실패하였습니다.");
}
//context에 인증 객체 넣는 로직
SecurityContextHolder.getContext().setAuthentication(authenticationResult);
}
//예외가 발생했을때의 코드
} catch (ExpiredJwtException e) {
SecurityContextHolder.clearContext();
response.sendError(401, e.getMessage());
// void 메서드 임으로 예외가 발생하면 return
return;
}catch (Exception e) {
SecurityContextHolder.clearContext();
response.setStatus(400);
// void 메서드 임으로 예외가 발생하면 return
return;
}
//로직을 예외 없이 통과했으면 다음 필터로 넘어가는 메서드 호출
filterChain.doFilter(request, response);
}
}
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtProvider implements AuthenticationProvider {
private final JwtUtil tokenService;
//인증 객체를 생겅하는 로직
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.debug("토큰 인증을 수행합니다.");
if (supports(authentication.getClass())) {
MyTokenPayload verified = tokenService.verify(((MyTokenAuthentication) authentication).getToken());
authentication.setAuthenticated(true);
((MyTokenAuthentication) authentication).setPayload(verified);
return authentication;
} else {
throw new BadCredentialsException("지원하지 않는 인증입니다.");
}
}
//특정 Authentication 객체를 지원하는지 확인하는 로직
@Override
public boolean supports(Class<?> authentication) {
return MyTokenAuthentication.class.equals(authentication);
}
}
@Service
@RequiredArgsConstructor
public class JwtUtil {
// 비밀 키 값
@Value("${jwt.secret}")
private String secret;
// 토큰의 만료 기간
@Value("${jwt.expiration}")
private long expiration;
private SecretKey secretKey;
// 필터 및 security 를 태우지 않을 URL 의 배열
public final String[] allowedUrls = {
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
/* swagger v3 */
"/v3/api-docs/**",
"/swagger-ui/**",
"/",
"/user/**",
"/api/first_categories/**",
"/error"
};
//비밀 키 값을 SecretKey 객체로 만드는 메서드
@PostConstruct
public void init() {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
//토큰으르 발급하는 로직
public String generate(MyTokenPayload payload) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.claim("id", payload.getUserId())
.claim("username", payload.getUsername())
.claim("roles", payload.getRoles())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey)
.compact();
}
// 토큰을 열어보고 유저의 정보를 담는 로직
public MyTokenPayload verify(String token) throws BadCredentialsException {
try {
Claims payload = Jwts.parserBuilder()
.setSigningKey(secret.getBytes(StandardCharsets.UTF_8))
.build()
.parseClaimsJws(token)
.getBody();
Long id = payload.get("id", Long.class);
String username = payload.get("username", String.class);
List<String> roles = (List<String>) payload.get("roles", List.class);
MyTokenPayload myTokenPayload = new MyTokenPayload(id, username, roles);
return myTokenPayload;
} catch (Exception e) {
e.printStackTrace();
throw new BadCredentialsException("올바르지 않은 토큰입니다.");
}
}
// 토큰을 열어보고 username을 가져오는 로직
public String extractUsername(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
}
// 토큰의 유효성을 검증하는 로직
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
throw e;
} catch (Exception e) {
return false;
}
}
//헤더에서 토큰을 빼오는 로직
public String extractJwtFromRequest(HttpServletRequest request) {
// Extract JWT from the Authorization header
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
@Getter
@RequiredArgsConstructor
//인증 객체
public class MyTokenAuthentication implements Authentication {
//토큰
private final String token;
//유저 정보를 담아놓은 payload
@Setter
private MyTokenPayload payload = new MyTokenPayload();
//인증을 받았는지 여부
@Setter
private boolean authenticated = false;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return payload.getRoles().stream()
.map(SimpleGrantedAuthority::new)
.toList();
}
@Override
public Object getCredentials() {
return null;
}
@Override
public MyTokenPayload getDetails() {
return payload;
}
@Override
public Long getPrincipal() {
return payload.getUserId();
}
@Override
public String getName() {
return payload.getUsername();
}
}
'Spring' 카테고리의 다른 글
Security 인가(Authorization) 적용 (1) | 2024.03.16 |
---|---|
Mappstruct 사용 (0) | 2024.03.11 |
SpringSecurity 인증 방식 (0) | 2024.03.02 |
SpringSecurity 인증 (0) | 2024.03.01 |
SpringSecurity 아키텍쳐이해하기 (0) | 2024.02.24 |