본문 바로가기

카테고리 없음

Spring Security,JWT로그인, 권한 나눠 접근불가, Validation

SecurityConfig를 쓰는 이유는,

,홈페이지, 로그인 페이지, 회원가입 페이지 등은 로그인하지 않은 사용자도 접근할 수 있어야 하고 어떤 요청은 인증된 사용자만 할 수 있어야 하고, 어떤 요청은 특정 권한을 가진 사용자만 할 수 있어야 하는데, 그런 복잡한걸 다루기 위해서 필요하다.

 

 

Spring Security를 쓰려면

 

메인 어플을

@SpringBootApplication
        //(exclude = SecurityAutoConfiguration.class) // Spring Security를 쓰려면 필요없음
public class SpringAuthApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringAuthApplication.class, args);
    }

}

 

이렇게 (exclude = SecurityAutoConfiguration.class)부분을 없애야 한다. 그리고

 

필터에선 

@Component 를 제거한다.

그리고

package com.teamsparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@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 접근 허용 설정
                        //permitAll이니 전부 허용
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

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

        return http.build();
    }
}

 

이걸 추가한다. 하지만 이건 아직 로그인이 불가능하기 때문에,

 

package com.teamsparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@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들도 간편하게 처리할 수 있습니다.

 

 

formLogin 밑에 코드를 추가해 웹에서도 스프링 시큐리티로 로그인 기능을 수행할수 있게 된다. 이게 없이 기본인

        http.formLogin(Customizer.withDefaults()); 만 쓰면 우리가 전에 만든 화면을 쓰지 못한다.

 

그 다음엔 

package com.teamsparta.springauth.security;


import com.teamsparta.springauth.entity.User;
import com.teamsparta.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;
    }
}

 

이것과 

package com.teamsparta.springauth.security;

import com.teamsparta.springauth.entity.User;
import com.teamsparta.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
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);
    }
}

 

이걸 만들어 준다. 이것은

 

  1. 사용자 세부 정보 로드: UserDetailsServiceImpl 클래스는 UserDetailsService 인터페이스를 구현합니다. 이 인터페이스의 loadUserByUsername 메소드는 주어진 사용자 이름에 해당하는 사용자의 세부 정보를 로드하는 역할을 합니다. 이 메소드는 로그인 시도 시 Spring Security에 의해 호출되며, 사용자 이름과 비밀번호를 확인하는 데 사용됩니다.
  2. 사용자 세부 정보 제공: UserDetailsImpl 클래스는 UserDetails 인터페이스를 구현합니다. 이 인터페이스는 Spring Security에서 사용자의 세부 정보를 나타내는 데 사용됩니다. 이를 통해 Spring Security는 사용자의 이름, 비밀번호, 권한 등의 정보를 얻을 수 있습니다.
  3. 사용자 권한 관리: UserDetailsImpl 클래스의 getAuthorities 메소드는 사용자가 가진 권한을 반환합니다. 이를 통해 Spring Security는 사용자가 특정 작업을 수행할 수 있는지 여부를 결정할 수 있습니다.

따라서, 이 클래스들을 만들면 Spring Security를 사용하여 사용자 인증을 보다 효과적으로 처리할 수 있습니다.

 

 

그리고 basic.js를

$(document).ready(function () {
    const auth = getToken();
    if(auth === '') {
        window.location.href = host + "/api/user/login-page";
    } else {
        $('#login-true').show();
        $('#login-false').hide();
    }
})이거를 

$(document).ready(function () {
    const auth = getToken();
    if(auth === '') {
        $('#login-true').show();
        $('#login-false').hide();
        // window.location.href = host + "/api/user/login-page";
    } else {
        $('#login-true').show();
        $('#login-false').hide();
    }
})

이렇게 바꾼다.

 

첫 번째 코드에서는, 사용자가 로그인하지 않았을 경우(if(auth === '')) 사용자를 로그인 페이지로 리다이렉한다.(window.location.href = host + "/api/user/login-page";). 즉, 로그인하지 않은 사용자는 해당 페이지에 접근할 수 없다.

반면에 두 번째 코드에서는, 로그인하지 않은 사용자가 페이지에 접근했을 때 로그인 페이지로 리다이렉트하는 코드가 주석 처리되어 있어서(// window.location.href = host + "/api/user/login-page";). 따라서 로그인하지 않은 사용자도 해당 페이지에 접근할 수 있다.

 

따라서, 이 변경을 하는 이유는 로그인하지 않은 사용자가 페이지에 접근할 수 있도록 하기 위함이다.

 

 

@GetMapping("/products")
public String getProducts(HttpServletRequest req) {
    System.out.println("ProductController.getProducts : 인증 완료");
    User user = (User) req.getAttribute("user");
    System.out.println("user.getUsername() = " + user.getUsername());

    return "redirect:/";
}
이것을

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

        return "redirect:/";
    }
  • @AuthenticationPrincipal을 사용해서
    • Authentication의 Principal 에 저장된 UserDetailsImpl을 가져올 수 있다.
    • UserDetailsImpl에 저장된 인증된 사용자인 User 객체를 사용할 수 있다.
  •  
@GetMapping("/")
public String home(Model model) {
    model.addAttribute("username", "username");
    return "index";
}

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

        return "index";
    }
}

 

이것도 AuthenticationPrincipal을 사용한다.

 

이제 앱을 돌려서 2라는 사용자로 가입 뒤 로그인을 하면

 

 

이렇게 잘 나온다

 

예외도 

2024-07-24T16:05:33.725+09:00 ERROR 26768 --- [spring-auth] [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalArgumentException: 중복된 사용자가 존재합니다.] with root cause

java.lang.IllegalArgumentException: 중복된 사용자가 존재합니다.

이런식으로 잘 나온다

 

 

그리고 이번엔 JWT를 사용해보도록 한다.

 

<!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>

로그인 화면을 이렇게 바꾸고,

 

@PostMapping("/user/login")
//    public String login(LoginRequestDto requestDto, HttpServletResponse res) {
//        try{
//            userService.login(requestDto, res);
//        }catch (Exception e){
//            return "redirect:/api/user/login-page?error";//예외가 발생하면 login-page?error 경로로 리다이렉트하여 사용자에게 오류 메시지
//        }
//        return"redirect:/";//로그인 성공 시 / 경로로 리다이렉트하여 메인 페이지로 이동
//    }
//    public void login(LoginRequestDto requestDto, HttpServletResponse res) {
//        String username = requestDto.getUsername();
//        String password = requestDto.getPassword();
//
//        // 사용자 확인
//        User user = userRepository.findByUsername(username).orElseThrow(
//                () -> new IllegalArgumentException("등록된 사용자가 없습니다.")
//        );
//
//        // 비밀번호 확인
//        if (!passwordEncoder.matches(password, user.getPassword())) {
//            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
//        }
//
//        // JWT 생성 및 쿠키에 저장 후 Response 객체에 추가
//        String token = jwtUtil.createToken(user.getUsername(), user.getRole());
//        jwtUtil.addJwtToCookie(token, res);
//    }

이런식으로 컨트롤러와 서비스에 로그인 관련을 지운다. 이제 jwt로 할것이기 때문이다.

 

package com.teamsparta.springauth.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.teamsparta.springauth.dto.LoginRequestDto;
import com.teamsparta.springauth.entity.UserRoleEnum;
import com.teamsparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

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

이 클래스는 사용자의 로그인 요청을 처리하고, 인증 토큰을 생성하여 쿠키에 저장하는 역할을 합니다. 이를 통해 서버는 사용자의 다음 요청에서 이 토큰을 확인하여 사용자를 인증할 수 있습니다. 이렇게 하면 사용자는 매번 로그인을 할 필요 없이 한 번 로그인하면 서버가 사용자를 기억하게 됩니다

 

package com.teamsparta.springauth.jwt;

import com.teamsparta.springauth.security.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

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

JwtAuthorizationFilter 클래스는 JWT를 사용하여 인증된 사용자의 요청을 처리하는 역할을 합니다. 이 클래스를 통해 서버는 클라이언트의 요청이 유효한 토큰을 포함하고 있는지 확인하고, 토큰이 유효한 경우 해당 사용자에 대한 인증을 진행합니다.

 

이제 로그인을 하면

 

2024-07-24T18:09:07.686+09:00  INFO 16740 --- [spring-auth] [nio-8080-exec-8] JWT 검증 및 인가                              : eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyIiwiYXV0aCI6IlVTRVIiLCJleHAiOjE3MjE4MTU3NDcsImlhdCI6MTcyMTgxMjE0N30.kUbWBwqkmYOb_kxTGimaVeGFM8t8j0TrZYuc6QTqFIg
Hibernate: 
    /* <criteria> */ select
        u1_0.id,
        u1_0.email,
        u1_0.password,
        u1_0.role,
        u1_0.username 
    from
        users u1_0 
    where
        u1_0.username=?
Hibernate: 
    /* <criteria> */ select
        u1_0.id,
        u1_0.email,
        u1_0.password,
        u1_0.role,
        u1_0.username 
    from
        users u1_0 
    where
        u1_0.username=?

 

이렇게 토큰이 잘 나온걸 확인 가능.

 

이제 유저, 어드민으로 권한을 나눠 접근 불가 페이지를 만든다

 

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

 

이것과, UserDetailsImpl에

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

 

사용자의 역할(UserRoleEnum role = user.getRole();)을 가져와서 해당 역할의 권한(String authority = role.getAuthority();)을 SimpleGrantedAuthority 객체로 변환하고, 이를 권한 목록에 추가. 따라서 사용자의 역할에 따라 적절한 권한이 부여되는

 

이걸 이용해서

Secured

를 사용한다.

productcontroller에

@Secured("ROLE_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:/";
}

 

를 추가하고

WebSecurityConfig에

 

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

 

를 추가한다.

 

그리고 접근불가 페이지를 만든다

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>

 

 

 

그리고 WebSecurityConfig의 밑에

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

 

이걸 넣어서

package com.teamsparta.springauth.config;

import com.teamsparta.springauth.jwt.JwtAuthorizationFilter;
import com.teamsparta.springauth.jwt.JwtAuthenticationFilter;
import com.teamsparta.springauth.jwt.JwtUtil;
import com.teamsparta.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();
    }
}

 

이렇게 만든다