시큐리티 설정 파일을 만든다.
@Configuration
@EnableWebSecurity // 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 어노테이션.
// 내부적으로 SpringSecurityFilterChain이 동작하여 URL 필터가 적용된다.
@RequiredArgsConstructor // Adapter를 상속함으로써 httpSecurity의 다양한 속성을 설정.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// defines which URL paths should be secured and not.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 인가 요청 받기
.antMatchers("/", "/home", "/join",
"/css/**", "/*.ico", "/error").permitAll() // 전체 접근 허용
.anyRequest().authenticated();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
1. authorizeRequests()는
BCryptPasswordEncoder()는 패스워드를 암호화 할 때 사용이 되는데, 객체를 직접 생성하는 방식보다는 빈으로 생성하여 사용하는 것이 유지보수에 좋기 때문에 빈으로 등록해주었다.
회원가입 구현
SiteUser, SiteUserRepository, SiteUserService를 생성한다.
데이터베이스에서 사용할 User를 생성해야 하는데, 스프링 시큐리티에서 제공하는 User라는 클래스가 있어 SiteUser라고 이름을 정하였다.
@Data
@Builder
public class SiteUser {
private long id;
private String email;
private String password;
private String username;
}
스프링 시큐리티에 더욱 집중하기 위해 특정 DB가 아닌 HashMap을 이용하여 데이터를 저장하고 가져오도록 하였다.
@Slf4j
@Repository
@RequiredArgsConstructor
public class SiteUserRepository {
private final SiteUserMapper mapper;
public SiteUser save(SiteUser user) {
mapper.save(user);
return user;
}
public Optional<SiteUser> findByEmail(String email) {
return getAllUser().stream()
.filter(m -> m.getEmail().equals(email))
.findFirst();
}
public List<SiteUser> getAllUser() {
return mapper.getAllUser();
}
}
@Service
@RequiredArgsConstructor
public class SiteUserService {
private final SiteUserRepository siteUserRepository;
public List<SiteUser> getAllUser() {
return siteUserRepository.getAllUser();
}
public void save(SiteUser user) {
siteUserRepository.save(user);
}
public SiteUser findByEmail(String email) {
Optional<SiteUser> findUser = siteUserRepository.findByEmail(email);
return findUser.get();
}
}
joinForm.html
<body>
<div class="container">
<div class="py-5 text-center">
<h2>회원 가입</h2>
</div>
<h4 class="mb-3">회원 정보 입력</h4>
<form action="" th:action th:object="${member}" method="post">
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control">
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}" class="form-control">
</div>
<div>
<label for="name">이름</label>
<input type="text" id="name" th:field="*{name}" class="form-control">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">회원 가입</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'"
th:onclick="|location.href='@{/join}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
로그인 구현
시큐리티 설정 파일에 로그인 관련 부분 추가해주기
@Configuration
@EnableWebSecurity // 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 어노테이션.
// 내부적으로 SpringSecurityFilterChain이 동작하여 URL 필터가 적용된다.
@RequiredArgsConstructor // Adapter를 상속함으로써 httpSecurity의 다양한 속성을 설정.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// defines which URL paths should be secured and not.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 인가 요청 받기
.antMatchers("/", "/home", "/join",
"/css/**", "/*.ico", "/error").permitAll() // 전체 접근 허용
.anyRequest().authenticated()
.and()
.formLogin() // 폼 로그인을 사용합니다.
.loginPage("/login") // 로그인 페이지 url
.defaultSuccessUrl("/") // 로그인 성공시 디폴트 화면으로 이동
.failureUrl("/login")
.usernameParameter("loginId")
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserController 추가하기
유저가 login 페이지에 들어오면 loginForm을 렌더링 하는 Get방식의 loginForm 메서드를 추가한다. 여기서 Post방식의 메서드는 스프링 시큐리티가 대신 처리하므로 구현할 필요가 없다.
@Controller
@RequiredArgsConstructor
public class LoginController {
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm loginForm) {
return "login/loginForm";
}
}
loginForm.html
<body>
<div class="container">
<div class="py-5 text-center">
<h2>로그인</h2>
</div>
<form action="item.html" th:action th:object="${loginForm}" method="post">
<div>
<label for="loginId">로그인 ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control">
</div>
<div>
<label for="password">비밀번호</label>
<input type="password" id="password" th:field="*{password}" class="form-control">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">로그인</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">취소</button>
</div>
</div>
</form>
</div>
</body>
다음과 같이 http://localhost:8080/login로 이동은 할 수 있으나, 로그인을 할 수는 없다. 그 이유는 스프링 시큐리티에 어떤 기준을 가지고 로그인 처리를 할 것인지 설정하지 않았기 때문이다.
스프링 시큐리티 로그인 처리
스프링 시큐리티에서는 인증뿐만 아니라 권한에 대한 부분도 처리한다. 따라서 권한에 대한 부분을 유저에게 할당해줘야 한다.
1. UserRole enum 클래스를 만든다.
public enum UserRole {
USER("user"), ADMIN("admin");
private String role;
UserRole(String role) {
this.role = role;
}
}
2. SiteUser에 UserRole을 추가한다.
@Data
@Builder
public class SiteUser {
...
private UserRole role;
}
3. 스프링 시큐리티에게 로그인 처리 방법을 알려주기 위해 이를 처리할 UserSecurityService를 만든다.
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class UserSecurityService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<SiteUser> _user = userService.findByEmail(email);
if(_user.get() == null) {
throw new UsernameNotFoundException("존재 하지 않는 유저입니다.");
}
SiteUser user = _user.get();
List<GrantedAuthority> authorities = new ArrayList<>();
if("admin".equals(user.getRole().toString())) {
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getRole()));
} else {
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getRole()));
}
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 implements 한 다음에 loadUserByUsername이라는 메서드를 구현해야 한다. loadUserByUsername 메서드는 사용자명으로 비밀번호를 조회하여 리턴하는 메서드이다.
loadUserByUsername 메서드는 username으로 SiteUser 객체를 조회하고 만약 일치하지 않는 사용자가 존재하지 않으면 예외를 던지도록 하였다. 사용자명이 "admin"인 경우에는 ADMIN권한을 부여하고, 그 외에는 USER 권한을 부여한다.
사용자명, 비밀번호, 권한을 순서대로 인자에 넣어 User객체를 생성하여 리턴하면 스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의 비밀번호가 화면으로 입력 받은 비밀번호와 일치하는지 검사한다.
UserSecurityService를 WebSecurity 설정 파일에 등록한다.
package com.example.demo2.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserSecurityService userSecurityService; // 추가
// defines which URL paths should be secured and not.
@Override
protected void configure(HttpSecurity http) throws Exception {
...
...
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override // 추가
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
}
}
uthenticationManagerBuilder는 스프링 시큐리티의 인증을 담당한다. auth 객체에 위에서 작성한 UserSecurityService를 등록하여 사용자 조회를 UserSecurityService가 담당하도록 설정했으며, 비밀번호 검증에 사용할 passwordEncoder도 함께 등록했다.
데모
1. 기본적으로 홈 화면에서 시작한다.
2. 홈 화면에서 회원 가입을 한다. (인증이 되지 않은 사용자는 홈, 회원가입, 로그인 페이지에만 접근이 가능하다). 회원 가입 이후에는 로그인 페이지로 이동한다.
3. 로그인 페이지에서 로그인을 한다.
4. 로그인을 하면 홈 화면으로 이동한다.
지금까지 스프링 시큐리티를 이용하여 로그인 페이지를 구현해보았는데 여기에는 개선해야 할 부분이 있다. 회원가입 이후 로그인 페이지로 이동하여 다시 아이디와 비밀번호를 입력해야하는데, 회원가입에 성공하면 자동 로그인이 되도록 해야 한다.