JWTFilter 구현
package com.example.stock.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String TOKEN_HEADER = "Authorization"; // 토큰 헤더 키
public static final String TOKEN_PREFIX = "Bearer "; // value : 인증 Prefix
private final TokenProvider tokenProvider; // 토큰의 유효성 검증
// 서블릿 거치기 전에 필터를 먼저 거친다.
// 요청이 들어올때마다 컨트롤러 보다 먼저 시작되면서
// 요청의 헤더에 토큰이 있는지 확인하고, 토큰이 유효하다면 인증정보를 context에 담는다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = resolveTokenFromRequest(request); // request로 부터 헤더를 꺼내온다.
if(StringUtils.hasText(token) & tokenProvider.validateToken(token)){ // 토큰 유효성 검증
Authentication auth = tokenProvider.getAuthenticate(token); // 사용자 정보, 사용자 권한 정보 포함
SecurityContextHolder.getContext().setAuthentication(auth);
}
// 유효하지 않을 경우
filterChain.doFilter(request, response); // 필터가 연속적으로 실행될 수 있게 해줌
}
private String resolveTokenFromRequest(HttpServletRequest request){
String token = request.getHeader(TOKEN_HEADER);
if(!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)){ // 토큰 존재 & 토큰 프리픽스로 시작
return token.substring(TOKEN_PREFIX.length()); // 토큰부위 도려내주기
}
return null;
}
}
Controller로 요청이 들어오기 전에, 제일 먼저 앞단에서 Filter를 거치고, Servlet을 거치고, Interceptor를 거치고 AOP를 거치고 Controller로 가게 된다. 응답도 마찬가지이다.
OnePerRequestFilter : 한 요청당 한번 필터가 실행이된다.
필터에서 요청이 들어올때마다 요청에 토큰이 포함되어 있는지 확인하고, 그 토큰이 유효한지 아닌지에 대해서도 판별해준다.
TokenProvider
package com.example.stock.security;
import com.example.stock.service.MemberService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
@Component
@RequiredArgsConstructor
public class TokenProvider {
private static final String KEY_ROLES = "roles"; // 상수값은 요렇게
private static final long TOKEN_EXPIRE_TIME = 1000 * 60 * 60; // 1hour
@Value("{spring.jwt.secret}")
private String secretKey;
private final MemberService memberService;
/*
* 토큰을 생성(발급)
* @param username
* @param rules
* @return
* */
public String generateToken(String username, List<String> rolse) {
// 사용자 권한 정보를 저장하기 위한 claim 생성
Claims claims = Jwts.claims().setSubject(username);
claims.put(KEY_ROLES, rolse); // key,value로 저장
Date now = new Date();
Date expiredDate = new Date(now.getTime() + TOKEN_EXPIRE_TIME);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now) // 토큰 생성 시간
.setExpiration(expiredDate) // 토큰 만료 시간
.signWith(SignatureAlgorithm.HS512, secretKey) // 사용할 암호화 알고리즘, 비밀키
.compact();
}
public Authentication getAuthenticate(String jwt){ // jwt 토큰으로 부터 인증 정보 가져오기
UserDetails userDetails = memberService.loadUserByUsername(getUsername(jwt));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); // 스프링 지원
}
public String getUsername(String token){
return parseClaims(token).getSubject();
}
public boolean validateToken(String token){ // 토큰이 유효한가
if(!StringUtils.hasText(token)){ // 빈값인 경우
return false;
}
return parseClaims(token).getExpiration().before(new Date()); // 토큰의 만료시간이 현재 시간의 이전인지 아닌지에 대해 알려준다.
}
private Claims parseClaims(String token){
try{
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
}catch(ExpiredJwtException e){
return e.getClaims();
}
}
}
getAuthenticatin 메소드를 추가로 선언해준다.(JWT 토큰으로부터 인증 정보를 가져오는 메소드)
filter 실행 -> 요청 헤더 토큰 확인 -> 토큰 유효시 인증정보를 Context에 담는다.
SecurityConfiguration
package com.example.stock.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Slf4j
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration implements AuthenticationManager{
private final JwtAuthenticationFilter authenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic(HttpBasicConfigurer::disable) // RESTFul api
.csrf(CsrfConfigurer::disable)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize ->
authorize.
requestMatchers(
new AntPathRequestMatcher("/**/signup"),
new AntPathRequestMatcher("/**/signin")
).permitAll() // signin, signup에는 모든 권한을 주겠다.
)
.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring(). // 인증 정보를 따로 필요로 하지 않는 경로
requestMatchers(
new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/company/**")
)
);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return authentication;
}
}
권한 제약을 거는 방법
권한 Enum
package com.example.stock.model.constants;
public enum Authority {
ROLE_READ,
ROLE_WRITE;
}
ROLE_ 뒷부분 : Spring Security에서 지원해주는 기능을 사용하고자 함
@GetMapping // 회사 리스트 조회
@PreAuthorize("hasRole('READ')")
public ResponseEntity<?> searchCompany(final Pageable pageable){
// final 키워드로 임의 변환 방지
// Pagination 요청 정보를 담기위한 추상 인터페이스
Page<CompanyEntity> companies = companyService.getAllCompany(pageable);
// 회사명은 전부 다 가져와야 하는가?
// 가져와야하는 데이터가 클수록 네트워크 대역푝 증가
// client 단에서 한번에 보여줄 수 있는 아이템의 갯수는 정해져 있다!
// paging 사용
return ResponseEntity.ok(companies);
}
@PostMapping // 회사 저장
//@PreAuthorize("hasRole('WRITE')") // 쓰기권한만 있는 Authroity enum 권한
public ResponseEntity<?> addCompany(@RequestBody Company request){
String ticker = request.getTicker().trim();
if(ObjectUtils.isEmpty(ticker)){
throw new RuntimeException("ticker is empty");
}
Company company = this.companyService.save(ticker);
companyService.addAutocompleteKeyword(company.getName());
return ResponseEntity.ok(company);
}
@PreAuthorize를 사용해서 입맞에 맡게 권한을 부여해준다.
'Project' 카테고리의 다른 글
[stock] 완성도 높이기 - 예외 처리 (0) | 2023.09.27 |
---|---|
[stock] 회원 관리 - 회사 삭제 기능 구현 (0) | 2023.09.25 |
[stock] 회원 관리 - 컨트롤러 구현 (0) | 2023.09.21 |
[stock] 회원 관리 - JWT (0) | 2023.09.21 |
[stock] 회원관리 - 회원가입 (0) | 2023.09.21 |