(14) 기능구현 - 로그인
로그인과 로그아웃 기능을 구현하자
시작하기 전에
개요 : 질문과 답변을 할 수 있는 게시판 서비스를 스프링부트를 통해 만들어 본다.
학습사이트 : https://wikidocs.net/book/7601
예제 코드 : https://github.com/pahkey/sbb
로그인 기능
스프링 시큐리티를 사용하여 로그인 기능을 구현한다.
로그인 URL
스프링 시큐리티에 로그인 URL을 등록한다.
- SecurityConfig.java 수정
// 경로 sbb/src/main/java/com/mysite/sbb/SeurityConfig.java
package com.mysite.sbb;
(...생략...)
public class SecurityConfig {
(...생략...)
// X-Frame-Options 설정
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
// URL 매핑
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
;
return http.build();
}
(...생략...)
}
로그인페이지 URL (“/user/login”) 과 성공시에 이동하는 디폴트 URL (“/”) 을 설정.
- UserController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/user/UserController.java
(import 생략)
@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {
private final UserService userService;
(...생략...)
@GetMapping("/login")
public String login() {
return "login_form";
}
}
로그인 URL을 만들었으니 컨트롤러에 해당 매핑을 추가.
실제 로그인을 진행하는 메서드는 스프링 시큐리티가 대신 처리함.
로그인 폼 생성
URL매핑을 하였으니, 로그인 폼을 만들어보자.
- login_form.html 생성
<!-- 경로 : /sbb/src/main/resources/templates/login_form.html -->
<!-- layout.html상속 -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!-- 로그인 폼 (post) -->
<form th:action="@{/user/login}" method="post">
<!-- 로그인 error를 전달받을시 -->
<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 type="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>
</form>
</div>
</html>
로그인 실패시 파라미터로 error가 전달되는 것은 스프링 시큐리티의 규칙이다.
- navbar.html 수정
<!-- 경로 : /sbb/src/main/resources/templates/navbar.html -->
<nav th:fragment="navbarFragment"
class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
(...생략...)
<!-- 로그인 링크 -->
<li class="nav-item">
<a class="nav-link" th:href="@{/user/login}">로그인</a>
</li>
(...생략...)
</nav>
로그인 링크를 네비게이션 바에 추가해주었다.
- SBB결과
사용자 조회하기
하지만 아직 스프링 시큐리티에 무엇을 기준으로 로그인을 해야 하는지 아직 설정하지 않았으므로 실제로 로그인을 수행할 수는 없다.
로그인을 수행하기 위해 사용자를 조회하는 서비스를 만들고, 스프링 시큐리티에 등록하는 방법을 알아보자.
- UserRepository.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/user/UserRepository.java
package com.mysite.sbb.user;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<SiteUser, Long> {
Optional<SiteUser> findByusername(String username);
}
사용자 명으로 조회하는 findByusername 메서드 추가.
- UserRole.java 생성
// 경로 : sbb/src/main/java/com/mysite/sbb/user/UserRole.java
package com.mysite.sbb.user;
import lombok.Getter;
@Getter
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER"); // 권한설정 - 관리자, 이용자
UserRole(String value) {
this.value = value;
}
private String value;
}
관리자(ADMIN)와 일반유저(USER) 의 권한을 관리.
- UserSecurityService.java 생성
// 경로 : sbb/src/main/java/com/mysite/sbb/user/UserDetailsService.java
package com.mysite.sbb.user;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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 lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
// UserDetailsService 인터페이스 상속
// UserDetailsService 인터페이스 : loadUserByUsername 메서드를 구현하도록 강제하는 인터페이스
private final UserRepository userRepository;
@Override // loadUserByUsername 구현부 : 사용자명으로 비밀번호를 조회하여 리턴하는 메서드
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 사용자명으로 SiteUser 객체를 조회
Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);
// 사용자명에 해당하는 데이터가 없을 경우
if (_siteUser.isEmpty()) {
throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
}
// 사용자 권한 검사
SiteUser siteUser = _siteUser.get();
List<GrantedAuthority> authorities = new ArrayList<>();
// admin 유저
if ("admin".equals(username)) {
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
}
// 일반 유저
else {
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
}
// 스프링 시큐리티의 User객체 리턴
return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}
}
SiteUser 객체를 조회하고, 권한을 부여하여 User객체를 리턴한다.
- SecurityConfig.java 수정
// 경로 sbb/src/main/java/com/mysite/sbb/SeurityConfig.java
package com.mysite.sbb;
(...)
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.authentication.AuthenticationManager;
(...)
import com.mysite.sbb.user.UserSecurityService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 자동 생성하는 에너테이션
@Configuration // 스프링의 환경설정 파일임을 의미하는 에너테이션
@EnableWebSecurity // 모든 요청URL이 스프링 시큐리티의 제어를 받도록 만드는 에너테이션
public class SecurityConfig {
// 스프링 시큐리티에 로그인 검증 등록
private final UserSecurityService userSecurityService;
(...)
// 스프링 시큐리티 인증 담당 - AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
AuthenticationManager Bean 생성 시 스프링의 내부 동작으로 인해 위에서 작성한 UserSecurityService와 PasswordEncoder가 자동으로 설정된다.
로그아웃
이제 로그인이 정상적으로 수행된다.
하지만, 로그인을 해도 네비게이션바의 로그인링크는 그대로이고, 로그인 한 상태에서 또 로그인이 가능하므로 어색하다. 이를 수정해보자.
로그아웃 구현
- SecurityConfig.java 수정
// 경로 sbb/src/main/java/com/mysite/sbb/SeurityConfig.java
package com.mysite.sbb;
(...)
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
(...)
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 자동 생성하는 에너테이션
@Configuration // 스프링의 환경설정 파일임을 의미하는 에너테이션
@EnableWebSecurity // 모든 요청URL이 스프링 시큐리티의 제어를 받도록 만드는 에너테이션
public class SecurityConfig {
// 스프링 시큐리티에 로그인 검증 등록
private final UserSecurityService userSecurityService;
// 스프링 시큐리티 세부설정하기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// 로그인을 하지 않더라도 모든 페이지에 접근할 수 있음
http.authorizeHttpRequests().antMatchers("/**").permitAll()
// H2 콘솔은 예외처리하기
.and()
.csrf().ignoringAntMatchers("/h2-console/**")
// X-Frame-Options 설정
.and()
.headers()
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
// 로그인URL 매핑
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/")
// 로그아웃URL 매핑
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true) // 로그아웃시 이전에 생성된 사용자 세션 삭제
;
return http.build();
}
(...)
}
로그아웃 URL 매핑을 추가
- navbar.html 수정
<!-- 경로 : /sbb/src/main/resources/templates/navbar.html -->
<nav th:fragment="navbarFragment"
class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
(...생략...)
<!-- 로그인 링크 -->
<li class="nav-item">
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
</li>
(...생략...)
</nav>
로그아웃 링크를 네비게이션 링크에 넣어주었다.
타임리프의 sec:autorize 속성을 사용.
sec:authorize=”isAnonymous()” - 로그인 되지 않은 경우에만 해당 엘리먼트가 표시.
sec:authorize=”isAuthenticated()” - 로그인 된 경우에만 해당 엘리먼트가 표시.
- SBB 결과
로그아웃 구현 완료!
Leave a comment