본문 바로가기
Java , Spring/Spring

[Spring] 스프링 시큐리티 - 커스텀 로그인, 로그아웃, UserDetailsService, UserDetails 실습

by 방배킹 2024. 2. 13.

커스텀 로그인 적용하기

프로젝트를 만들고 실행을 하면 다음과 같이 로그인 창이 뜬다.

의존성은 위 7개를 추가해주었다.

 

스프링 시큐리티를 적용하면 자동으로 로그인창이 생긴다.

 

user와 프로젝트 시작시 나오는 비밀번호를 통해 로그인을 할 수 있다.

 

커스텀 로그인창을 적용시켜보자

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean //비밀번호 암호화를 할때 사용
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder(); 
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/","/login","/logout","/loginSuccess","/join","/joinSuccess").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
                        .anyRequest().authenticated() // 나머지 경로에 대해서 로그인한 사용자는 접근 허용
//                        .anyRequest().denyAll() // 모두 접근 불가
                );
        http
                .formLogin((auth) -> auth
                        .loginPage("/login")
                        .loginProcessingUrl("/loginSuccess")
                        .permitAll()
                );

        http
                .csrf((auth) -> auth
                        .disable()
                );

        return http.build();
    }

}

 

SecurityConfig 클래스를 만든다,

SecurityFilterChain을 반환하는 filterChain 메서드를 만들고 http를 빌드해서 리턴한다.

 

동작 순서는 상단 부터 적용된다. (순서 유의하자)

만약 아래와 같은 코드가 있을경우

.requestMatchers("/").permitAll()
.requestMatchers("/").denyAll()

첫줄에서 이미 permitAll()을 했으므로 denyAll()은 무시된다.

 

"/","/login","/logout","/loginSuccess","/join","/joinSuccess"에 대한 접근을 permitAll()을 한뒤 (join에 대한 내용은 뒤에서 회원가입 구현을 위해 미리 추가해 놓자)

http
        .formLogin((auth) -> auth
                .loginPage("/login")
                .loginProcessingUrl("/loginSuccess")
                .permitAll()
        );

커스텀 로그인 페이지를 설정해준다.

loginPage의 접근 url과 성공시 url를 설정해준다.

@Controller
public class LoginController {
    @GetMapping("/login")
    public String loginP(){
        return "login";
    }
}

LoginController를 통해 해당  커스텀 로그인 페이지를 반환해준다.

 

커스텀 로그인 페이지가 적용되었다.

<form action="/loginSuccess" method="post" name="loginForm">
    <input id="username" type="text" name="username" placeholder="id"/>
    <input id="password" type="password" name="password" placeholder="password"/>
    <input type="submit" value="login"/>
</form>

로그인 form 전송 url을 loginSucess로 설정해준다.

 

회원가입

<p>main page</p>
<hr>
<button onclick="location.href='/login'">로그인</button>
<button onclick="location.href='/join'">회원가입</button>

메인 페이지에 로그인과 회원가입 버튼을 만들어줬다.

 

 

 

@Controller
@RequiredArgsConstructor
public class JoinController {

    private final JoinService joinService;

    @GetMapping("/join")
    public String joinP(){
        return "/join";
    }
    @PostMapping("/joinSuccess")
    public String joinSuccess(JoinDTO joinDTO){
        joinService.save(joinDTO);
        return "redirect:/login";
    }
}
@Service
@RequiredArgsConstructor
public class JoinService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public void save(JoinDTO joinDTO){

        if(userRepository.existsByUsername(joinDTO.getUsername())){
            return;
        }

        UserEntity entity = UserEntity.builder()
                .username(joinDTO.getUsername())
                .password(bCryptPasswordEncoder.encode(joinDTO.getPassword()))
                .role("ROLE_USER")
                .build();
        userRepository.save(entity);
    }
}
@Repository
public interface UserRepository extends JpaRepository<UserEntity,Long> {
    boolean existsByUsername(String username);
    UserEntity findByUsername(String username);
}

Spring Security는 role 값을 넣어줄때 접두사 ROLE_을 붙여줘야한다.

 

ADMIN 페이지

SecurityConfig 클래스에서 .requestMatchers("/admin").hasRole("ADMIN")를 통해  admin 페이지는 role이 ADMIN인 경우만 접근이 가능하도록 설정했다.

role이 ROLE_ADMIN인 경우에만 접근이 가능하다.

스프링 시큐리티는 역할을 부여할떄 접두사 ROLE_을 적어줘야한다.

 

CSRF 활성화

/**
         * 개발환경에서는 csrf 토큰 비활성화 해도 되지만
         * 배포를 할때는 csrf 공격을 방지하기 위해 활성화 해야한다.
         * 활성화를 하면 post 전송을 하는 form 태크 내부에
         * <input type="hidden" name="_csrf" th:value="${_csrf.token}"> 를 추가해줘야 한다.
         * default 값은 enable이다.
         */
         
//        http
//                .csrf((auth) -> auth
//                        .disable()
//                );

csrf disable을 지워서 csrf를 활성화해주자.

활성화를 하면 post 요청의 경우 csrf 토큰을 같이 보내야한다.

<input type="hidden" name="_csrf" th:value="${_csrf.token}">

모든 post 요청에 csrf 토큰을 추가해주자 

 

로그아웃 구현

스프링 시큐리티의 로그아웃은 post 요청만 받아드린다. 우리는 csrf 토큰을 활성화 했으므로 로그아웃 요청을 보낼때 csrf 토큰을 추가해서 post 요청으로 보내야 한다.

 

 

http
        .logout((auth) -> auth
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .permitAll()
        );

SecurityConfig 클래스의 filterChain메서드에 위 로그아웃 로직을 추가해주고

<form action="/logout" method="post" name="logout">
    <input type="hidden" name="_csrf" th:value="${_csrf.token}">
    <input type="submit" value="로그아웃"/>
</form>

csrf 토큰을 넣어서 post 요청으로 로그아웃 요청을 보내면 된다.

 

<button onclick="location.href='/logout'">로그아웃</button>

get 요청은 csrf 토큰이 없어도 되므로 토큰없이  로그아웃 요청을 보낼수 있지만

스프링 시큐리티는 로그아웃을 post 요청만 처리하기 때문에 get 요청으로 로그아웃을 처리하고 싶으면 컨트롤러를 만들어서 구현해야한다.

@GetMapping("/logout")
public String logout() {
    String id = SecurityContextHolder.getContext().getAuthentication().getName();
    if(!id.equals("anonymousUser")){
        log.info("[logout] username: {}",id);
        SecurityContextHolder.getContext().setAuthentication(null);
    }
    return "redirect:/";
}

UserDetailsService와 UserDetails

 

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userEntity.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);
        // 아이디가 존재
        if(userData != null){
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

 

UserDetailsService와 UserDetails에 대해서는 다음 포스팅에 더 자세히 다루었다.

https://bangbaeking.tistory.com/108

 

[Spring] 스프링 시큐리티 정리

스프링 시큐리티란? Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 '인증'과 '인가'에 대한 부분을 Filter 에서 처

bangbaeking.tistory.com

 

@Controller
public class MainController {
    @GetMapping("/")
    public String mainP(Model model){

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String id = authentication.getName();
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Iterator<? extends GrantedAuthority> iter = authorities.iterator();
        GrantedAuthority auth = iter.next();
        String role = auth.getAuthority();


        model.addAttribute("id",id);
        model.addAttribute("role",role);
        return "main";
    }
}

 

id와 role을 구해서 model에 담아서 main페이지 뷰로 전달해서 출력해보자

댓글