이제 Spring Security를 사용해보자
package com.github.backproject.config.security;
import com.github.backproject.web.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtProvider jwtProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer:: disable)
.rememberMe(AbstractHttpConfigurer::disable)
.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize.requestMatchers("/resources/static/**", "/api/*").permitAll())
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomerAccessDeniedHandler()))
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager (AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
SecurityFilterChain을 빈으로 등록을 해주고 AuthenticationManager도 빈으로 등록을 해준다.
package com.github.backproject.web.filter;
import com.github.backproject.config.security.JwtProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwtToken = jwtProvider.resolveToken(request);
if(jwtToken != null && jwtProvider.validateToken(jwtToken)) {
Authentication auth = jwtProvider.getAuthentication(jwtToken);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
UsernamePasswordAuthenticationFilter를 실행하기 전에 토큰을 먼저 확인하고 넣어주는 커스텀 필터를 만들어서 넣어준다.
package com.github.backproject.respository.UserDetails;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CustomerUserDetail implements UserDetails {
private Integer user_id;
private String email;
private String password;
private List<String> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return null;
}
public String getEmail() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
사용자의 정보를 담는 인터페이스이다.
인터페이스를 구현해주어야 한다.
package com.github.backproject.service.security;
import com.github.backproject.respository.UserDetails.CustomerUserDetail;
import com.github.backproject.respository.roles.RolesEntity;
import com.github.backproject.respository.userPrincipal.UserPrincipalEntity;
import com.github.backproject.respository.userPrincipal.UserPrincipalRepository;
import com.github.backproject.respository.userPrincipalRoles.UserPrincipalRolesEntity;
import com.github.backproject.service.exceptions.NotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
@Primary
@RequiredArgsConstructor
@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
private final UserPrincipalRepository userPrincipalRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserPrincipalEntity userPrincipalEntity = userPrincipalRepository.findByEmailFetchJoin(email)
.orElseThrow(() -> new NotFoundException("email에 해당하는 UersPrincipal이 없습니다"));
CustomerUserDetail customerUserDetail = CustomerUserDetail.builder()
.user_id(userPrincipalEntity.getUserEntity().getUserId())
.email(userPrincipalEntity.getEmail())
.password(userPrincipalEntity.getPassword())
.authorities(userPrincipalEntity.getUserPrincipalRoles().stream().map(UserPrincipalRolesEntity::getRolesEntity).map(RolesEntity::getName).collect(Collectors.toList()))
.build();
return customerUserDetail;
}
}
DB에서 유저의 정보를 가져오는 역할을 하는 인터페이스이다.
DB에서 유저의 정보를 가지고 와서 UserDetailes를 구현한 구현체에 담아 주어야 한다.
public String login(Login loginRequest) {
try {
String email = loginRequest.getEmail();
String password = loginRequest.getPassword();
Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password));
SecurityContextHolder.getContext().setAuthentication(auth);
UserPrincipalEntity userPrincipalEntity = userPrincipalRepository.findByEmailFetchJoin(email)
.orElseThrow(() -> new NotFoundException("UserPrincipal을 찾을 수 없습니다."));
UserEntity userEntity = userRepository.findByEmail(email).orElseThrow(
() -> new NotFoundException("userEntity를 찾을 수 없습니다")
);
List<String> roles = userPrincipalEntity.getUserPrincipalRoles()
.stream().map(UserPrincipalRolesEntity::getRolesEntity).map(RolesEntity::getName).collect(Collectors.toList());
return jwtProvider.createToken(userEntity, roles);
} catch (Exception e) {
throw new NotFoundException("로그인 할 수 없습니다");
}
}
이메일과 비밀번호로 UsernamePasswordAuthenticationToken을 만들고 이 토큰을 AuthenticationManager에게 전달하여 Authentication 인증용 객체를 받는다.
그리고 이를 SecurityContextHolder에 담아주어서 사용을 한다.