Spring Security 란?
스프링 기반의 애플리케이션의 보안을 담당하는 프레임워크이다.필터 기반으로 동작하기 때문에 스프링 MVC와 분리되어 관리 및 동작한다. Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고
seungg8361.tistory.com
Controller에서 @PostMapping("/login")을 처리하지 않고 filter에서 로그인을 처리해 보자.
먼저 Spring Security 관련 작업을 진행해 보자
// Spring Security에서 사용자의 상세 정보를 담는 클래스 -> 실제 인증된 사용자의 정보를 담고있다.
public class UserDetailsImpl implements UserDetails {
private final User user;
// UserDetails 인터페이스의 메서드
@Override
public String getPassword() {return user.getPassword();}
@Override
public String getUsername() {return user.getUsername();}
// 사용자의 권한 정보를 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public boolean isAccountNonExpired() {return true;}
@Override
public boolean isAccountNonLocked() {return true;}
@Override
public boolean isCredentialsNonExpired() {return true;}
@Override
public boolean isEnabled() {return true;}
}
GrantedAuthority : 사용자의 권한을 나타내는 인터페이스로 SimpleGrantedAuthority는 GrantedAuthority의 기본 구현체다. 우선 SimpleGrantedAuthority로 권한을 생성하고, Collection으로 여러 권한을 관리할 수 있다.
// Spring Security에서 사용자 정보를 가져오는 서비스 클래스
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
// UserDetailsService의 loadUserByUsername 메서드를 활용해서 User를 찾음
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
}
인증 과정을 간단하게 보자면
1. 사용자가 로그인을 시도한다.
POST /login
username : user
password : 1234
2. UsernamePasswordAuthenticationFilter 동작
// Spring Security의 기본 인증 필터가 요청을 가로채서 token 생성
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
3. AuthenticationManager 호출
// AuthenticationManager가 인증 처리
public Authentication authenticate(Authentication authentication){
}
4. UserDetailsService 호출해서 UserDetails 구현체 반환
5. 비밀번호 검증이 끝나고 인증이 완료되면 SecurityContext 설정
// 인증이 성공하면 Authentication 객체가 SecurityContext에 저장됨
SecurityContextHolder.getContext().setAuthentication(authentication);
이제 filter에서 로그인을 처리해 보자
JWT 활용하기 (2) - 회원 가입, 로그인 기능 만들기
회원 가입 기능을 구현하기에 앞서 JWT 관련해서 간단한 설정들을 알아보자 JWT 활용하기 (1)인증(Authentication)과 인가(Authorization) 란? 인증(Authentication)과 인가(Authorization) 란?인증 ▼해당 유저가
seungg8361.tistory.com
filter에서 로그인을 처리할 클래스
// UsernamePasswordAuthenticationFilter의 기능들을 사용하기 위해 상속받은 filter 클래스
// 위에 1~3 번까지의 인증과정을 수행하는 filter로 jwt방식을 사용하기 때문에 직접 커스텀해서 사용
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login"); // 생성자 호출 시 로그인 엔드포인트 작성
}
// 로그인을 시도하는 메서드
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// JSON 형태의 String 데이터를 Object로 바꿈. POST, PUT 등의 요청에서 전송된 데이터를 읽기 위해
// getInputStream()을 사용해서 바이트 스트림 형태로 데이터 반환
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
// UsernamePasswordAuthenticationFilter의 getAuthenticationManager를 호출해서 token을 만들어서 반환
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
// 로그인이 성공했을 때 수행되는 메서드, Authentication인증 객체를 가져온다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
// Authentication에는 UserDetailsImpl이 담겨있다. getPrincipal()을 캐스팅
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
response.setStatus(401);
}
}
인가를 진행할 클래스
// 동일한 요청에 대해 필터가 여러번 실행되는 것을 방지하기 위해 OncePerRequestFilter사용
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
// 순수한 토큰을 바로 뽑아냄.
String tokenValue = jwtUtil.getJwtFromHeader(req);
if (StringUtils.hasText(tokenValue)) {
if (!jwtUtil.validateToken(tokenValue)) {
log.error("Token Error");
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
filterChain.doFilter(req, res);
}
// 인증 처리
public void setAuthentication(String username) {
// 보안 주체의 세부 정보를 포함
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String username) {
// 저장되어 있는 인증 객체 가져오기
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
직접 filter를 구현해서 인증, 인가 처리를 진행하였으면 Spring Security Config를 수정해 보자
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
// AuthenticationManager를 세팅하기 위해 주입 받아옴.
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// AuthenticationManager 생성하고 등록하는 메서드
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
// JwtAuthenticationFilter 생성자를 활용하기 위해
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
// getAuthenticationManager() 으로 가져왔으므로 setAuthenticationManager()로 만들어줌
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
http.formLogin((formLogin) ->
formLogin
.loginPage("/api/user/login-page").permitAll()
);
// JwtAuthenticationFilter가 수행되기 전에 jwtAuthorizationFilter를 수행해서 먼저 인가 단계를 거침
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
// UsernamePasswordAuthenticationFilter가 수행되기 전에 jwtAuthenticationFilter를 먼저 실행
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
이제 Controller에서 인증된 유저의 정보를 가져오는 방법을 보자
@GetMapping("/")
// @AuthenticationPrincipal -> Spring Security에서 현재 인증된 사용자의 정보를 받아올 때 사용
public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails){
model.addAttribute("username", userDetails.getUsername());
return "home";
}
이상으로 Spring Security와 JWT를 활용해서 로그인 기능을 구현해 보았습니다.🖐️🖐️
'🔥스파르타 TIL (Spring)' 카테고리의 다른 글
Pageable을 알아보자 (0) | 2025.02.25 |
---|---|
QueryDSL 이란? (0) | 2025.02.25 |
RestTemplate란? (0) | 2025.02.24 |
JWT 활용하기 (2) - 회원 가입, 로그인 기능 만들기 (0) | 2025.02.13 |
Spring Security 란? (0) | 2025.02.10 |