팀프로젝트 중 플래너 등급 유저만 포토폴리오 작성 기능이 가능하도록 하였다.
원래의 경우, 버튼 작성을 눌렀을 시, ajax를 통해 planner를 찾도록 해, exception이 날 경우, alert와 redirect 시켜 주었었다.
function portfolio() {
$.ajax({
type: 'GET',
url: 'boards/portfolio',
success: function (check) {
if (check == false) {
alert("플래너 등급 유저만 작성 가능합니다.")
location.href="/"
}
else {
location.href = "/boards/portfolio/write"
}
}
});
}
// 포토폴리오신청
//포토폴리오 작성 권한 확인
@ResponseBody
@GetMapping("/portfolio")
public boolean toPortfolioWrite(Principal principal) {
try {
PlannerDetailResponse response = plannerService.findByUser(principal.getName());
} catch (Exception e) {
return false;
}
return true;
}
// 포토폴리오 작성
@GetMapping("/portfolio/write")
public String portfolioWrite(Model model) {
model.addAttribute(new PortfolioCreateRequest());
public String portfolioWrite(Model model, Principal principal) {
PlannerDetailResponse response = plannerService.findByUser(principal.getName());
model.addAttribute(new BoardCreateRequest());
model.addAttribute("planner", response);
return "boards/portfolioWrite";
}
다만, 권한에 따른 접근 제한을 SecurityConfigure에서 설정해달라는 요청을 받았고,
해당 관련해서 코드 리펙토링을 해보았다.
1. SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final AccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/boards/portfolio/write").hasAuthority("PLANNER")
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.and()
// 폼 로그인 시작
.formLogin()
...
해당 포토폴리오 글 작성 url에 hasAuthority를 추가하였다. 그 후 권한이 없으면 accessDeniedHandler에 설정한 대로 이동하도록 하였다.
2. UserDetail
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class UserDetail implements UserDetails, OAuth2User {
private String role; // 권한 (USER, ADMIN, BLACKLIST, PLANNER)
// 권한부여
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(role));
return authorities;
}
}
유저의 role에 따른 권한 부여를 override를 통해 설정해 주었다.
authorities.add(new SimpleGrantedAuthority(role))로 authority를 부여해 주었다.
만약 enum이 ROLE_xxx로 설정이 되어 있지 않을 시, hasRole을 사용하려면 밑의 이슈 글을 참고하면 된다.
3. AccessDeniedHandler
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String requestUri = request.getRequestURI();
if (requestUri.contains("/boards/portfolio/write")) {
request.setAttribute("msg", "플래너 등급 유저만 작성 가능합니다.");
request.setAttribute("nextPage", "/");
request.getRequestDispatcher("/error/redirect").forward(request, response);
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
AccessDeniedHandler는 접근이 가능한지 권한을 체크 후 접근이 불가능 할 시 동작된다.
antmatchers().hasRole이나 antmatchers().hasAuthority에서 해당하는 역할이나 권한이 없을 시 여기서 다루게 된다.
AuthenticationEntryPoint는 인증이 되지않은 유저가 요청을 했을때 동작된다.
해당 경우는 로그인은 된 상태기 때문에, 인증은 된 상태이고, 권한이 없기 때문에 AccessDeniedHander를 통해 설정을 해 주어야 한다.
만약 request의 uri가 플래너 작성으로 이동하는 uri일 때, 권한이 없다면,
alert할 message를 msg에 담고, redirect할 uri를 nextPage에 setAttribute에 담는다.
그 후 forward 방식으로 /error/redirect로 이동시키고, 그 uri에 해당하는 페이징 html파일을 만들어 javascript로 msg를 alert창을 통해 띄우고, nextPage에 담긴 페이지로 redirect 시킨다.
4. ErrorController
@Controller
@RequiredArgsConstructor
public class ErrorController {
@GetMapping("/error/redirect")
public String accessDenied(){
return "error/redirect";
}
}
/error/redirect.html로 이동하기 위한 controller를 작성해 주었다.
5. redirect.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DEMO</title>
</head>
<body>
<script th:inline="javascript">
window.onload = function() {
let nextPage = [[${nextPage}]]
let msg = [[${msg}]];
alert(msg);
location.href = nextPage;
}
</script>
</body>
</html>
경로는 templates/error directory에 만들어 주었다.
AccessDeniedHandler에서 설정한 msg와 nextPage를 받아, alert해주고 redirect해주는 코드를 javascript에 추가해 주었다.
<Reference>
https://kim-jong-hyun.tistory.com/36
https://mighty96.github.io/til/access-authentication/
발생 이슈들
해당 과정에서 많은 시행착오가 있었다.
1. hasRole
먼저 antmatcher를 hasRole로 접근 제한을 두었을 때 문제가 발생하였다.
hasRole을 불러올 때 prefix가 ROLE_ 접두사를 자동적으로 붙여주는데, 프로젝트에서의 usrRole의 enum이 ROLE_PLANNER 형식이 아닌 PLANNER로 지정되어있었기 때문에 추가 설정이 필요했다.
이를 위해서는 userDetail Class에서 추가 설정이 필요했다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class UserDetail implements UserDetails {
...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(role));
//authorities.add(new SimpleGrantedAuthority(role));
if (this.role.equals(UserRole.USER.name())) {
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
} else if (this.role.equals(UserRole.ADMIN.name())) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
} else if (this.role.equals(UserRole.PLANNER.name())) {
authorities.add(new SimpleGrantedAuthority("ROLE_PLANNER"));
} else if (this.role.equals(UserRole.BLACKLIST.name())) {
authorities.add(new SimpleGrantedAuthority("ROLE_BLACKLIST"));
}
return authorities;
}
...
}
role에 따라 권한을 "ROLE_xxx"형태로 부여해 주었고, antmathcers().hasRole("PLANNER")와 같은 형태로 작성하였다.
다만, 다른 팀원들이 thymeleaf에서 sec:authority를 사용한 것을 발견하여, "ROLE_xxx"로 변경시 error가 발생할 수 있어, 위에서 서술한 authority설정으로 바꾸어 주었다.
<Reference>
https://www.baeldung.com/spring-security-expressions
2. sec:authority
securityConfig에서 막혀 여러가지 방안을 생각해 보던 도중,
thymeleaf에서 sec:authority tag를 이용하여 alert와 redirect로 빠져나오고자 하였다.
<sec:authorize access="hasAnyRole('USER','BlACKLIST')" var="haRoleUser"></sec:authorize>
<script type="text/javascript">
if('${haRoleUser}' == true){
f = function() {
alert('권한이 없습니다.');
location.href = "/";
}
}
</script>
그러나, 이미 해당 페이징으로 넘어 올 시 controller를 통해 넘어오기 때문에, 거기서 호출한 planner service에서 planner검색을 하고 exception을 발생시키기 때문에, 해당 코드가 먹히진 않았다.
<Reference>
https://stackoverflow.com/questions/30775001/springsecurity-role-check-inside-javascript
3. @PreAuthorize
이번엔 컨트롤러에서 접근을 막아보고자 하여 해당 annotation을 사용해 보았다.
@PreAuthorize("hasAuthority('PLANNER')")
@GetMapping("/portfolio/write")
public String portfolioWrite(Model model, Principal principal) {
PlannerDetailResponse response = plannerService.findByUser(principal.getName());
model.addAttribute(new BoardCreateRequest());
model.addAttribute("planner", response);
return "boards/portfolioWrite";
}
이 역시 권한이 막혔을 때, redirect 페이징을 처리 할 수 없어 근본적인 해결책이 되지는 않았다.
<Reference>
정말 돌아돌아 해결 한 듯 하다.
SecurityConfigure에서 AccessDeniedHandler에 대해서만 조금 알고 있었더고 금방 해결 될 수 있는 문제였던 것 같다...
<전체 Reference>
https://blog.jiniworld.me/53
https://kim-jong-hyun.tistory.com/36
https://mighty96.github.io/til/access-authentication/
https://www.baeldung.com/spring-security-expressions
https://stackoverflow.com/questions/30775001/springsecurity-role-check-inside-javascript
https://stackoverflow.com/questions/56254632/how-to-redirect-with-spring-security-a-logged-user-to-his-front-page-and-a-non
'SpringBoot' 카테고리의 다른 글
[SpringBoot] SpringBoot로 AWS S3 Bucket에 이미지 업로드 (5) | 2023.01.31 |
---|