[Spring Security] 페이지에 Security 설정하기

spring_security

Task #


SecurityConfig class를 설정한다.

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request->{
                request
                    .antMatchers("/").permitAll() // root page permit all
                    .anyRequest().authenticated() // need auth
                    ;
            })
            ;
    }
}

모든 request를 승인받는것으로 변경하니 css 가 동작하지 않는 문제가 생긴다.

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .requestMatchers(
                PathRequest.toStaticResources().atCommonLocations()
            );
    }
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request->{
                request
                    .antMatchers("/").permitAll() // root page permit all
                    .anyRequest().authenticated() // need auth
                    ;
            })
            .formLogin() // *
            ;
    }

formLogin 을 설정해준다. 이러면 default login 화면이 뜬다. 그 이유는 로그인을 특정하지 않으면 기본적인 default login page가 뜨기 때문.

.formLogin(
    login -> login.loginPage("/login")
    .permitAll() // 무한루프 방지
)

무한루프 방지를 위하여 permitAll 을 반드시 더해주고 login page 를 지정해준다.


Login #

그 다음은 로그인을 시켜보자.

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
    .inMemoryAuthentication()
    .withUser(
        User.withDefaultPasswordEncoder()
            .username("user1")
            .password("1111")
            .roles("USER")
    ).withUser(
        User.withDefaultPasswordEncoder()
            .username("admin")
            .password("2222")
            .roles("ADMIN")
    );
}
<div class="container center-contents">
    <div class="row">
        <form class="form-signin" method="post" th:action="@{/login}">
            <h1 class="h3 mb-3 font-weight-normal"> 로그인 </h1>

다시 새로고침하여 브라우저에서 dev mode로 살펴보면 hidden 으로 csrf value가 달려있음을 알 수 있다.

<input type="hidden" name="_csrf" value="50468860-ccbc-4c0c-89f8-14252f45ff44">

이렇게하면 로그인 성공!


그 밖의 formLogin 설정 #

.formLogin(
    login -> login.loginPage("/login")
    .permitAll() // 무한루프 방지
    .defaultSuccessUrl("/", false) // 로그인에 성공했을때
    .failureUrl("/login-error") // 로그인 실패했을때 이 url으로 이동
)

Logout setting #

이제 로그아웃을 설정해보자.

로그아웃 버튼 만들기 #

index.html 에 아래와 같이 로그아웃 버튼을 만들고

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

로그아웃에 성공했을 때, root page로 이동하게 설정한다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(request->{
            request
                ...
        })
        .formLogin(
            ...
        )
        .logout(logout -> logout.logoutSuccessUrl("/")) // *
        ;
}

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

<div sec:authorize="isAuthenticated()">
    로그인 된 유저에게만 보이는 content.
</div>
<div sec:authorize="hasRole('ROLE_ADMIN')">
    administrators에게만 보이는 content.
</div>
<div sec:authorize="hasRole('ROLE_USER')">
  users에게만 보이는 content.
</div>

관리자 페이지에 모든 유저가 들어갈 수 없게 해결 #

SecurityConfig class에 EnableGlobalMethodSecurity true를 달면

@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true) // *
public class SecurityConfig extends WebSecurityConfigurerAdapter {

기존에 아래와 같이 admin page에 설정되어있던 role대로 security가 적용되게 된다.

@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
@GetMapping("/admin-page")
public String adminPage(){
    return "AdminPage";
}

이렇게 설정하면 user가 admin page에 접근할 수 없게된다.

exceptionHandling #

access denied 되었을때 이동할 페이지를 설정할 수 있다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        ...
        .exceptionHandling(exception -> exception.accessDeniedPage("/access-denied"))
        ;
}

RoleHierarchy #

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

authenticationDetailsSource #

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RequestInfo {

    private String remoteIp;
    private String sessionId;
    private LocalDateTime loginTime;
}
@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()
        ;
    }
}
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomAuthDetails customAuthDetails;

    public SecurityConfig(CustomAuthDetails customAuthDetails) {
        this.customAuthDetails = customAuthDetails;
    }
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(request->{
                request
                    ...
            })
            .formLogin(
                login -> login.loginPage("/login")
                .permitAll()
                .defaultSuccessUrl("/", false)
                .failureUrl("/login-error")
                .authenticationDetailsSource(customAuthDetails) // *
            )
            ...

CustomAuthDetails 를 final로 지정하여 generate constructor 을 통하여 생성자를 만들고, authenticationDetailsSource의 파라미터로 customAuthDetails를 넘겨준다.

{
  "authorities": [
    {
      "authority": "ROLE_ADMIN"
    }
  ],
  "details": {
    "remoteIp": "127.0.0.1",
    "sessionId": "CE195685E5809C7C55CC6A973BCF350E",
    "loginTime": "2022-01-04T16:12:47.5583688"
  },
...

위와 같이 details가 바뀌는것을 알 수 있다.


Auth page 만들기 #

@ResponseBody // json 객체로 내려보내기
@GetMapping("/auth")
public Authentication auth() {
    return SecurityContextHolder.getContext().getAuthentication();
}