TIL

+27 Spring Security 로그인

JAVABOZA 2023. 7. 12. 23:24

 

Security 적용 전

 

Security 적용 후

▶ 인증 인가 성공 시 Controller로 사용자정보 요청 전달 (UserDetails)

▶ 인증 인가 실패 시 요청 전달 x / client에게 Error Response 보냄

 

 

 

☞Security 적용 후 이미지의 노란박스 상세

 

먼저

① 로그인시도 (username, password)정보를 HTTP body로 전달(POST요청)

② Authentication Manager(인증관리자) UserDetailsService에게 username을 전달하고 회원상세 정보 요청

③ UserDetailsService에서 요청한 정보(username)를 회원DB에 회원상세정보 요청 

    ㄴ 요청한 정보가 없을 땐 Error 발생

User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

     ㄴ 정보가 있을 땐 UserDetails로 변환

UserDetails userDetails = new UserDetailsImpl(user)

 

반환 후 UserDetails를 Authentication Manager(인증관리자) 에게 전달

 

Authentication Manager(인증관리자) 가 인증처리

@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getTokenFromRequest(req);

        if (StringUtils.hasText(tokenValue)) {
            // JWT 토큰 substring
            tokenValue = jwtUtil.substringToken(tokenValue);
            log.info(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());
    }
}

 

 

● 로그인 구현

 

먼저 config파일에서 로그인 처리 url 설정해야 한다.

package com.sparta.springauth.config;

import com.sparta.springauth.jwt.JwtAuthorizationFilter;
import com.sparta.springauth.jwt.JwtAuthenticationFilter;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
        this.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 {
        // 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 접근 허용 설정
                        // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .requestMatchers("/api/user/**").permitAll() 
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );
		//loginPage()안에 있는 url 허가
        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );

        // 로그인 사용
        http.formLogin((formLogin) ->
                formLogin
                        // 로그인 View 제공 (GET /api/user/login-page)
                        .loginPage("/api/user/login-page")
                        // 로그인 처리 (POST /api/user/login)
                        .loginProcessingUrl("/api/user/login")
                        // 로그인 처리 후 성공 시 URL
                        .defaultSuccessUrl("/")
                        // 로그인 처리 후 실패 시 URL
                        .failureUrl("/api/user/login-page?error")
                        .permitAll()
        );

        return http.build();
    }
}

 

● UserDetailsService

-> db의 회원 정보 조회 -> security의 인증관리자에게 전달

 

package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
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;

@Service
//UserDetailsService는 인터페이스
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
		
		@Override
        //loadUserByUsername()는 사용자 정보를 가져옴
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
     // 없다면 UsernameNotFoundException 예외던짐
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
	//UserDetailsImpl객체에 user 변환하여 반환
        return new UserDetailsImpl(user);
    }
}

 

그러면 반환한 값을 UserDetails 구현해서 UserDetailsImpl을 상속받아서 전달을 구현하면 된다

package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @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;
    }
}

해당 코드에서 UserDetailsImpl() 메서드로 user에 주입
getUser()로 사용자 정보가 필요한 곳에서 사용

이렇게 UserDetailsService와 UserDetails를 구현해서 사용하게 되면 default로 설정된 security 로그인 기능을 사용 하지 않게 된다.

 

config파일에 post로 usl을 설정했으므로!

 

정리하면 UserDetailsService를 통해서 인증확인 작업을 하고 그 인증 객체를 UserDetails에 담는다

 

getUser사용 예시

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        // Authentication 의 Principal 에 저장된 UserDetailsImpl 을 가져옵니다.
        // Principal = 사용자 정보
        User user =  userDetails.getUser();
        System.out.println("user.getUsername() = " + user.getUsername());

       return "redirect:/";
    }
}

.그전 TIL에 정리했지만 UserDetailsImpl 을 가져오려면 @AuthenticationPrincipal 을 사용한 다음 Authentication 담긴 principle(사용자 정보) = UserDetailsImpl을 사용

 

getUsername()사용 예시

@Controller
public class HomeController {
    @GetMapping("/")
    public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        // 페이지 동적 처리 : 사용자 이름
        model.addAttribute("username", userDetails.getUser().getUsername());

        return "index";
    }
}

 

'TIL' 카테고리의 다른 글

+29 RestTemplate의 exchange  (0) 2023.07.14
+28 RestTemplate Get요청  (1) 2023.07.13
+26 RestTemplate이란 무엇일까?  (0) 2023.07.11
+25 Spring Security : JWT로그인  (0) 2023.07.09
+24 Spring Security 프레임워크  (1) 2023.07.08