스프링 시큐리티를 적용하면서 크게 6개의 클래스를 사용했습니다.
- WebSecurityConfig
- UserDetailsImpl
- UserDetailsServiceImpl
- JwtUtil
- JwtAuthenticationFilter
- JwtAuthorizationFilter
코드를 올리고, 해당 클래스가 어떤 역할을 맡았는지 이야기해보겠습니다. 코드를 읽으며 아래 설명을 차례대로 보시면 이해하기 편하실 겁니다.
- WebSecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable());
http.sessionManagement((sm) -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests((request) ->
request
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
- @EnableWebSecurity: Spring Security 지원을 가능하게 합니다.
- AuthenticationManager 를 바로 Bean 으로 등록할 수 없어서 AuthenticationConfiguration 을 통해 가져와서 등록합니다.
- JwtAuthenticationFilter 를 Bean 으로 등록합니다. 이 필터에서 인증 처리를 할 것이기에 AuthenticationManager 을 set합니다.
- JwtAuthorizationFilter 를 Bean 으로 등록합니다. 이 필터는 인가 처리를 합니다.
- SecurityFilterChain 을 Bean 으로 등록합니다. 여기서는 CSRF 설정, 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정, 요청의 URL 패턴에 따라 접근 권한 부여 등을 설정합니다.
인증 관리자 (AuthenticationManager) :
UserDetailsService 에게 username 을 전달하고 회원 상세 정보를 요청
SecurityFilterChain 을 Bean 으로 등록하는 이유:
필터 체인을 빈으로 등록해야 스프링 컨테이너가 이를 관리하면서 HTTP 요청이 들어올 때마다 등록된 필터 체인을 사용해 요청을 처리하게 됩니다.
- UserDetailsImpl
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRole role = user.getUserRole();
String authority = role.toString();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
// 사용자는 email 을 id로 하여 로그인 합니다.
@Override
public String getUsername() {
return user.getEmail();
}
public User getUser() {
return user;
}
}
- 회원 정보를 담기 위한 객체를 생성합니다.
- UserDetailsServiceImpl
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
// 사용자는 email 을 id로 하여 로그인 합니다.
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email).orElseThrow(() ->
new UsernameNotFoundException(email + "을 찾지 못했습니다."));
return new UserDetailsImpl(user);
}
}
- 회원 DB 에서 회원 조회합니다.
- 조회된 회원 정보(user) 를 UserDetails 로 변환합니다.
- JwtUtil: Jwt 방식을 채택했기 때문에 필요한 클래스
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createToken(Long userId, String email, String nickname, UserRole userRole) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("nickname", nickname)
.claim("userRole", userRole)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
throw new ServerException("Not Found Token");
}
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, error :{}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, error :{}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, error :{}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, error :{}", e.getMessage());
}
return false;
}
}
- Jwt 와 관련된 기능을 담어놓은 클래스입니다.
- 토큰 생성, 토큰 앞 접두사 탈착, 토큰으로부터 사용자 정보 획득, 토큰 검증 등의 기능이 있습니다.
- JwtAuthenticationFilter
@Slf4j(topic = "인증 및 Jwt 발급")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/auth/signin");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
SigninRequest signinRequest = new ObjectMapper().readValue(request.getInputStream(), SigninRequest.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(signinRequest.getEmail(), signinRequest.getPassword(), null)
);
} catch (IOException e) {
log.error("로그인 에러:{}", e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("로그인 성공, Jwt 발급");
Long userId = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getId();
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
String nickname = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getNickname();
UserRole role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUserRole();
String jwt = jwtUtil.createToken(userId, username, nickname, role);
response.addHeader("Authorization", jwt);
}
}
- 인증 필터입니다.
- 로그인(인증) 요청 URL 을 설정합니다.
- AuthenticationManager 에 의해 인증이 진행됩니다. 로그인 - 인증 처리는 사용자가 입력한 아이디와 비밀번호가 UsernamePasswordAuthenticationToken 에 담깁니다. 이것과 위에서 UserDetails 에 담은 아이디와 비밀번호가 매칭되며 이루어 집니다.
- 인증에 성공하면 회원 정보를 담은 Jwt를 발급합니다.
- JwtAuthorizationFilter
@Slf4j(topic = "인가")
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
token = jwtUtil.substringToken(token);
log.info(token);
if (!jwtUtil.validateToken(token)) {
log.error("Validation Failed");
return;
}
Claims claims = jwtUtil.extractClaims(token);
setAuthentication(claims.getSubject());
}
filterChain.doFilter(request, response);
}
private void setAuthentication(String userId) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(userId);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
private Authentication createAuthentication(String userId) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
- 인가 필터입니다.
- HTTP 요청이 인가 필터를 탄다는 것은, 해당 요청이 Jwt를 가진 -로그인이 완료된- 사용자가 보낸 요청이라 할 수 있습니다.
- jwtUtil.extractClaims(token): 토큰에 담긴 사용자 정보를 읽습니다.
- setAuthentication(claims.getSubject()): 인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.
SecurityContext:
인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.SecurityContext는 SecurityContextHolder 로 접근할 수 있습니다.
인증 객체 Authentication:
현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다. principal, credentials, authorities 로 구성되며, 일반적으로 principal 에 UserDetails 가 담기게 됩니다.
'Spring > Spring 문법' 카테고리의 다른 글
Transaction Propagation (0) | 2024.11.25 |
---|---|
QueryDsl - Projections 의 4가지 방식 (0) | 2024.11.21 |
QueryDSL이란 무엇일까?? (0) | 2024.11.14 |
@RequestParam 에서 매개 변수에 null 넣기 (0) | 2024.11.14 |
Java Spring properties (1) | 2024.10.26 |