기록을 합시다.
[Spring] 개인프로젝트-2- 로그인 기능 구현하기 본문
GitHub - stir084/SpringSecurity-Example: Spring Security & Thymeleaf 세션 로그인
Spring Security & Thymeleaf 세션 로그인. Contribute to stir084/SpringSecurity-Example development by creating an account on GitHub.
github.com
참고한 코드
WebSecurityConfig.java를 통해서 로그인을 위한 Spring Security를 설정해주고, 로그인 할 때 필요한 UserDetails와 UserDetailService를 구현해줘야 한다.
Spring Security에서 로그인을 처리하기 위해 UserDetails와 UserDetailsService를 구현해야 하는 이유는 다음과 같다.
- UserDetails는 Spring Security에서 인증 및 권한 부여에 필요한 사용자 정보를 제공하는 인터페이스이다. 이를 구현하여 사용자의 정보를 제공하면, Spring Security는 해당 정보를 기반으로 인증과 권한 검사를 수행한다. 특히나 getPassword(), getUsername(), getAuthorities() 등의 메서드를 구현하여 사용자의 자격 증명과 권한 정보를 제공해야 한다.
- UserDetailsService는 Spring Security에서 사용자 정보를 가져오는 인터페이스다. 이 인터페이스를 구현하여 사용자의 정보를 데이터베이스 또는 다른 저장소에서 가져올 수 있다. loadUserByUsername() 메서드를 구현하여 사용자 이름을 기반으로 사용자 정보를 조회하고 UserDetails 객체로 반환해줘야 한다.
- UserDetails와 UserDetailsService를 구현함으로써 Spring Security는 사용자의 자격 증명 정보를 확인하고 권한을 부여할 수 있게 된. 이를 통해 로그인 기능을 구현하고, 사용자 인증과 권한 관리를 효과적으로 처리할 수 있게 된다.
일단 4줄 요약해서 이야기 해 보자면..
- Spring Security 설정에서 formLogin, loginPage, loginProcessingUrl을 설정해준다.
- 로그인을 할 때, html의 form에 포함된 input name이 username인 것의 value를 이용하여 loginProcessingUrl에 해당하는 주소를 통해 UserDetailsService에게 넘겨준다.
- UserDetailsService는 UserRepository를 이용하여 사용자 정보가 포함된 row를 가져오게 되고, 이를 UserDetails에 넘겨주고, UserDetails 객체를 반환해준다.
- SpringSecurity는 UserDetails를 이용하여 사용자가 form에 입력한 비밀번호와 DB에 있는 비밀번호를 비교하여 로그인을 하게 해주고, 로그인을 하게 되면 사용자에게 부여된 권한(Role)을 부여해준다.
WebSecurityConfig.java
package com.example.aparttalk.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
//스프링 시큐리티 적용 제외 대상 설정 스프링빈 등록
@Bean
WebSecurityCustomizer webSecurityCustomizer(){
return (web) -> web.ignoring().requestMatchers("/images/**", "/css/**", "/profile/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.csrf().disable()
.authorizeHttpRequests((authz) -> {
try {
authz
.requestMatchers("/user/**").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/signup").permitAll()
.requestMatchers("/signin").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/signin")
.loginProcessingUrl("/loginProc")
.defaultSuccessUrl("/freeboard");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
해당 코드는 Spring Security에서 로그인 기능을 구성하기 위한 설정을 작성했다. (나머지 설명은 회원가입 기능 구현 포스트에 있다.)
- formLogin(): 폼 기반 인증을 사용하겠다는 설정을 나타낸다. 사용자는 로그인 폼을 통해 인증을 수행한다.
- loginPage("/signin"): 로그인 페이지의 URL을 지정한다.
- loginProcessingUrl("/loginProc"): 로그인 폼의 제출 URL을 지정한다. 사용자가 로그인 폼을 작성하고 제출할 때 이 URL로 요청이 전송된다.
- defaultSuccessUrl("/freeboard"): 로그인이 성공한 후 리다이렉트될 기본 URL을 지정한다. 로그인 성공 시에는 일반적으로 사용자를 대시보드 또는 메인 페이지로 리다이렉트시키는 것이 일반적이다.
위의 설정은 로그인 폼, 로그인 처리 URL, 로그인 성공 후의 기본 URL을 정의하여 Spring Security에서 로그인을 처리할 수 있도록 구성한다. 실제로 `/signin`에 접근하면 로그인 폼이 표시되고(signin.html 필요), 사용자가 폼을 작성하고 제출하면 `/loginProc`로 요청이 전송되어 인증이 수행된다. 로그인이 성공하면 `/freeboard`로 리다이렉트된다. 이를 통해 간단한 로그인 기능을 구현 할 수 있다.
MemberDetails.java
package com.example.aparttalk.auth;
import com.example.aparttalk.user.model.User;
import com.example.aparttalk.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MemberDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUserId(username);
System.out.println(username);
System.out.println(user);
if(user!=null){
return new MemberDetails(user);
}
return null;
}
}
- MemberDetailsService클래스는 Spring Security의 UserDetailsService 인터페이스를 구현한 클래스다. 이 클래스는 사용자의 인증 과정에서 필요한 사용자 정보를 제공하는 역할을 한다.
- loadUserByUsername(): 이 메서드는 주어진 사용자명(username)을 기반으로 사용자 정보를 가져오는 역할을 한다. UserRepository를 사용하여 주어진 사용자명을 가진 사용자를 데이터베이스에서 조회한다.
- userRepository.findByUserId(username): 주어진 사용자명으로 사용자를 데이터베이스에서 조회한다. 조회된 사용자가 있다면, 해당 사용자 정보를 `MemberDetails` 객체로 변환하여 반환한다. 사용자가 존재하지 않는 경우, UsernameNotFoundException을 던지거나 null을 반환한다.
MemberDetails.java
package com.example.aparttalk.auth;
import com.example.aparttalk.user.model.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Data
public class MemberDetails implements UserDetails {
private final User user;
public MemberDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(
new SimpleGrantedAuthority(user.getRole())
);
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Spring Security는 UserDetails 인터페이스를 통해 제공되는 이러한 메서드들을 사용하여 인증과 인가 작업을 수행한다. 인증 과정에서는 비밀번호를 확인하고, 사용자를 식별하며, 인가 과정에서는 사용자의 권한을 확인하여 접근 제어를 수행한다. 따라서 UserDetails 인터페이스를 상속 받은 MemberDetails 클래스에서 이러한 메서드들을 구현하여 필요한 사용자 정보를 제공해야 한다.
- MemberDetails 클래스는 Spring Security의 UserDetails 인터페이스를 구현한 사용자 세부 정보 클래스이다. 이 클래스는 사용자 인증과 관련된 정보를 제공하는 역할을 한다.
- MemberDetails(User user): MemberDetails 생성자로, User 객체를 인자로 받아 초기화한다.
- getAuthorities(): 사용자의 권한을 반환한. 여기서는 사용자의 role을 단일 권한으로 설정하여 반환한다. 참고로 저번 포스트를 보면 알겠지만, 이것 때문에 User모델에 Role 필드를 만들었었다.
- getPassword(): 사용자의 비밀번호를 반환한다.
- getUsername(): 사용자의 아이디(사용자명)을 반환한다.
- isAccountNonExpired()`: 사용자 계정이 만료되지 않았는지 여부를 반환한다. 항상 true를 반환하여 계정 만료 체크를 수행하지 않게 했다.
- isAccountNonLocked(): 사용자 계정이 잠겨있지 않은지 여부를 반환한다. 항상 true를 반환하여 계정 잠금 체크를 수행하지 않게 했다. 이 기능은 비밀번호 5번 정도 틀렸을 때 사용하면 좋을 것 같다.
- isCredentialsNonExpired(): 사용자의 인증 정보가 만료되지 않았는지 여부를 반환한다. 항상 true를 반환하여 인증 정보 만료 체크를 수행하지 않게 했다.
- isEnabled(): 사용자 계정이 활성화되었는지 여부를 반환한다. 항상 true를 반환하여 계정 활성화 체크를 수행하지 않게 했다. 이 기능은 나중에 마지막 로그인 시간과 현재 시간을 비교하여 1년 동안 로그인 하지 않았다면 계정을 잠금 처리할 때 사용하면 유용할 것 같다.
signin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" th:href="@{'/css/global.css'}">
</head>
<body>
<main>
<form th:action="@{/loginProc}" method="post">
<div>
<label for="username">아이디:</label>
<input type="text" id="username" name="username"/>
</div>
<div>
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password"/>
</div>
<div>
<button type="submit">로그인</button>
</div>
</form>
</main>
</body>
</html>
결과
로그인을 성공해서 freeboard 페이지로 갔다!!
다음에는 로그인 기능을 조금 보완 해보고, freeboard에서 CRUD하는 기능을 만들어보겠다.