앞서 Security Chain에 대한 이론과 실습과정을 진행했다.
Spring Security적용을 하려면 Security Filter의 구조를 알아야 한다
이제 Jwt를 이용해아 인증 인가를 구현해보자.
우선 Dependency를 추가해주자
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
토큰을 발급받는 메서드가있는 클래스이다.
로그인시 CreateToken
를 통해 토큰을 발급받는다.
public class JwtUtil {
public static String getUserName(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().get("userName", String.class);
}
public static boolean isExpired(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().getExpiration().before(new Date());
}
public static String CreateToken(String userName, String key, Long expireTimeMs) {
Claims claims = Jwts.claims(); //일종의 map
claims.put("userName", userName);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireTimeMs))
.signWith(SignatureAlgorithm.HS256,key)
.compact()
;
}
}
JwtFilter를 만들어 준다.
OncePerRequestFilter = 매요청시 한번 작동하라는 뜻이다.
이 클래스에서 권한을 확인하고 접근을 허용할지 말지 결정할 것이다.
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//SecurityConfig에서 Post요청을 전부 막아놓았는데 열여줄지 말지 결정하는 곳이다.
//헤더에서 authentication을 꺼내서 문을 열어줄지 말지 결정 할 것이다.
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info("authorization : {}", authorization);
//token 안보냈다면 null이라면 권한부여하지 않고 리턴한다.
if (authorization == null || !authorization.startsWith("Bearer ")) {
log.error("authorization을 잘못보냈습니다.");
filterChain.doFilter(request,response);
return;
}
//토큰 꺼내기
String token = authorization.split(" ")[1];
//Token Expired 되었는지 여부
if(JwtUtil.isExpired(token,secretKey)){
log.error("token이 만료되었습니다.");
filterChain.doFilter(request,response);
return;
}
// UserName을 Token에서 꺼내기
String userName = JwtUtil.getUserName(token,secretKey);
log.info(userName);
//Role 꺼내기
User user = userService.getUserByUserName(userName);
UserRole role = user.getRole();
//문 열어주기
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken
(userName, null, List.of(new SimpleGrantedAuthority(role.toString())));
//Detail을 넣는다.
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
SecurityFilterChain에서 jwtFilter를 UsernamePasswordAuthenticationFilter보다 먼저 작동하게 설정
@RequiredArgsConstructor
@EnableWebSecurity(debug = true) // request가 올 떄마다 어떤 filter를 사용하고 있는지 출력을 해준다.
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.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/posts/**").authenticated()
.antMatchers(HttpMethod.POST, "/api/v1/users/{id}/role/change").access("hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.PUT, "/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").authenticated()
.antMatchers(HttpMethod.GET, "/api/v1/posts/my").authenticated()
.and()
.exceptionHandling()// 예외처리기능 작동
.authenticationEntryPoint(new CustomAuthenticationEntryPointHandler()) // 인증처리 실패시 처리
.accessDeniedHandler(new CustomAccessDeniedHandler())// 인가처리 실패시 처리
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS = jwt사용하는 경우 씀 : 매번 토큰을 사용하는 개념?
.and()
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class)
.build();
}
}
위 코드에서 .addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
가 UserNamePasswordAuthenticationFilter적용하기 전에 직접만든 JwtFilter를 적용하는 설정이다.
이제 securityFilterChain에서 인증인가 설정에 맞게 설정이 되었다.