저번 포스팅
이번엔 Cookie로 사용자를 데이터를 받아올 때 보안 문제가 있으므로 이 점을 개선하기 위해 스프링 프레임워크에서 제공하는 Spring Security를 적용했다.
우선 pom.xml에 dependency를 추가해준 뒤, SecurityConfig에 접근 권한 설정을 해준다.
개발 환경
언어: java
Spring Boot ver : 2.3.1.RELEASE
Mybatis : 2.3.0
IDE: intelliJ
SDK: JDK 17
의존성 관리툴: Maven
DB: MySQL 8.0.12
pom.xml 설정 추가
관련 dependency를 추가해준다. 아래는 Maven Project 기준으로 작성했다.
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
package org.study.board.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/join").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/main", true) // 로그인 성공 후 리다이렉트 설정
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/main") // 로그아웃 성공 후 리다이렉트 설정
.permitAll();
}
}
package org.study.board.service;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.study.board.dto.User;
import org.study.board.repository.UserMapper;
import java.util.ArrayList;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
User user = mapper.findByLoginId(userId);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(user.getUserId(), user.getPassword(), new ArrayList<>());
}
}
/user/main
요청
test(ADMIN) 로그인 → 관리자 권한o
/user/info/user
---
1. **user(USER) 로그인** → 일반 사용자 (관리자 권한x)
- 403 Forbidden 에러페이지
Spring Security 적용 방식이 버전마다 조금씩 달라서 헷갈렸다.
사용자가 요청한 login페이지에서 다음 페이지로 넘어가지 않는 문제
cookie에 로그인한 값이 저장되지 않는 문제
jsp에서 이전 쿠키로그인 방식의 데이터를 받아오고 있었어서 사용자 로그인 정보를 불러오는 방식을 수정했다.
수정 코드 (board/main.jsp)
<!-- 로그인 여부에 따라 버튼 표시 -->
<div class="button-container">
<c:choose>
<c:when test="${pageContext.request.userPrincipal != null}">
<!-- 사용자가 로그인한 경우 -->
<input type="button" value="user 목록" onclick="location.href='/user/main'"><br/><br/>
<input type="button" value="글 작성" onclick="location.href='/write'"><br/><br/>
<input type="button" value="로그아웃" onclick="location.href='/logout'"><br/><br/>
</c:when>
<c:otherwise>
<!-- 사용자가 로그인하지 않은 경우 -->
<input type="button" value="로그인" onclick="location.href='/login'"><br/><br/>
<input type="button" value="회원가입" onclick="location.href='/join'"><br/><br/>
</c:otherwise>
</c:choose>
</div>
Spring Security를 적용하면, UserController에서 작성한 기존 login 메소드가 무의미해진다.
글 상세/작성 페이지에서 작성자(로그인한 사용자) 정보를 받아오지 못함
BoardController
@RequestMapping("/write")
public String write(@CookieValue(name="idx", required = false) Long idx, Model model, Board board){
/*User loginUser=userMapper.findById(idx);
model.addAttribute("user", loginUser);*/
**// 수정 1**
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
model.addAttribute("user", username);
if(board.getBno()==null){
model.addAttribute("getBoard", board);
model.addAttribute("getFile", boardService.getFile(board));
}
return "board/write";
}
@RequestMapping("/insertBoard")
public String insertBoard(@ModelAttribute Board board, @CookieValue(name="idx", required = false) Long idx, Model model) {
/*User loginUser=userMapper.findById(idx);
board.setWriter(loginUser.getUsername());
model.addAttribute("user", loginUser);*/
**// 수정 2**
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
board.setWriter(username);
// 모델에 사용자 정보 추가
model.addAttribute("user", username);
boardService.insertBoard(board);
return "redirect:/main";
}
board/write.jsp
<tr width="90%">
<td width="10%" align="center">작성자</td>
<c:if test="${not empty board.writer}">
<td width="50%">${board.writer}</td>
</c:if>
<%--<td width="50%">${user.username}</td>--%>
<td width="50%">${user}</td> **// 수정 3**
</tr>
AuthInterceptor
// 3. Spring Security를 이용한 사용자 인증 상태 확인
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 사용자가 인증되어 있지 않은 경우 로그인 페이지로 리다이렉트
if (authentication == null || !authentication.isAuthenticated()) {
response.sendRedirect("/login");
return false;
}
// 사용자가 인증되어 있으면 요청을 허용
return true;
**접근 권한 403페이지 이슈!!**
SecurityConfig
http.authorizeRequests()
.antMatchers("/login", "/join").permitAll()
.antMatchers("/user/**").hasRole("ADMIN") **// 권한 부여**
.anyRequest().authenticated()
데이터베이스 수정 (USER table UPDATE)
update board_study.user set role='ADMIN' where userId ='test';
commit;
‘test’라는 userId를 가진 사용자에게 ‘ADMIN’권한을 부여함
그런데도 test로 로그인해서 /user/main 에 접근할 때도 똑같이 403에러가 뜬다??!
기본적으로 스프링에서는 따로 예외 처리를 하지 않았다면 예외 발생 시 500 에러가 발생한다. 그런데 스프링 시큐리티를 적용하면 메소드에서 예외가 발생했을 때 403 에러가 발생한다.
심지어 존재하지 않는 URL로 접속하여 404 Not Found가 발생해야 하는 상황에서도 403 Forbidden이 발생한다.
스프링 공식 블로그에 따르면 스프링부트에서는 에러가 발생하면 /error
라는 URI로 매핑을 시도한다. 실제로 localhost:8080/error 링크로 이동하면 아래와 같은 페이지가 나타난다.
일반적으로 permitAll()
을 통해 모든 사용자의 접근을 허용할 URI에는 권한 검증이 필요하지 않은 URI만 추가한다.
스프링부트 프로젝트에서 에러가 발생하면 /error
로 매핑한다.
그런데 /error
는 모두에게 허용된 URI에 포함되지 않는다.
이 문제는 anyRequest().authenticated()
로 인해 /error
도 인증이 필요한 것으로 간주되어 발생한 것이기 때문에 모두에게 허용할 URI 목록에 /error
를 추가하면 단순하게 해결할 수 있다
.antMatchers("/login", "/join", "/error").permitAll()
그런데.. 아직도 403에러가 뜬다!
그렇다면 이번에는 핸들러를 추가해보겠다.
aunthenticationEntryPoint 추가
package org.study.board.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
RequestDispatcher dispatcher = request.getRequestDispatcher("/error/401");
dispatcher.forward(request, response);
}
}
accessDeniedHandler 추가
package org.study.board.config;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
RequestDispatcher dispatcher = request.getRequestDispatcher("/error/403");
dispatcher.forward(request, response);
}
}
SecurityConfig 수정
package org.study.board.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login", "/join").permitAll()
.antMatchers("/user/main").hasRole("ADMIN")
.antMatchers("/user/info/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("loginId") // 로그인 ID 필드 이름 설정
.passwordParameter("password") // 비밀번호 필드 이름 설정
.defaultSuccessUrl("/main", true) // 로그인 성공 후 리다이렉트 설정
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/main") // 로그아웃 성공 후 리다이렉트 설정
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler) // Custom AccessDeniedHandler 등록
.authenticationEntryPoint(authenticationEntryPoint) // Custom AuthenticationEntryPoint 등록
.and()
.csrf().disable() // csrf 비활성화
.rememberMe();
}
}
에러페이지 생성
error/401
```java
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>401 Unauthorized</title>
</head>
<body>
<h1>401 Unauthorized</h1>
<p>You need to log in to access this page.</p>
</body>
</html>
```
error/403
```java
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403 Forbidden</title>
</head>
<body>
<h1>403 Forbidden</h1>
<p>You do not have permission to access this page.</p>
</body>
</html>
```
여전히 403이 뜬다. 권한 설정 자체가 잘못된 건가?
그리고 나서는 이것저것 바꿔보다 보니 실행됐다
user테이블에서 role컬럼의 데이터 값들에 ROLE_을 붙여줬다. ex) USER → ROLE_USER 이런식
아마 데이터베이스에서 role을 정할 때 이러한 규칙으로 정해야 인식하는 것 같다.
- **User** implements ****UserDetails**
```java
package org.study.board.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User implements UserDetails { **//수정**
private Long idx;
private String userId;
private String password;
private String username;
private Timestamp regdate;
private String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String role : role.split(",")){
authorities.add(new SimpleGrantedAuthority(role));
}
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;
}
}
```
- **SecurityConfig**
```java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService; **//수정**
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
```
핵심만 정리 잘 해주셔서 참고 잘 했습니다 잘 읽고 갑니다!