새소식

TIL

[TIL] 230527 <Spring> 사용자 관리하기, 데이터 검증하기

  • -

 


Spring Security 적용

'Spring Security' 프레임워크는 Spring 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어 줍니다. 마치 'Spring' 프레임워크가 웹 서버 구현에 편의를 제공해 주는 것과 같습니다.

 

  • 'Spring Security' 프레임워크 추가
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

 

  • LoggingFilter, AuthFilter 등록 해제 (@Component 주석처리)

 

  • config> WebSecurityConfig
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// CSRF 설정
		http.csrf((csrf) -> csrf.disable());

		http.authorizeHttpRequests((authorizeHttpRequests) ->
			authorizeHttpRequests
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		// 로그인 사용
		http.formLogin(Customizer.withDefaults());

		return http.build();
	}
}

 

CSRF(사이트 간 요청 위조, Cross-site request forgery)

    • 공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것
    • CSRF 설정이 되어있는 경우 html에서 CSRF 토큰 값을 넘겨주어야 요청을 수신 가능
    • 쿠키 기반의 취약점을 이용한 공격이기 때문에 REST 방식의 API 에서는 disable 가능
    • POST 요청마다 처리해 주는 대신 CSRF protection 을 disable 하겠음
      • http.csrf((csrf) -> csrf.disable());

 

 

Spring Security의 default 로그인 기능

  • Username: user
  • Password: Spring 로그 확인! (서버 시작 시마다 변경됨)

 

Spring Security 이해하기

Spring Security - Filter Chain

  • Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller로 분배됨
  • 이 때, 각 요청에 대해서 공통적으로 처리해야할 필요가 있을 때 DispatcherServlet 이전에 단계가 필요하며 이것이 Filter!

  • Spring Security도 인증 및 인가를 처리하기 위해 Filter를 사용하는데 FilterChainProxy를 통해서 상세로직을 구현함

 

Form Login 기반은 인증

Form Login 기반 인증은 인증이 필요한 URL 요청이 들어왔을 때 인증이 되지 않았다면 로그인 페이지를 반환하는 형태

 

UsernamePasswordAuthenticationFilter

  • UsernamePasswordAuthenticationFilter는 Spring Security의 필터인 AbstractAuthenticationProcessingFilter를 상속한 Filter
  • 기본적으로 Form Login 기반을 사용할 때 username 과 password 확인하여 인증함

  • 인증 과정
    1. 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 인증된 사용자의 정보가 담기는 인증 객체인 Authentication의 종류 중 하나인 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에게 넘겨 인증을 시도
    2. 실패하면 SecurityContextHolder를 비움
    3. 성공하면 SecurityContextHolder에 Authentication를 세팅


  • SecurityContextHolder

  • SecurityContext는 인증이 완료된 사용자의 상세 정보(Authentication)를 저장
  • SecurityContext는 SecurityContextHolder로 접근가능
// 예시코드
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
context.setAuthentication(authentication); // SecurityContext 에 인증 객체 Authentication 를 저장합니다.

SecurityContextHolder.setContext(context);

 

  • Authentication
  • 현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있음
  • principal : 사용자를 식별
    • Username/Password 방식으로 인증할 때 일반적으로 UserDetails 인스턴스
  • credentials : 주로 비밀번호, 대부분 사용자 인증에 사용한 후 비움
  • authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용
<UserDetails>
@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;
}

Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

UsernamePasswordAuthenticationToken는 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, 인증객체를 만드는데 사용됨

 

  • UserDetailsService

- UserDetailsService는 username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환
- Custom하여 Bean으로 등록 후 사용 가능

  • UserDetails

- 검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅됨
- Custom하여 사용가능

 

 



로그인 처리 과정 이해

스프링 시큐리티 사용 전

스프링 시큐리티 사용 후

  • Client 의 요청은 모두 Spring Security 를 거치게됨

  • Spring Security 역할
    • 인증/인가
      • 성공 시: Controller 로 Client 요청 전달
        ㄴ Client 요청 + 사용자 정보 (UserDetails)
      • 실패 시: Controller 로 Client 요청 전달되지 않음
        ㄴ Client 에게 Error Response 보냄

 

로그인 처리 과정

 

 

 

 

 

 

 

 

로그인 구현

1. 로그인 처리 URL 설정

WebSecurityConfig

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// CSRF 설정
		http.csrf((csrf) -> csrf.disable());

		http.authorizeHttpRequests((authorizeHttpRequests) ->
			authorizeHttpRequests
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
				.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		// 로그인 사용
		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();
	}
}
  • 우리가 직접 Filter를 구현해서 URL 요청에 따른 인가를 설정한다면 코드가 매우 복잡해지고 유지보수 비용이 많이 들 수 있음
  • Spring Security를 사용하면 이러한 인가 처리가 굉장히 편해짐
    • requestMatchers("/api/user/**").permitAll()
      • 이 요청들은 로그인, 회원가입 관련 요청이기 때문에 비회원/회원 상관없이 누구나 접근이 가능해야함
      • 이렇게 인증이 필요 없는 URL들을 간편하게 허가할 수 있음
    • anyRequest().authenticated()
      • 인증이 필요한 URL들도 간편하게 처리가능

 

2. DB의 회원 정보 조회 → Spring Security의 "인증 관리자"에게 전달

    (1) UserDetailsService 인터페이스 → UserDetailsServiceImpl

security > UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	private final UserRepository userRepository;

	public UserDetailsServiceImpl(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username)
			.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

		return new UserDetailsImpl(user);
	}
}

 

    (2) UserDetails 인터페이스 → UserDetailsImpl

security > UserDetailsImpl

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;
	}
}
  • UserDetailsService와 UserDetails를 직접 구현해서 사용하게 되면 Security의 default 로그인 기능을 사용하지 않겠다는 설정이 되어 Security의 password를 더 이상 제공하지 않는 것을 확인가능
  • POST "/api/user/login" 을 로그인 인증 URL로 설정했기 때문에 이제 해당 요청이 들어오면
    우리가 직접 구현한 UserDetailsService를 통해 인증 확인 작업이 이뤄지고 인증 객체에 직접 구현한 UserDetails가 담기게됨

 

 

@AuthenticationPrincipal

controller>ProductController

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

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

		return "redirect:/";
	}
}
  • @AuthenticationPrincipal
    • Authentication의 Principal 에 저장된 UserDetailsImpl을 가져올 수 있음
    • UserDetailsImpl에 저장된 인증된 사용자인 User 객체를 사용가능
  • @AuthenticationPrincipal 사용해서 메인 페이지 사용자 이름 반영하기

ㄴ controller>HomeController

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

        return "index";
    }
}

 



Security Filter 순서 확인하기

 

로그인 페이지 수정

login.html

더보기

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link rel="stylesheet" type="text/css" href="/css/style.css">
  <script src="https://code.jquery.com/jquery-3.7.0.min.js"
          integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
  <meta charset="UTF-8">
  <title>로그인 페이지</title>
</head>
<body>
<div id="login-form">
  <div id="login-title">Log into Select Shop</div>
  <br>
  <br>
  <button id="login-id-btn" onclick="location.href='/api/user/signup'">
    회원 가입하기
  </button>
  <div>
    <div class="login-id-label">아이디</div>
    <input type="text" name="username" id="username" class="login-input-box">

    <div class="login-id-label">비밀번호</div>
    <input type="password" name="password" id="password" class="login-input-box">

    <button id="login-id-submit" onclick="onLogin()">로그인</button>
  </div>
  <div id="login-failed" style="display: none" class="alert alert-danger" role="alert">로그인에 실패하였습니다.</div>
</div>
</body>
<script>
$(document).ready(function () {
    // 토큰 삭제
    Cookies.remove('Authorization', {path: '/'});
  });

  const host = 'http://' + window.location.host;

  const href = location.href;
  const queryString = href.substring(href.indexOf("?")+1)
  if (queryString === 'error') {
    const errorDiv = document.getElementById('login-failed');
    errorDiv.style.display = 'block';
  }

  function onLogin() {
    let username = $('#username').val();
    let password = $('#password').val();

    $.ajax({
      type: "POST",
      url: `/api/user/login`,
      contentType: "application/json",
      data: JSON.stringify({username: username, password: password}),
    })
            .done(function (res, status, xhr) {
              window.location.href = host;
            })
            .fail(function (xhr, textStatus, errorThrown) {
              console.log('statusCode: ' + xhr.status);
              window.location.href = host + '/api/user/login-page?error'
            });
  }
</script>
</html>

 

JWT 인증 처리 (Filter)

jwt>JwtAuthenticationFilter : 로그인 진행 및 JWT 생성

@Slf4j(topic = "로그인 및 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 {
        log.info("로그인 시도");
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.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 생성");
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        jwtUtil.addJwtToCookie(token, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("로그인 실패");
        response.setStatus(401);
    }
}

 

 

JWT 인가 처리 (Filter)

jwt>JwtAuthorizationFilter : API에 전달되는 JWT 유효성 검증 및 인가 처리

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

 

WebSecurityConfig 필터 등록

config>WebSecurityConfig

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
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 접근 허용 설정
				.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		http.formLogin((formLogin) ->
			formLogin
				.loginPage("/api/user/login-page").permitAll()
		);

		// 필터 관리
		http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
		http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

		return http.build();
	}
}

 

 



API 접근 권한 제어 이해

'일반 사용자'는 관리자 페이지에 접속이 인가되지 않아야함!!

1. Spring Security에 "권한(Authority)" 설정방법

  1. 회원 상세정보 (UserDetailsImpl) 를 통해 "권한 (Authority)" 설정 가능
  2. 권한을 1개 이상 설정 가능
  3. "권한 이름" 규칙 : "ROLE_" 로 시작해야 함
    예) "ADMIN" 권한 부여 → "ROLE_ADMIN"
          "USER" 권한 부여 → "ROLE_USER"
public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}
public class UserDetailsImpl implements UserDetails {
		// ...

		@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(adminAuthority);

        return authorities;
    }
}
  • new SimpleGrantedAuthority("ROLE_ADMIN");
    • 예시 코드는 ROLE_ADMIN 으로 고정되어 있지만 아래와 같이 실제 코드에서는 사용자에 저장되어있는 role의 authority 값을 사용하여 동적으로 저장됨
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();

SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);

ㄴ UserDetailsImpl 저장된 authorities 값을 사용하여 간편하게 권한을 제어가능

 

2. Spring Security를 이용한 API 별 권한 제어 방법

  • Controller 에 "@Secured" 애너테이션으로 권한 설정가능
    • @Secured("권한 이름") 선언
      • 권한 1개 이상 설정 가능

controller>ProductController

	@Secured(UserRoleEnum.Authority.ADMIN) // 관리자용
	@GetMapping("/products/secured")
	public String getProductsByAdmin(@AuthenticationPrincipal UserDetailsImpl userDetails) {
		System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
		for (GrantedAuthority authority : userDetails.getAuthorities()) {
			System.out.println("authority.getAuthority() = " + authority.getAuthority());
		}

		return "redirect:/";
	}

 

  • "@Secured" 애너테이션 활성화 방법

config>WebSecurityConfig

@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 애너테이션 활성화

 

 

접근 불가 페이지 적용

스프링 시큐리티 설정을 이용해 일반 사용자가 '관리자용 상품조회 API' 에 접속 시도 시 접속 불가 페이지가 뜨도록 구현

1. 프론트엔드 개발자 작업 → Forbidden 페이지 적용

resources > static > forbidden.html

더보기

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>접근 불가</title>
        <style>
            html,body{
                margin:0;
                padding:0;
                display:flex;
                justify-content:center;
                align-items:center;
                background-color:salmon;
                font-family:"Quicksand", sans-serif;

            }

            #container_anim{
                position:relative;
                width:100%;
                height:70%;
            }

            #key{
                position:absolute;
                top:77%;
                left:-33%;
            }

            #text{
                font-size:4rem;
                position:absolute;
                top:55%;
                width:100%;
                text-align:center;
            }

            #credit{
                position:absolute;
                bottom:0;
                width:100%;
                text-align:center;
                bottom:
            }

            a{
                color: rgb(115,102,102);
            }
        </style>
        <script>
          var lock = document.querySelector('#lock');
          var key = document.querySelector('#key');


          function keyAnimate(){
            dynamics.animate(key, {
              translateX: 33
            }, {
              type:dynamics.easeInOut,
              duration:500,
              complete:lockAnimate
            })
          }


          function lockAnimate(){
            dynamics.animate(lock, {
              rotateZ:-5,
              scale:0.9
            }, {
              type:dynamics.bounce,
              duration:3000,
              complete:keyAnimate
            })
          }


          setInterval(keyAnimate, 3000);
        </script>
    </head>
    <body>
        <div id="container_anim">
            <div id="lock" class="key-container">
                <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="317.286 -217 248 354" width="248" height="354"><g><path d="M 354.586 -43 L 549.986 -43 C 558.43 -43 565.286 -36.144 565.286 -27.7 L 565.286 121.7 C 565.286 130.144 558.43 137 549.986 137 L 354.586 137 C 346.141 137 339.286 130.144 339.286 121.7 L 339.286 -27.7 C 339.286 -36.144 346.141 -43 354.586 -43 Z" style="stroke:none;fill:#2D5391;stroke-miterlimit:10;"/><g transform="matrix(-1,0,0,-1,543.786,70)"><text transform="matrix(1,0,0,1,0,234)" style="font-family:'Quicksand';font-weight:700;font-size:234px;font-style:normal;fill:#4a4444;stroke:none;">U</text></g><g transform="matrix(-1,0,0,-1,530.786,65)"><text transform="matrix(1,0,0,1,0,234)" style="font-family:'Quicksand';font-weight:700;font-size:234px;font-style:normal;fill:#8e8383;stroke:none;">U</text></g><path d="M 343.586 -52 L 538.986 -52 C 547.43 -52 554.286 -45.144 554.286 -36.7 L 554.286 112.7 C 554.286 121.144 547.43 128 538.986 128 L 343.586 128 C 335.141 128 328.286 121.144 328.286 112.7 L 328.286 -36.7 C 328.286 -45.144 335.141 -52 343.586 -52 Z" style="stroke:none;fill:#4A86E8;stroke-miterlimit:10;"/><g><circle vector-effect="non-scaling-stroke" cx="441.28571428571433" cy="63.46153846153848" r="10.461538461538453" fill="rgb(0,0,0)"/><rect x="436.055" y="66.538" width="10.462" height="34.462" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,0,0)"/></g></g></svg>
            </div>

            <div id="key">
                <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="232.612 288.821 169.348 109.179" width="169.348" height="109.179"><g><path d=" M 382.96 349.821 L 368.96 349.821 L 368.96 314.821 L 382.96 307.821 L 382.96 349.821 Z " fill="rgb(55,49,49)"/><path d=" M 292.134 354.827 L 379.96 315.39 L 379.96 305.547 L 292.134 343.094 L 292.134 354.827 Z " fill="rgb(55,49,49)"/><path d=" M 280.96 340.109 L 401.96 288.821 L 401.96 340.109 L 382.96 349.972 L 382.96 308.547 L 265.96 360.821 L 259.96 349.972 L 280.96 340.109 Z " fill="rgb(115,102,102)"/><path d=" M 401.96 288.821 L 382.96 288.821 L 280.96 332.821 L 292.134 340.109 L 401.96 288.821 Z " fill="rgb(115,102,102)"/><g><path d=" M 232.755 354.125 C 230.958 328.501 246.297 306.519 266.988 305.068 C 287.679 303.617 305.937 323.243 307.734 348.867 C 309.531 374.492 294.191 396.473 273.5 397.924 C 252.809 399.375 234.552 379.75 232.755 354.125 Z " fill="rgb(55,49,49)"/><path d=" M 239.241 352.316 C 237.564 328.406 252.144 307.876 271.779 306.499 C 291.414 305.122 308.716 323.416 310.393 347.326 C 312.07 371.236 297.49 391.766 277.855 393.143 C 258.22 394.52 240.917 376.226 239.241 352.316 Z " fill="rgb(115,102,102)"/><path d=" M 260.038 353.084 C 259.196 348.171 261.788 343.621 265.822 342.929 C 269.856 342.238 273.816 345.665 274.658 350.578 C 275.5 355.49 272.909 360.041 268.874 360.732 C 264.84 361.424 260.88 357.997 260.038 353.084 Z " fill="salmon"/></g></g></svg>
            </div>
        </div>

        <p id="text">403 FORBIDDEN</p>
        <p id="credit">사용자 접근 불가 페이지입니다.</a></p>
    </body>
</html>

 

2. WebSecurityConfig 파일 수정

"접근 불가" 페이지 URL 설정 → "/forbidden.html"

config>WebSecurityConfig

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) // @Secured 애너테이션 활성화
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 접근 허용 설정
				.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		http.formLogin((formLogin) ->
			formLogin
				.loginPage("/api/user/login-page").permitAll()
		);

		// 필터 관리
		http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
		http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

		// 접근 불가 페이지
		http.exceptionHandling((exceptionHandling) ->
			exceptionHandling
				// "접근 불가" 페이지 URL 설정
				.accessDeniedPage("/forbidden.html")
		);

		return http.build();
	}
}

 

 


 

프로그래밍을 하는데에 있어서 가장 중요한 부분 중 하나

  • 특히나 Java는 null 값에 대한 접근에 대해 NullPointerException 오류가 발행하기 때문에 이러한 부분을 예방하기 위해 Validation 즉, 검증 과정이 필요!
  • Spring에서는 null 확인 뿐 아니라 문자의 길이 측정과 같은 다른 검증 과정도 쉽게 처리할 수 있도록 Bean Validation 제공
  • Bean Validation : 간편하게 사용할 수 있는 여러 애너테이션을 제공 (아래는 일부)

build.gradle의 dependencies에 아래 추가

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

 


Validation 적용

1. dto>ProductRequestDto

@Getter
public class ProductRequestDto {
	@NotBlank
	private String name;
	@Email
	private String email;
	@Positive(message = "양수만 가능합니다.")
	private int price;
	@Negative(message = "음수만 가능합니다.")
	private int discount;
	@Size(min=2, max=10)
	private String link;
	@Max(10)
	private int max;
	@Min(2)
	private int min;
}

 

2. @Valid

  • Bean Validation을 적용한 해당 Object validation 실행

controller>ProductController

	@PostMapping("/validation")
	@ResponseBody
	public ProductRequestDto testValid(@RequestBody @Valid ProductRequestDto requestDto) {
		return requestDto;
	}

▶ Postman에서 실행해보기

Cookie와 value에 각 값 넣어서 저장
성공

 

 



Validation 예외처리 회원가입 적용

1. dto>SignupRequestDto

@Getter
@Setter
public class SignupRequestDto {
	@NotBlank
	private String username;
	@NotBlank
	private String password;
	@Email
	@NotBlank
	private String email;
	private boolean admin = false;
	private String adminToken = "";
}

2. controller>UserController : signup에 @Valid 적용

	@PostMapping("/user/signup")
	public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
		// Validation 예외처리
		List<FieldError> fieldErrors = bindingResult.getFieldErrors();
		if(fieldErrors.size() > 0) {
			for (FieldError fieldError : bindingResult.getFieldErrors()) {
				log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
			}
			return "redirect:/api/user/signup";
		}

		userService.signup(requestDto);

		return "redirect:/api/user/login-page";
	}
  • BindingResult
    • 예외가 발생하면 BindingResult 객체에 오류에 대한 정보가 담김
    • 파라미터로 BindingResult 객체를 받아올 수 있음
  •  bindingResult.getFieldErrors()
    • 발생한 오류들에 대한 정보가 담긴 List<FieldError> 리스트를 가져옴

 

 

 

+) Bean Validation의 애너테이션 @Pattern을 사용해서 회원 가입 Email 데이터 검증을 진행

  • Email 형식 : 계정@도메인.최상위도메인
  • 정규 표현식 (Regular Expression)

dto>SignupRequestDto

// @ 기호를 확인합니다. 기호 앞과 뒤 문자는 신경쓰지 않습니다.
String regx1 = "^(.+)@(.+)$";

// @ 기호 앞에 오는 방식에 제한을 추가합니다.
// A-Z, a-z, 0-9, ., _ 를 사용할 수 있습니다.
String regx2 = "^[A-Za-z0-9+_.-]+@(.+)$";

// 이메일 형식에 허용되는 문자를 모두 사용할 수 있습니다.
String regx3 = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$";
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.