Spring security session방식 - 로그인과정 (3)

2025. 2. 17. 21:46Springboot/security

 

 

현재 SecurityConfig 부분.

@Configuration   //Spring에서 설정 파일임을 나타냄.
@EnableWebSecurity
//Spring Security의 보안 설정을 활성화하는 애너테이션(annotation)
// Spring Boot 2에서는 필수였지만, Spring Boot 3에서는 생략가능. but security설정임을 명시
public class SecurityConfig {

    @Bean //security는 password를 DB에 저장할 때 인코딩해서 저장.  비교할 때는 디코딩 후 비교.
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception{
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/h2-console/**").permitAll() // H2 콘솔 접근 허용
            )
            .csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**")) // H2 콘솔 CSRF 비활성화
            .headers(headers -> headers.frameOptions(frame -> frame.disable())); // H2 콘솔을 iframe에서 허용

        http.authorizeHttpRequests((auth) -> auth
            .requestMatchers("/join/**", "/login").permitAll()    // 이 url들은 로그인 안해도됨
            .requestMatchers("/admin").hasAuthority("ADMIN")   //ADMIN 권한 사용자만
            .requestMatchers("/my/**").hasAnyAuthority("ADMIN", "USER")  // ADMIN, USER 권한 중 하나 가지면 ㅇㅋ
            .anyRequest().authenticated()   // 그 외 요청들은 로그인해야만..
        );


        http.formLogin((auth) -> auth.loginPage("/login")   // login페이지 URL 지정.  @RequestMapping("/login")을 만들어야한다.  => login.html
            .loginProcessingUrl("/loginProc")   // login.html에서  form태그의 action URL이  /loginProc여야한다.
                                                // @RequestMapping("/loginProc")는 없다. login과정은 security가 하기때문.
            .defaultSuccessUrl("/")             // 로그인 성공 후 redirect 되는 URL
            .permitAll()
        );

        http.logout((auth) -> auth.logoutUrl("/logout")   //   /logout으로 요청하면 logout이 된다.  @RM은 없다. 로그아웃도 security가 한다.
            .logoutSuccessUrl("/"));                     //    로그아웃 성공 후 /로 redirect


        http.csrf((auth) -> auth.disable());   //보안관련설정. 자세한 설명은 csrf 따로.
        return http.build();
    }
}

 

 

 

 

 

로그인 페이지지정

config 로그인페이지 지정 코드

  http.formLogin((auth) -> auth.loginPage("/login")   // login페이지 URL 지정.  @RequestMapping("/login")을 만들어야한다.  => login.html
            .loginProcessingUrl("/loginProc")   // login.html에서  form태그의 action URL이  /loginProc여야한다.
                                                // @RequestMapping("/loginProc")는 없다. login과정은 security가 하기때문.
            .defaultSuccessUrl("/")               //로그인 성공 후 redirect 되는 url
            .permitAll()
        );

 

 

LoginController

@Controller
public class LoginController {

    @GetMapping("/login")
    public  String login(){
        return "login";
    }
}

 

 

 

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

로그인페이지입니다.

<form action="/loginProc" method="post" name="loginForm">              <!--  securityConfig의  loginProcessingUrl("/loginProc")    -->
    <input id="username" type="text" name="username" placeholder="id(username)"/>   <!-- name이 반드시 username이어야한다.  -->
    <input id="password" type="password" name="password" placeholder="password"/>  <!-- name이 반드시 password이어야한다.  -->
    <button type="submit" >로그인</button>
</form>

</body>
</html>

 

 

 

 

 

 

 

 

 

 

로그인 과정

위의 로그인페이지에서 id(username) /password를 입력하고 로그인버튼을 누른다.

/loginProc로 요청이 오면  security가 자동으로 로그인과정을 진행한다.

( securityConfig에 loginProceessingUrl("/loginProc") )

security session방식은   이 과정에서 security가 로그인하는 과정을 이해하는게 핵심이다.

 

 

로그인 시 다음과 같은 과정을 거친다.

 

 

 

 

참고 :공식 문서  그림

 

우리의 목적은 오직  로그인페이지에서 입력한 username, password의 값과 

DB에 있는 userEntity정보로  로그인 여부를 결정하게 하는 

+  DB에 있는 UserEntity정보로부터 로그인정보를 저장할 객체를  session에 저장하는것이다.

 

하지만 SecurityConfig를 보면  DB와 관련된 설정은 보이지 않는다. 

security는 이  2가지 목적을 개발자가 작성하도록 한다. 

UserDetailService과 UserDetails는 interface로서 이를 구현한 클래스를 만들어야한다.

나머지부분은 신경쓰지 않아도 자동으로 동작한다.

 

 

CustomUserDetails

public class CustomUserDetails implements UserDetails {
    private UserEntity userEntity;  //엔티티필드.  사실 UserDTO필드를 만드는게 맞음.
    public CustomUserDetails(UserEntity userEntity) {
        this.userEntity = userEntity;
    }

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

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        for(String role : userEntity.getRoles()){
            collection.add(  ()-> role );  //new GrantedAuthority
        }
        return collection;
    }


    //isAccountNonExpired ,  isAccountNonLocked , isCredentialsNonExpired ,  isEnabled
    //spring boot 2 에서는 필수로 구현했지만,  3부터는 userDetials가  default 메소드

}

 

 

CustomUserDetailService

@Service  //UserDetailsService의 구현체 빈등록
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);
        if (userData != null) {
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

 

 

 

 

 

 

다시 로그인과정을 보자.

 

 

 

 

이 후  로그인 성공을 한다면  로그인 정보를 저장하게 된다. 

 

로그인을 성공했다면  로그인정보를 저장하는 것도 security가 알아서 저장한다.

우린 단순히 이 로그인정보를 이제 Controller(+service 등)에서 사용하면 된다.

 

 

※저장 과정 살펴보기.  SecurityContextHolder, SecirtyContext, Authentication 이란.

더보기

 

 

SecurityContextHolder는  SecurityContext들을 저장하는 싱글톤(Singleton) 클래스입니다.

내부적으로 ThreadLocal을 사용하여 각 요청(Request)마다 인증 정보를 저장 및 관리합니다.

 

SecurityContext는 매 요청마다 생성되고 요청이 끝난 후 사라집니다. 

Autehnticaion(인증정보= 사용자정보+자격증명+권한+ 기타 등등  )는 SecurityContext에 저장됩니다.

 

 

SecurityContext는 요청 후 사라지지만, 사라지기전에  

Session에   SecurityContext가  저장됩니다.  (복사)

 

로그인정보를 Controller에서 사용하는 것은

session에 담긴 SecurityContext 안의 Authentcation(  또는 그 안의 UserDetails외 여러가지)를 말합니다. 

 

Principal : 사용자정보  우리가 만든 CustomUserDetails(UserDetails).

  Security 내부적으로는 Object타입. 

  왜 UserDetails타입이 아니라 Object타입인지는  Oauth2에서 설명.

  (UserDetails) authentication.getPrincipal() 

 

Credentials :  자격증명인증판단에 사용되는 정보

  사용자를 인증하는 데 사용되는 값 (비밀번호, 토큰, OTP 등)

 

 

Autrhoties : 권한들

 

 

 

 

 

로그인정보 사용하기

SecurityContextPersistenceFilter에서 로그인정보를 sesssion에 저장했다.

spring security에서 로그인정보란 Autehnticaion객체를 말하고 Autehntcation객체안에

개발자가 만든 UserDetails 객체가 들어있다고 보면된다. 

(실제로 session에 저장되는건 Authentication을 갖고있는 SecurityContext지만 

session에 로그인정보=Autehntcation이 저장됬다고 생각해도 OK)

 

 

그래서 이 로그인정보를 Contrller 등에서 사용하는 방법은   

Autehntciation을 직접 사용해도되고, 그 안의 담긴 여러가지 정보들을 사용해도 돼서 여러가지이다.

하지만 사실 로그인정보를 사용할 때 개발자가 직접 로그인정보라고 생각한 UserDetails를 사용하는게 일반적이다.

 

 

 

MainController

@Controller
public class MainController {

    //로그인 후
    @RequestMapping("/my/info")
    @ResponseBody
    public String  myInfo(@AuthenticationPrincipal CustomUserDetails userDetails,  //개발자가 직접 만든 로그인 정보. 이거 사용하세요!
        Principal principal, //  username만 가져올 수 있음.
        Authentication authentication,    //spring securiy객체가 실제 사용하는 로그인정보.  userDetails를 포함 + 좀 더 다양한정보
        HttpSession session,
        HttpServletRequest request
        ){
        StringBuilder sb=new StringBuilder();
        sb.append(  "권한 : "+    userDetails.getAuthorities().iterator().next().getAuthority() +"<br>"   );  //첫번째권한.
        sb.append(  "password : "+  userDetails.getPassword()  +"<br> ");
        sb.append(  "username : "+  userDetails.getUsername() + "<br>" );
        //DB에 있는 정보들을 더 원한다면 CusomuserDetails에서 getUserEntity() 메소드를 만들거나 하자.

        //SecurityContextHolder.getContext().getAuthentication();   위의 Authentcaton과 동일
        //request.getUserPrincipal()    위의 Principal과 동일
         //session.getAttribute("SPRING_SECURITY_CONTEXT").  SecurityContextHolder.getContext()랑 동일


        return sb.toString();
    }

    @RequestMapping("/admin")
    @ResponseBody
    public String  admin(){
        return "권한이없어서 여기에 도달 할 수 없다.";
    }
}

 

 

 

 

 

※ 참고 

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

 

라이브러리 추가 후 thymeleaf 에서  security 관련된 태그를 사용할 수 있다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<!-- thymeleaf의 th 태그와  security의 sec 태그 사용가능  -->
<head>
    <meta charset="UTF-8">
    <title>Spring Security Test</title>
</head>
<body>
<h1>Spring Security와 Thymeleaf 테스트</h1>

<!-- 로그인한 사용자 이름 표시 -->
<!--Controller에서 Autehntcation authentication의 getName 값.  -->
<p>현재 사용자: <span sec:authentication="name"></span></p>
<!--  @AutehntcationPrincipal CustomUserDetails userDetails 의 값을 사용하고 싶다면 Controller에서 model에 직접 담아야 함. -->


<!-- 역할(ROLE) 출력 -->
<p>권한: <span sec:authentication="authorities"></span></p>

<!-- 로그인 상태 확인 -->
<div sec:authorize="isAuthenticated()">
    <p>로그인한 사용자만 볼 수 있습니다.</p>
    <form th:action="@{/logout}" method="post">
        <button type="submit">로그아웃</button>
    </form>
</div>

<!-- 비로그인 상태 확인 -->
<div sec:authorize="isAnonymous()">
    <p>로그인하지 않은 사용자입니다.</p>
    <a th:href="@{/login}">로그인</a>
</div>

<!-- 특정 권한 확인 -->
<div sec:authorize="hasAuthority('ADMIN')">
    <p>관리자 전용 페이지</p>
</div>

</body>
</html>

 

 

 

 

실행결과

회원가입 후