세션기반의로그인 로직수행 및인증 정보가Security Context Holder에 저장되는 과정
Authentication Manager, Authentication Provider, UserDetailsService, UserDetails, SecurityContextHolder
1.로그인시 제공된Authentication 구현체의인증정보획득 및AuthenticationManager에 의해 해당인증정보에 적합한AuthenticationProvider식별
。해당Authentication 구현체는credentials 정보로ID / PW만 포함하는미완성 상태
2.AuthenticationProvider는UserDetailsService를 호출하여 해당ID를 기반으로DB에서 찾은사용자 정보를 통해UserDetails 구현체를 생성한 후 반환
。Spring Security에 의한세션 로그인 인증을 수행하기 위해 반드시UserDetailsService의loadUserByUsername() 메서드를 통해UserDetails 구현체로 반환하도록 설정해야한다.
3. 해당UserDetails 구현체의Password와 입력된Authentication 구현체의Password를 서로비교검증을 수행
。Authentication 구현체 Password:평문,UserDetails 구현체 Password:암호문이며,PasswordEncoder.matches(raw, encoded)를 통해암호화후 비교
。검증이 성공할 경우,principal,authorities정보를 추가로 포함한 완전한Authentication 구현체가 반환되어SecurityContextHolder의현재 스레드의SecurityContext로 등록 후인증상태유지
▶principal에UserDetails 구현체가 저장됨.
▶검증이후Authentication 구현체의credentials는null로 설정
사전 설정
Spring Starter정의
application.yml정의spring: application: name: session datasource: url: jdbc:mysql://localhost:3306/bblog username: root password: wjd747 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: create
암호저장용PasswordEncoder을Spring Bean으로 등록@Configuration public class PasswordConfiguration { @Bean public PasswordEncoder passwordEncoderB(){ return new BCryptPasswordEncoder(10); } }
SecurityFilterChain설정 및Spring Bean으로 등록@Configuration public class SecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity){ return httpSecurity .cors(cors -> cors.disable()) .csrf(csrf->csrf.disable()) .formLogin(Customizer.withDefaults()) .logout(Customizer.withDefaults()) .authorizeHttpRequests( auth-> { auth .requestMatchers("/sign-up") .permitAll() .requestMatchers("/login") .anonymous() .requestMatchers("/user/**") .hasAuthority("USER") .anyRequest() .denyAll(); } ) .build(); } }
DB Entity Class정의@Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String email; private String role = "USER"; @Builder public Member(String userName, String password, String email){ this.email = email; this.password = password; this.userName = userName; } }
DTO 역할의UserDetails 구현클래스정의
。세션 로그인시UserDetailsService.loadUserByUsername("username")에 의해 DB에서 가져온 사용자 정보를 포함한UserDetails DTO를 생성 후AuthenticationProvider에게 반환@Getter @Builder @RequiredArgsConstructor public class CustomUserDetails implements UserDetails { private final String username; private final String password; private final String role; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of( new SimpleGrantedAuthority("ROLE_" + this.role.name()) ); } }▶
Lombok을 사용 시getUsername()과getPassword()를 생략가능
Member Repository정의@Repository public interface MemberRepository extends JpaRepository<Member,Long> { Optional<Member> findMemberByUsername(String username); default Member findMemberByUsernameOrThrow(String username){ return findMemberByUsername(username).orElseThrow( () -> new RuntimeException("멤버 없음") ); } }
회원가입 DTO정의@Getter @AllArgsConstructor public class SignUpRequest { private String username; private String password; private String email; }
Member Service정의
。세션 로그인시@Service Class에서 해당UserDetailsService 인터페이스를 구현 및loadUserByUsername() 구현 메서드를 구현해야한다.
▶JWT 로그인시는 구현할 필요가 없음.
。회원가입을 위한Member Entity생성 시 반드시PasswordEncoder를 통한암호화를 수행 후DB에 저장
。UserDetailsService의loadUserByUsername(String username)에서DB에서username에 해당하는Entity를 가져온 후UserDetails 구현체로서 생성 후Authentication Provider에게 반환@Service @RequiredArgsConstructor @Transactional(readOnly = true) public class MemberService implements UserDetailsService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; @Transactional public void saveMember(SignUpRequest signUpRequest){ memberRepository.save( Member.builder() .email(signUpRequest.getEmail()) .password(passwordEncoder.encode(signUpRequest.getPassword())) .userName(signUpRequest.getUsername()) .build() ); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Member founded = memberRepository.findMemberByUsernameOrThrow(username); return CurrentUser.builder() .username(founded.getUsername()) .password(founded.getPassword()) .role(founded.getRole()) .build(); } }
Controller정의
。GET 호출시회원가입 페이지도출
。POST 호출시회원가입 로직수행@Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; @GetMapping("/sign-up") public String showSignUpPage(){ return "sign_up"; } @PostMapping("/sign-up") public String doSignUpPage( SignUpRequest signUpRequest ){ memberService.saveMember(signUpRequest); return "redirect:/login"; } }
VIEW정의
。src/main/resources/templates내sign_up.html로서 저장<!doctype html> <html lang="ko"> <head> <title>Document</title> </head> <body> <form action="sign-up" method="post"> <div> <div> <label for="username"> 사용자 ID </label> <input type="text" name="username" id = "username"> </div> <div> <label for="username"> 사용자 email </label> <input type="email" name="email" id = "email"> </div> <div> <label for="username"> 사용자 password </label> <input type="password" name="password" id = "password"> </div> <div> <button type="submit">가입</button> <button type="reset">취소</button> </div> </div> </form> </body> </html>。
<form method="post">로회원가입 API로POST전송 시<form 태그>내 각<input> 태그들은name 속성을 기준으로@ModelAttribute에 의해Controller의DTO와바인딩
▶<input name="username">는SignUpRequest DTO의private String username과바인딩
▶<input name="email">는SignUpRequest DTO의private String email과바인딩
▶<input name="password">는SignUpRequest DTO의private String password과바인딩
회원가입및로그인수행
。회원가입후 성공적으로 생성된계정에 대해로그인이 수행
▶ 이후인가검증시필터체인에서URL : /login에 지정된anonymous()로 인해권한 에러발생
。Spring Security의form-base Login을 통해 로그인 시Authentication 구현체 Password:평문,UserDetails 구현체 Password:암호문으로서PasswordEncoder.matches(raw, encoded)에 의해검증이 수행.