구현
JwtAuthenticationFilter
SpringSecurityFilter
에 등록할 필터 중 하나로,- 요청에 token이 포함되어 있는지,
- 포함되어 있다면 유효한지,
- 유효하다면 DB에 조회하여
Authentication
객체를 만들고,SecurityContextHolder
를 통해서 저장하는 역할을 한다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtilities jwtUtilities;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String jwt = jwtUtilities.getToken(request);
if (StringUtils.hasText(jwt) && jwtUtilities.validateToken(jwt)) {
String email = jwtUtilities.extractUsername(jwt);
Collection<? extends GrantedAuthority> grantedAuthorities = jwtUtilities.extractAuthorities(jwt);
UserDetails user = new User(email, "", grantedAuthorities);
Authentication authentication = new UsernamePasswordAuthenticationToken(user, jwt, grantedAuthorities);
log.info("authenticated user with email: {}", email);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
SpringSecurityConfig
- SpringSecurity에 대한 전반적인 설정을 담당하는 클래스다.
- JWT로 인증할 것이기 때문에 세션, login form등 기능을 비활성화 한다.
- CSRF 비활성화
- 세션 관리 기능 비활성화(STATELESS로 설정)
- SpringSecurity에서 기본 제공하는 formLogin 기능 비활성화
- API별 authentication, authorization 설정 추가
UsernamePasswordAuthenticationFilter
동작 전에JwtAuthenticationFilter
가 동작하도록 필터 추가-
UsernamePasswordAuthenticationFilter
: formLogin 기능 사용시 동작하는 필터로, formLogin을 비활성화 했으므로 사용하지 않는다. 실제로 bp를 걸어봐도 걸리지 않는다.
-
- 설정에 필요한 여러 Bean들을등록한다.
AuthenticationManager
:UsernamePasswordAuthenticationToken
을 사용하여 실제 정보와 일치하는지 authenticate해주는 클래스다.AuthenticationManagerBuilder
가 Bean으로 등록되어 있기 때문에AuthenticationManagerBuilder.getObject()
로 가져올 수도 있지만, 편의를 위해 Bean으로 등록한다.
PasswordEncoder
: 회원가입 시 password 부분을 암호화 시켜줄 인코더 클래스다. SpringSecurity에서 기본으로 제공하는BCryptPasswordEncoder
를 등록시킨다.
@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("WebSecurityConfig.filterChain");
/* seucrity 6.0부터 설정방식이 변경됨 */
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable).httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
.requestMatchers(HttpMethod.POST, "/api/users/login").permitAll()
.anyRequest().authenticated());
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserDetailsServiceImpl
UserDetailsService
인터페이스의 구현체로,UserRepository
를 사용하여 얻은 비지니스로직상의User
클래스를UserDetails
객체로 변환시켜서 반환해주는 역할을 한다.- 프로젝트 설계에 따라
User
클래스를UserDetails
클래스의 자식클래스로 구현할 수도 있는데, 경우에 따라 다르기 때문에 따로Service
를 구현해두었다.
- 프로젝트 설계에 따라
@RequiredArgsConstructor
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.map(this::createUserDetails)
.orElseThrow(() -> new UsernameNotFoundException("User not found!"));
}
private UserDetails createUserDetails(User user) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(user.getEmail(),
user.getPassword(),
grantedAuthorities);
}
}
JwtUtilities
- JWT 토큰을 생성, 검증, 토큰의 정보 추출 등의 기능을 제공하는 유틸리티성 클래스다.
@Slf4j
@Component
public class JwtUtilities {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.token-valid-time-in-seconds}")
private Long jwtExpiration;
private Key key;
@PostConstruct
public void postConstruct() {
this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String email = extractUsername(token);
return (email.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Collection<? extends GrantedAuthority> extractAuthorities(String token) {
return extractClaim(token, claims -> {
if (Objects.equals(claims.get("role").toString(), "")) return List.of();
return Arrays.stream(claims.get("role").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
});
}
public String generateToken(Authentication authentication) {
/* Authority 정보를 String 으로 변환 */
String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(
Collectors.joining(","));
return generateToken(authentication.getName(), authorities);
}
public String generateToken(String email, String authorities) {
return Jwts.builder()
.setSubject(email)
.claim("role", authorities)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(Date.from(Instant.now().plus(jwtExpiration, ChronoUnit.MILLIS)))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.info("Invalid JWT signature.");
log.trace("Invalid JWT signature trace: {}", e);
} catch (MalformedJwtException e) {
log.info("Invalid JWT token.");
log.trace("Invalid JWT token trace: {}", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
log.trace("Expired JWT token trace: {}", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
log.trace("Unsupported JWT token trace: {}", e);
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
log.trace("JWT token compact of handler are invalid trace: {}", e);
}
return false;
}
public String getToken(HttpServletRequest httpServletRequest) {
final String bearerToken = httpServletRequest.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Token ")) {
return bearerToken.substring(6);
} // The part after "Token "
return null;
}
}
API에서 사용
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping
public ResponseEntity<String> getUser(@AuthenticationPrincipal UserDetails userDetails) {
System.out.println("userDetails = " + userDetails);
return ResponseEntity.ok().build();
}
}
@AuthenticationPrincipal
어노테이션을 통해서 쉽게UserDetails
를 불러올 수 있다.- 이
UserDetails
는 이전에JwtAuthenticationFilter
에서SecurityContextHolder.
getContext
().setAuthentication(authentication);
를 통해 context에 넣어주었던Authentication
객체 안에 들어있는 객체다.
- 실제 User정보가 아니라 Jwt안에 있는 email을 담아오기 위한 객체이므로 주의해야한다.
- 이
Uploaded by N2T
(23.06.15 13:42)에 작성된 글 입니다.