본문 바로가기

Spring 정리/Spring Security

basic login 과정

반응형

 

 

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(request -> {
                                request
                                        .antMatchers("/").permitAll()
                                        .anyRequest().authenticated();
                });
    }
}

 

"/" 이외에 모든 경로는 허용이 안된다. 요청이 오면 인증을 해야 한다. 하지만, 해당 코드에는 오류가 있다. 정적인 web resource에 대해서는 경로를 열어주어야 한다.

 

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers(
                        PathRequest.toStaticResources().atCommonLocations()
                );
    }

 

 

 

 

이 설정으로 정적 리소스에 대한 접근 제한은 무시한다.

 

 

 

atCommonLocations()는 css, js, image 등 web resource들의 요청은 무엇이든지 허용해주도록 바뀌었다.

 

http.formLogin()이라고만 적어두면 Seucirty Filter chain은 default로 다음처럼 설정된다.

  DefaultLoginPageGeneratingFilter
  DefaultLogoutPageGeneratingFilter

 

loginPage("/login")만 지정해주면 문제가 생길 수 있다.(무한루프) 로그인을 받아야만 들어갈 수 있으므로 로그인을 하고 오류가 나는데 계속 로그인 시도를 위해 login으로 접속한다. 따라서 permitAll()이 필요하다.

 

그렇다면 이제, 모든 권한이 필요한 페이지로 이동할 때는 자동으로 login페이지로 넘어간다.

 

 

사용할 계정을 만들기 위해 설정을 하면 deprecated를 볼 수 있다

 

 

비밀번호를 암호화하지 않은 것은 위험하기 떄문에 deprecated가 되었다. 하지만, 이는 실제 사용을 막는 것이 아니라 오류를 띄우기 위한 목적이므로 간단한 테스트시에는 사용해도 문제가 없다.

 

 

th:action="@{/login}">

 

타임리프에 해당 문법을 사용하면 자동으로 csrf 토큰을 달아준다.

 

* 로그인 성공, 실패 시 이동 페이지 설정

 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(request -> {
                                request
                                        .antMatchers("/").permitAll()
                                        .anyRequest().authenticated();
                })
                .formLogin(
                        login -> login.loginPage("/login")
                                .permitAll()
                                .defaultSuccessUrl("/", false)
                                .failureUrl("/login-error")
                );
    }

 

defaultSuccessUrl는 로그인을 하고나서 이동할 페이지를 설정한다. 로그인 성공 이후 이전 페이지로 돌아가게 해주기 위해 두번째 파라미터를 false로 설정한다.

 

failureUrl를 설정해 로그인 실패시 이동할 페이지를 추가한다.

 

 

 

* thymeleaf 에서 security를 적용하는 태그

 

<div sec:authorize="isAuthenticated()">
  This content is only shown to authenticated users.
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')">
  This content is only shown to administrators.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
  This content is only shown to users.
</div>

 

 

타임리프에서 security와 연동이 되기 때문에 프론트 설정을 바꾼다.

 

    <div class="links">
        <div class="link" sec:authorize="!isAuthenticated()">
            <a href="/login">  로그인 </a>
        </div>
        <div class="link" sec:authorize="isAuthenticated()">
            <a href="/user-page">  유저 페이지  </a>
        </div>
        <div class="link" sec:authorize="isAuthenticated()">
            <a href="/admin-page">  관리자 페이지  </a>
        </div>

        <div class="link" sec:authorize="isAuthenticated()">
            <form th:action="@{/logout}" method="post">
                <button class="btn btn-info" type="submit">로그 아웃</button>
            </form>
        </div>
    </div>

 

하지만 문제는, 유저임에도 어드민 페이지에 접근이 된다는 사실이다. 따라서 @EnableGlobalMethodSecurity(prePostEnabled = true) 을 활성화해주면, 정의한 @PreAuthorize대로 권한 검사를 한다.

 

 

이제 비로소 권한에 따라서 페이지 접근이 가능하게 된다. 

 

* 권한이 없을 때 이동하는 페이지 설정

 

 .logout(logout -> logout.logoutSuccessUrl("/"))
 .exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"));

 

또한 권한이 없을 때에 이동할 페이지를 정할 수 있다. 권한이 없을 때 접근권한이 없다는 예외를 발생시킨다. 하지만 이렇게되면, ADMIN 권한을 가지고 있는 경우, USER권한이 필요한 페이지로 이동하지 못하는 문제가 있다. 따라서 코드를 개선한다.

 

    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return roleHierarchy;
    }

 

ADMIN 권한은 USER가 가지고 있는 모든 권한을 가지게 하는 Bean이다. 따라서 ADMINUSER권한이 명시된 페이지도 마음껏 이동할 수 있다.

 

 

* 로그인 했을 때 detail 정보 저장하기

 

.formLogin(
                        login -> login.loginPage("/login")
                                .permitAll()
                                .defaultSuccessUrl("/", false)
                                .failureUrl("/login-error")
                                .authenticationDetailsSource(customAuthDetails)
                )

 

.authenticationDeatilsSource(customAuthDeatils)로 로그인 했을 때 ip정보, session정보 등을 리턴하도록 추가할 수 있다.  

 

 

@Data
@Builder
public class RequestInfo {
    private String remoteIp;
    private String sessionId;
    private LocalDateTime loginTime;
}

 

첫번째로, 리턴 할 정보를 담은 클래스를 만든다. ip주소, 세션 id값, 로그인 시간 등의 정보를 담는다.

 

@Component
public class CustomAuthDetails implements 
    AuthenticationDetailsSource<HttpServletRequest, RequestInfo> {
    @Override
    public RequestInfo buildDetails(HttpServletRequest request) {
        return RequestInfo.builder()
                .remoteIp(request.getRemoteAddr())
                .sessionId(request.getSession().getId())
                .loginTime(LocalDateTime.now())
                .build();
    }
}

 

두번째로, AuthenticationDetailsSourcebuildDetails메서드를 구현한다. 여기서 RequestInfo로 ip, 세션, 로그인 시간을 전달한다. HttpServletRequest로 확인 가능한 정보들을 서비스에서 호출하지 않고 필터에서 미리 가지고 있기 위해서다. 이 데이터들을 detail이라고 하고, 로그인 시에 리턴하도록 한다.

 

    @GetMapping("/auth")
    @ResponseBody
    public Authentication auth() {
        return SecurityContextHolder.getContext().getAuthentication();
    }

 

SecurityContextHolder.getContext().getAuthentication() 으로 현재 로그인한 인증정보를 확인하는 경로를 추가하고 실제 로그인 시 RequestInfo가 저장되었는지 확인한다.

 

 

RequestInfo의 remoteIp, sessionId, loginTime이 정상적으로 출력되었다.

 

반응형