✏️ 전체적인 순서
- Configuration
- log-in / log-out url 설정
- 인증 기능 Bean 등록
- Repostiory
- 등급 설정
- Security Service
- Security 로직만을 담당하는 객체 를 별도로 생성
- 사용자가 입력한 name 을 기반으로 User 객체를 조회해줌
- ⚠️ 이후 Spring Security 에 의해 자동으로 검증 작업 이후 log-in 처리가 완료됨
- Controller 계층
- 요청 url 을 매핑해 login form 을 반환함
- Web 계층
- 클라이언트가 입력한 Param 값을 Configuration 에 등록한 url 로 Post 요청
✏️ Configuration 계층
- log-in 기능 추가
- 지난번에 설정했던 접근 권한에
and()
로 기능을 추가해주면 된다.
loginPage
- /user/login 로 오는 요청은 로그인 이라는 뜻
- 여기서 말하는 요청 url 은 POST 를 뜻한다.
defaultSuccessUrl
- 로그인이 완료될 경우 반환되는 페이지
- redirect:/ 와 동일한 기능
- log-out 기능 추가
- log-in 뒤에 또
and()
를 붙여 추가해주면 된다.
logoutRequestMatcher
logoutSuccessUrl
invalidateHttpSession(true)
- 인증 담당 Method
- UserSecurityService 의 반환값을 통해 db 의 user 정보와 클라이언트가 입력한 정보가 일치하는지 확인해주는 기능
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers(
new AntPathRequestMatcher("/**")
).permitAll()
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
// 로그 아웃 기능
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
return http.build();
}
//-- 인증을 담당하는 Bean 등록 --//
// UserSecurityService 와 PasswordEncoder 가 자동으로 설정된다.
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
...
}
✏️ login 기준 설정
- 무엇을 기준으로 로그인 성공 실패를 결정할지 정해주어야 한다.
- 이미 회원가입 할 때 DB 에 저장했으므로 그 정보를 기준으로 정하면 된다.
- Security 설정파일에 직접 계정을 등록해 처리하는 방법도 있다.
📍 Repository 계층
- DB 의 username 을 조회해야 하므로 기능을 추가해준다.
public interface UserRepository extends JpaRepository<SiteUser, Long> {
Optional<SiteUser> findByUsername(String username);
}
📍 권한 설정
- Spring Security 는 인증 뿐 아니라 권한도 관리할 수 있다.
- 인증 (로그인) 이 완료됬을 때 사용자에게 부여할 권한을 설정할 수 있다.
- ADMIN 과 USER 2개의 권한을 설정했다.
@Getter
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROEL_USER");
private String value;
UserRole(String value) {
this.value = value;
}
}
📍 Service 계층
- Business logic 을 수행했던 기존 Service 가 아닌 Security logic 을 수행하기 위한
UserSecurityService
를 새로 생성한다.
- 원하는 기능을 사용하기 위해
UserDetailsService
을 상속받는다.
- Spring Security 는
loadUserByUsername
Method 에 의해 반환된 User
객체의 Password 가 클라이언트가 입력한 Password 와 일치하는지 자동으로 검사해준다.
@Service
@RequiredArgsConstructor
public class UserSecurityService implements UserDetailsService {
private final UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<SiteUser> _siteUser = repository.findByUsername(username);
if (_siteUser.isEmpty())
throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
SiteUser siteUser = _siteUser.get();
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
if ("admin".equals(username))
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
else
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}
}
✏️ Controller 계층
- Get 방식으로 login_form.html 을 렌더링하는 Method 를 추가했다.
- 실제 로그인을 진행하는 @PostMapping Method 는 Spring Security 가 대신 처리해주기 때문에 직접 구현할 필요가 없다.
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
...
@GetMapping("/login")
public String login() {
return "login_form";
}
✏️ Web 계층
📍 로그인 폼
- 로그인을 실패할경우 다시 폼으로 Redirect 되고 error 가 함께 전달되 예외 처리 로직이 동작한다.
- 로그인 실패시 파라미터로 error가 전달되는 것은 스프링 시큐리티의 규칙이다.
<html layout:decorate="~{layout}" xmlns:layout="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
<form th:action="@{/user/login}" method="post">
<div th:if="${param.error}">
<div class="alert alert-danger">
사용자 ID 또는 비밀번호를 확인해 주세요.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">사용자 ID</label>
<input text="text" name="username" id="username" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
<a th:href="@{/user/signup}" class="btn btn-primary">회원가입</a>
</form>
</div>
</html>
📍 네비게이션 바
- 로그인을 완료할 경우 로그인 → 로그아웃으로 버튼을 변경시키는 기능 구현
sec:authorize="isAnonymous()”
- 로그아웃 상태일 경우 true
- 로그인 링크가 표시됨
sec:authorize="isAuthenticated()”
- 로그인 상태일 경우 true
- 로그아웃 링크가 표시됨
<nav th:fragment="navbarFragment"
class="navbar navbar-expand-lg navbar-light bg-light border-bottom" xmlns:sec="http://www.w3.org/1999/xhtml">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse"
id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
</li>
<li class="nav-item">
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</nav>