프로그래밍/Spring
[Spring] Spring Security 회원가입/로그인 구현 예제
브랜치 미정
2023. 9. 18. 16:07
Spring Security로 회원가입 / 로그인을 처리 하는 예제는 아래와 같다.
- Spring Security 라이브러리 추가
- 시큐리티 설정을 할 SpringSecurity 클래스 생성
- 사용자 정보를 담는 UserDetails를 구현 한 PrincipalDetails 클래스 생성
- 시큐리티에서 유저의 정보를 가져오는 UserDetailsService를 구현 한 PrincipalDetailsService 클래스 생성
- 유저 정보를 담을 User 도메인 클래스 생성
- 회원가입 시 주고 받을 UserSaveRequestDto 생성
- User를 저장 할 UserRepository 클래스 생성
- 회원가입 화면을 띄워줄 UserController 클래스 생성
- 회원가입 기능을 구현 할 UserService 클래스 생성
- 회원가입 Post 요청을 처리 할 UserApiController 클래스 생성
- 회원가입 화면 user-save.html 생성
- 회원가입 Post 요청을 보낼 user-save.js 생성
- 로그인 화면을 띄워줄 LoginController 클래스 생성
- 로그인 화면 login.html 생성
Spring Security 라이브러리 추가
- gradle 기준
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
SpringSecurity
- WebSecurityConfigureAdaper를 상속 받은 시큐리티 설정 클래스
- 암호화를 위한 BCryprPasswordEncoder를 Bean으로 등록하고
- andMatchers 로 인증절차 없이 접근할 수 있는 경로를 설정해주고
- formLogin, loginPage로 로그인 페이지를 설정해주고
- login, logout 성공시 경로를 설정해준다.
@Configuration
@EnableWebSecurity
public class SpringSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private PrincipalDetailsService principalDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/",
"/css/**",
"/js/**",
"/api/v1/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/", true)
.and()
.logout()
.logoutSuccessUrl("/");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(principalDetailsService).passwordEncoder(passwordEncoder());
}
}
PrincipalDetails
- 사용자 정보를 담는 UserDetails를 구현한 클래스, 입맛에 맞게 구현
- getAuthorities() : 권한 목록을 리턴한다.
- getPassword() : 계정의 비밀번호를 리턴한다.
- getUsername() : 계정의 이름을 리턴한다.
- isAccountNonExpired() : 계정이 만료되지 않았는지 리턴한다 (true : 만료 안됨)
- isAccountNonLocked() : 계정이 잠겨있지 않았는지 리턴한다. (true : 잠기지 않음)
- isCredentialNonExpired() : 비밀번호가 만료되지 않았는 지 리턴한다 (true : 만료안됨)
- isEnabled() : 계정이 사용가능인지 리턴한다. (true : 가능)
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user){
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@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;
}
}
PrincipalDetailsService
- DB에서 유저 정보를 가져 오는 UserDetailsService를 구현한 클래스
- userRepository에서 유저를 가져와 PrincipalDetails 형으로 정보를 가져온다.
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> new IllegalArgumentException("유저가 없습니다. name=" + username));
return new PrincipalDetails(user);
}
}
User
- 유저 정보를 담을 도메인 클래스
- username, password 를 가진다
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String username;
private String password;
//생략
}
UserSaveRequestDto
- 회원가입 시 사용할 DTO
@Getter
@NoArgsConstructor
public class UserSaveRequestDto {
private String username;
private String password;
@Builder
public UserSaveRequestDto(String username, String password){
this.username = username;
this.password = password;
}
}
UserRepository
- 데이터베이스에 접근하여 User 엔티티를 저장할 클래스
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
UserService
- 회원가입 기능을 구현 할 Service 클래스
- passwordEncoder를 이용하여 password를 암호화 하여 저장한다.
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder passwordEncoder;
@Transactional
public Long save(UserSaveRequestDto userSaveRequestDto){
User user = User.builder()
.username(userSaveRequestDto.getUsername())
.password(passwordEncoder.encode(userSaveRequestDto.getPassword()))
.build();
return userRepository.save(user).getId();
}
// 생략
}
UserController
- 회원가입 화면을 띄워 줄 Controller 클래스
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
@GetMapping("/user/save")
public String userSave(Model model){
model.addAttribute("userSaveRequestDto", new UserSaveRequestDto("", ""));
return "user-save";
}
}
user-save.html
- 회원가입 화면
- userSaveRequestDto 에 username, password를 담아서 button을 클릭하면 user-save.js에 선언 한 userSave() 함수를 실행한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head th:replace="fragments/header :: header">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>회원가입</h1>
<form id="user-save-form" th:object="${userSaveRequestDto}">
<input type="text" th:field="*{username}" required>
<input type="password" th:field="*{password}" required>
<button type="submit" onclick="userSave()">회원가입</button>
</form>
<div th:replace="fragments/footer :: footer" />
<script src="/js/user-save.js"></script>
</body>
</html>
user-save.js
- user-save.html 에서 입력한 username, password를 JSON타입으로 /api/v1/user 경로로 post 요청을 보낸다.
function userSave(){
const form = document.getElementById("user-save-form");
const formData = new FormData(form);
const userSaveRequestDto = {
username: formData.get('username'),
password: formData.get('password')
};
event.preventDefault();
$.ajax({
type: 'POST',
url: '/api/v1/user',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(userSaveRequestDto)
}).done(function(){
alert('회원가입 완료');
window.location.href = '/';
}).fail(function(error){
alert(JSON.stringify(error));
})
}
UserApiController
- 회원가입 Post 요청을 처리할 클래스
- user-save.js 에서 보낸 Dto를 RequestBody로 받아서 Service에서 구현한 save를 실행한다.
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
@PostMapping("/api/v1/user")
public Long save(@RequestBody @Valid UserSaveRequestDto userSaveRequestDto){
return userService.save(userSaveRequestDto);
}
}
LoginController
- 로그인 화면을 띄워줄 Controller 클래스
@RequiredArgsConstructor
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
}
Login.html
- 로그인 화면
- SpringSecurity에서 loginPage 경로를 이 로그인 화면으로 설정하였으므로 해당 화면에서 폼을 제출하면 로그인 처리가 진행된다.
- username, password로 post 요청을 보낸다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head th:replace="fragments/header :: header">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>로그인</h1>
<form th:method="post">
<input type="text" name="username" required>
<input type="password" name="password" required>
<button type="submit">로그인</button>
</form>
<div th:replace="fragments/footer :: footer" />
</body>
</html>