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들도 간편하게 처리할 수 있습니다.
- requestMatchers("/api/user/**").permitAll()
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);
}
}
이걸 만들어 준다. 이것은
- 사용자 세부 정보 로드: UserDetailsServiceImpl 클래스는 UserDetailsService 인터페이스를 구현합니다. 이 인터페이스의 loadUserByUsername 메소드는 주어진 사용자 이름에 해당하는 사용자의 세부 정보를 로드하는 역할을 합니다. 이 메소드는 로그인 시도 시 Spring Security에 의해 호출되며, 사용자 이름과 비밀번호를 확인하는 데 사용됩니다.
- 사용자 세부 정보 제공: UserDetailsImpl 클래스는 UserDetails 인터페이스를 구현합니다. 이 인터페이스는 Spring Security에서 사용자의 세부 정보를 나타내는 데 사용됩니다. 이를 통해 Spring Security는 사용자의 이름, 비밀번호, 권한 등의 정보를 얻을 수 있습니다.
- 사용자 권한 관리: 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();
}
}
이렇게 만든다