2024.01.07 - [Spring Boot] - [Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현
1. 스프링 시큐리티 (Spring Security) 란 ?
- 스프링에서 제공하는 프레임워크, 애플리케이션에 인증, 인가 기능을 제공함
- 동작 원리
- 로그인 시도 시, 인증 필터가 작동하여 사용자의 id, 비밀번호를 가져옴
- 인증 필터는 id, 비밀번호를 Authentication 객체에 담아서 AuthenticationManager에게 전달
- AuthenticationManager는 UserDetailsService를 호출해서 사용자의 정보를 불러옴
- 이때 UserDetailsService는 DB에 접근하여 정보를 가져오고 이 정보를 UserDetails 객체에 담아서 반환함
- 비밀번호는 PasswordEncoder를 통해서 암호화됨
2. 스프링 시큐리티 설정
2-1. build.gradle
// dependencies 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
2-2. SecurityConfig ( + BCryptPasswordEncoder )
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 시큐리티 필터 메서드
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((auth) -> auth
// 인가 동작 순서 : 위에서 부터 아래로 순서대로 ! 따라서 순서 유의 (anyRequest 특히)
.requestMatchers("/", "/login", "/join").permitAll()
.requestMatchers("/admin").hasRole(Role.ADMIN.name())
// ** : 와일드카드
.requestMatchers("/my/**").hasAnyRole(Role.ADMIN.name(), Role.USER.name())
.anyRequest().authenticated()
);
// 로그인 설정
http
.formLogin((auth) -> auth.loginPage("/login")
.loginProcessingUrl("/loginProc")
.permitAll()
);
// 로그아웃 URL 설정
http
.logout((auth) -> auth
.logoutUrl("/logout")
);
// csrf : 사이트 위변조 방지 설정 (스프링 시큐리티에는 자동으로 설정 되어 있음)
// csrf기능 켜져있으면 post 요청을 보낼때 csrf 토큰도 보내줘야 로그인 진행됨 !
// 개발단계에서만 csrf 잠시 꺼두기
http
.csrf((auth) -> auth.disable());
return http.build();
}
// BCrypt password encoder를 리턴하는 메서드
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
- http.authorizeHttpRequests( (auth) -> auth : 각 url 별로 인가 설정
- .permitAll() : 로그인 하지 않아도 모든 사용자가 접근 가능
- .authenticated() : 로그인만 한다면 모든 사용자가 접근 가능
- .denyAll() : 로그인을 하더라도 모든 사용자가 접근 불가
- .hasRole() : 로그인 이후에 특정 role이 있어야 접근 가능
- .hasAnyRole() : hasRole과 같지만 여러개의 role 설정 가능
- .requestMatchers() : 특정 URL에 대한 설정
- .anyRequest() : 위에서 처리하지 않은 나머지 경로에 대한 처리
- http.formLogin( (auth) -> auth : 로그인 설정
- .loginPage() : 로그인이 발생하는 페이지 URL
- .loginProcessingUrl() : 프론트단 (html)에서 넘어온 로그인 정보를 해당 경로로 넘기면 스프링 시큐리티가 받아서 자동으로 로그인 진행
- http.logout( (auth) -> auth : 로그아웃 설정
- .logoutUrl() : 로그아웃이 발생하는 페이지 URL
2-3. CustomUserDetails
- 직접 로그인 컨트롤러를 구현하지 않아도 스프링 시큐리티가 요청을 가로채서 자동으로 로그인을 진행함
- 이에 따른 설정을 해줘야 함
- UserDetails 인터페이스 상속 => 오버라이드 필수
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
// 현재 user의 role을 반환 (ex. "ROLE_ADMIN" / "ROLE_USER" 등)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
// 앞에 "ROLE_" 접두사 필수 !
return "ROLE_" + user.getRole().name();
}
});
return collection;
}
// user의 비밀번호 반환
@Override
public String getPassword() {
return user.getPassword();
}
// user의 username 반환
@Override
public String getUsername() {
return user.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;
}
}
- 밑에 4개의 메서드는 따로 설정할 수 있지만 해당 구현에서는 따로 설정하지 않고 true로 고정
- isAccountNonExpired() : 계정 만료 여부 => true : 만료 X
- isAccountNonLocked() : 계정 잠김 여부 => true : 잠김 X
- isCredentialsNonExpired() : 비밀번호 만료 여부 => true : 만료 X
- isEnabled() : 계정 사용 가능 여부 => true : 사용 가능 O
2-4. CustomUserDetailsService
- 스프링 시큐리티로 로그인 진행 시 기존처럼 Member 객체가 아닌 CustomUserDetails(member) 가 필요함
- 이를 구현해 줄 서비스 단
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user != null) {
return new CustomUserDetails(user);
}
return null;
}
}
3. 로그인 프로젝트에 적용
- 위에 언급한 설정들을 현 프로젝트에 맞춰 수정 및 적용
- 로그인을 제외한 나머지 기능, 라이브러리 등은 아래 게시글 참고
2024.01.07 - [Spring Boot] - [Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현
3-1. config > SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 시큐리티 필터 메서드
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/security-login", "/security-login/login", "/security-login/join").permitAll()
.requestMatchers("/security-login/admin").hasRole(MemberRole.ADMIN.name())
.requestMatchers("/security-login/info").hasAnyRole(MemberRole.ADMIN.name(), MemberRole.USER.name())
.anyRequest().authenticated()
);
http
.logout((auth) -> auth
.logoutUrl("/security-login/logout")
);
http
.formLogin((auth) -> auth.loginPage("/security-login/login")
.loginProcessingUrl("/security-login/loginProc")
.failureUrl("/security-login/login")
.defaultSuccessUrl("/security-login")
.usernameParameter("loginId")
.passwordParameter("password")
.permitAll()
);
http
.csrf((auth) -> auth.disable());
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 로그인 설정에서 몇 가지 속성 추가
- .failureUrl() : 로그인 실패 시 이동할 URL
- .defaultSuccessUrl() : 로그인 성공 시 이동할 URL
- .usernameParameter("loginId") : 현 프로젝트에서는 username으로 로그인 x => loginId로 로그인 하므로 username 파라미터를 loginId로 수정
- .passwordParameter("password") : 기본값이 password 이지만 써줌
- 나머지 부분은 프로젝트에 맞게 URL만 수정
3-2. domain > dto > CustomUserDetails
// 다른 부분은 동일
@Override
public String getUsername() {
return member.getLoginId();
}
- username 파라미터를 loginId 로 수정 => getUsername() 메서드는 loginId를 반환하게 수정
3-3. service > CustomUserDetailsService
// 다른 부분은 동일
Member member = memberRepository.findByLoginId(username);
- loadUserByUsername 메서드는 username을 매개변수로 받음
- 따라서 MemberRepository 내의 findByLoginId 메서드를 통해 반환받은 member 객체 저장
3-4. service > MemberService
// BCryptPasswordEncoder 를 통해서 비밀번호 암호화 작업 추가한 회원가입 로직
public void securityJoin(JoinRequest joinRequest){
if(memberRepository.existsByLoginId(joinRequest.getLoginId())){
return;
}
joinRequest.setPassword(bCryptPasswordEncoder.encode(joinRequest.getPassword()));
memberRepository.save(joinRequest.toEntity());
}
- BCryptPasswordEncoder를 사용하여 비밀번호 암호화
3-5. controller > SecurityLoginController
@Controller
@RequiredArgsConstructor
@RequestMapping("/security-login")
public class SecurityLoginController {
private final MemberService memberService;
@GetMapping(value = {"", "/"})
public String home(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
String loginId = SecurityContextHolder.getContext().getAuthentication().getName();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
Member loginMember = memberService.getLoginMemberByLoginId(loginId);
if (loginMember != null) {
model.addAttribute("name", loginMember.getName());
}
return "home";
}
@GetMapping("/join")
public String joinPage(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
// 회원가입을 위해서 model 통해서 joinRequest 전달
model.addAttribute("joinRequest", new JoinRequest());
return "join";
}
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinRequest joinRequest,
BindingResult bindingResult, Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
// 비밀번호 암호화 추가한 회원가입 로직으로 회원가입
memberService.securityJoin(joinRequest);
// 회원가입 시 홈 화면으로 이동
return "redirect:/security-login";
}
@GetMapping("/login")
public String loginPage(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
model.addAttribute("loginRequest", new LoginRequest());
return "login";
}
@GetMapping("/info")
public String memberInfo(Authentication auth, Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
Member loginMember = memberService.getLoginMemberByLoginId(auth.getName());
model.addAttribute("member", loginMember);
return "info";
}
@GetMapping("/admin")
public String adminPage(Model model) {
model.addAttribute("loginType", "security-login");
model.addAttribute("pageName", "스프링 시큐리티 로그인");
return "admin";
}
}
- @PostMapping("/login"), info 에서의 로그인 인증 삭제 : 스프링 시큐리티가 자동으로 인증
- admin 에서의 role 체크 부분 삭제 : 스프링 시큐리티가 자동으로 인가
- memberInfo 의 매개변수로 Authentication auth 를 받음
- auth.getName() : 위의 설정을 토대로 현재 로그인 한 계정의 loginId 반환
4. 실행결과
- 홈 화면
- 로그인 후 홈 화면
- 마이 페이지
- 관리자 계정으로 로그인 후 관리자 페이지
- MySQL로 비밀번호 암호화 확인
'Spring Boot' 카테고리의 다른 글
[Spring Boot] 로그인 기능 구현 (4) - 스프링 시큐리티 사용 JWT 로그인 (1) | 2024.01.13 |
---|---|
[Spring Boot] 스프링으로 엑셀 파일 읽기 (1) | 2024.01.08 |
[Spring Boot] 로그인 기능 구현 (2) - 세션 로그인 (0) | 2024.01.07 |
[Spring Boot] 로그인 기능 구현 (1) - 쿠키 로그인 (0) | 2024.01.07 |
[Spring Boot] 로그인 기능 구현 (0) - 공통 기능, 코드 구현 (2) | 2024.01.07 |