본문 바로가기

SpringBoot

[SpringBoot] userRole에 따른 접근 제한 시 페이지 이동 + 메세지 출력

팀프로젝트 중 플래너 등급 유저만 포토폴리오 작성 기능이 가능하도록 하였다.

 

원래의 경우, 버튼 작성을 눌렀을 시, 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://blog.jiniworld.me/53

 

[Spring Boot Tutorial] 8. AccessDeniedHandler 구현클래스로 인증&인가 Exception 핸들링

※ [Spring Boot Tutorial] 7. JavaConfig 설정으로 Spring Security 커스터마이징에 이어서 진행됩니다. 이번 포스팅에서는 접근 권한 없는 페이지 접속에 관한 처리에 대해서 알아봅니다. (다른 표현으로는 ex

blog.jiniworld.me

https://kim-jong-hyun.tistory.com/36

 

[Spring] - AccessDeniedHandler VS AuthenticationEntryPoint

Spring으로 웹개발을 하면서 유저에 대한 인증 및 권한처리를 해주어야 할 때가 있다. 아래는 해당 예시다. Authentication(인증) : 인증이 되지않은 유저는 서버에 요청을 보내도 서버에선 요청에 대

kim-jong-hyun.tistory.com

https://mighty96.github.io/til/access-authentication/

 

[Spring Security] 인증/인가 실패에 따른 리다이렉트

개요

mighty96.github.io

 

발생 이슈들

해당 과정에서 많은 시행착오가 있었다.

 

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

 

springsecurity role check inside javascript

Is it adivisable to use java scripting inside JSP as below, var f = null; '<sec:authorize access="hasAnyRole(\'c2ladmin\',\'provider\')">' f = function() { alert('hi'); } '<...

stackoverflow.com

 

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>

https://stackoverflow.com/questions/56254632/how-to-redirect-with-spring-security-a-logged-user-to-his-front-page-and-a-non

 

How to redirect, with Spring Security, a logged user to his front page and a non logged one to a different one

I'm doing a Spring web-application project as a final project for school. We use SpringBoot with Hibernate and an H2 database. In order to authenticate and authorize, we use Spring Security. We als...

stackoverflow.com

 

정말 돌아돌아 해결 한 듯 하다.

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