오늘 공부할 주제
Github 코드 보기
1. GlobalExceptionHandler 만들기
먼저, 프로젝트에서 발생하는 모든 예외사항을 처리하기 위해 ExceiptionHandler클래스를 만들겠다.
handler 패키지를 만든 후 GlobalExceptionHandler 클래스를 아래와 같이 생성해준다.
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseDto<String> handleArgumentException(Exception e){
return new ResponseDto<String>(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}
}
@ControllerAdvice 어노테이션은 모든 예외사항이 발생할 시 해당 클래스로 받겠다는 의미를 갖는다.
해당 메소드를 통해 어떤 예외를 처리할지 정하기 위해 @ExceptionHandler 어노테이션을 사용하며,
나는 모든 예외사항을 해당 메소드로 받기 위해 최상단의 Exception클래스를 value값으로 넣었다.
ResponseDto클래스를 사용하여 예외 데이터를 처리하기 위해 @RestController를 사용한다.
2. 로그인 페이지 만들기
<form action="/auth/loginProc" method="post">
<div class="mb-3 mt-3">
<label for="username" class="form-label"><strong>아이디</strong></label>
<input type="username" class="form-control" id="username" placeholder="Enter username" name="username">
</div>
<div class="mb-3">
<label for="password" class="form-label"><strong>비밀번호</strong></label>
<input type="password" class="form-control" id="password" placeholder="Enter password" name="password">
</div>
<div class="form-check mb-3">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="remember"> 아이디 기억하기
</label>
</div>
<button class="btn btn-primary">로그인</button>
</form>
user폴더에 joinForm.jsp를 복사하여 loginForm.jsp파일을 생성한 후 본문 부분을 수정해 주겠다.
먼저 Security가 form의 action과 메소드를 인식할 수 있도록 action은 /auth/loginProc method는 post 로 설정해준다.
로그인 할 때, username, password를 받으면 되기 때문에 두 가지 입력을 받고
나중에 쿠키에 있는 아이디가 있다면 끌어다 쓰기 위해 아이디 기억하기 체크박스를 하나 추가해준다.
그리고 <form>태그 바깥쪽에 있던 button을 안쪽으로 옮겨준다. 이 때 button에 id는 정의 할 필요없다.
3. 로그인 페이지로 이동하는 Controller 만들기
// UserController
@GetMapping("/auth/loginForm")
public String loginForm(){
return "/user/loginForm";
}
위와 같은 메소드를 UserController 클래스에 추가해준다.
로그인 페이지도 로그인세션이 없더라도 들어갈 수 있도록 auth를 추가해준다.
위와 같은 Controller는 localhost:포트번호/auth/loginForm 이라는 주소를 호출하면
user폴더 아래의 loginForm.jsp 페이지를 return해준다.
이제 호출을 해보면 페이지가 잘 보여지는 것을 확인 할 수있다.
자 이제 로그인 기능을 구현하기 위해 Spring Security를 이용하겠다.
혹시 dependency에서 security 관련 라이브러리가 주석처리 되어있다면, 주석을 해제하고 build를 하자
4. SecurityConfig 클래스 생성
Security생성하기 전에 기존에 비밀번호를 입력받아 DB에 넣을 때 해쉬화를 하지않았기 때문에 해쉬화 처리먼저 한다.
// UserService 클래스
@Autowired
private BCryptPasswordEncoder encoder;
@Transactional
public Boolean 회원가입(User user){
String rawPassword = user.getPassword();
String hashPassword = encoder.encode(rawPassword);
user.setPassword(hashPassword);
user.setRoleType(RoleType.USER);
try {
userRepository.save(user);
return true;
} catch (Exception e) {
return false;
}
}
그 다음 config 패키지를 생성한 후 SecurityConfig 클래스를 생성 해준다.
@Configuration
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
};
@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //csrf 공격 방지 해제 -> Rest API 서버를 사용하기 때문에 disable
.authorizeRequests() // HttpServletRequest를 이용한다는 것을 의미
.antMatchers("/","/auth/**","/js/**","/css/**","/images/**").permitAll()
.anyRequest().authenticated() //나머지는 인증이 되어야한다.
.and()
.formLogin().loginPage("/auth/loginForm") //인증이 되지않으면 사용자가 정의한 로그인 페이지로 이동
.loginProcessingUrl("/auth/loginProc") //로그인 연산시킬 URI
.defaultSuccessUrl("/"); //로그인 성공시 응답할 URI
return http.build(); // build 패턴
}
}
SecurityFilterChain은 기본적으로 HTTP 요청이 발생하면
모든 필터를 통해 요청이 필터링 되고 나서 요청은 서블릿으로 전달된다.
위 코드에서 빌드패턴으로 작성된 configure 메소드의 로직에 대한 각각의 내용은 주석으로 간단하게 적어놓고
자세한 내용은 따로 글을 통해 설명하도록 하겠다.
5. PrincipalDetail, PrincipalDetailService 클래스 생성
먼저 config 패키지 아래에 auth 패키지를 생성하고 PrincipalDetail, PrincipalDetailService 클래스를 각각 생성해준다.
@Getter
public class PrincipalDetail implements UserDetails {
private User user;
public PrincipalDetail(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> {
return "ROLE_"+user.getRoleType();
});
return authorities;
}
@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;
}
}
User 객체를 세션에 리턴할 때 User타입으로 리턴할 경우 Authentication객체에 사용자의 정보를 담을 수 없기 때문에
우리가 정의한 PrincipalDetail 클래스에 UserDetails클래스를 implements 해주고 @Getter 어노테이션을 사용해준다.
PrincipalDetail 객체에 담을 User를 private으로 Composition해준다.
그리고 생성자를 통해 의존성 주입을 해준뒤 @Override한 Username과 Password의 return값을 채워준다.
나머지 boolean형 메소드들은 return 값을 모두 true로 바꾸어준다.
@Service
public class PrincipalDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(()->{
return new UsernameNotFoundException("해당 사용자를 찾을 수 업습니다. "+ username);
});
return new PrincipalDetail(principal);
}
}
이제 PrincipalDetailService를 만들고 UserDetailsService를 implements한 다음 @Service 어노테이션을 추가해준다.
loadUserByUsername() 메소드를 오버라이딩하고 입력 받은 username으로 DB에서 해당 사용자가 있는지 찾는다.
이 때 UserRepository 클래스에 다음과 같은 메소드를 추가해준다.
// UserRepository 인터페이스
Optional<User> findByUsername(String username);
Optional 타입을 주면 해당 메소드를 상속받을 시 예외처리를 해주어야 하기 때문에
.ElseThrow() 메소드를 이용하여 예외처리를 해준다.
(참고로 파라미터로 람다식을 사용하여 메소드 안에 메소드를 구현하였다.)
그리고 DB에서 찾은 정보를 User 객체에 담은 후 PrincipalDetail 클래스에 파라미터로 넘겨 객체를 생성한다.
이렇게 만들어진 PrincipalDetail 객체는 Authentication객체에 담겨 AuthenticationManger를
통해 SecurityContext에 담기게 된다.
이렇게 담긴 객체를 security taglib를 통해 쉽게 가져와 사용할 수 있는데 다음을 확인해보자.
** 다음은 Spring Security를 공부하며 자주 볼 수 있는 flow 그림이다 자세한 설명은 Security에 대한 글에서 기술하겠다.
6. spring security tag를 이용한 Session 사용
기존에 header.jsp의 상단에 달아둔 jstl, security tag를 사용해 볼 차례이다.
// jstl
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
// security tag
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
먼저 security 옵션 중 authorize, authentication을 사용하여 사용자의 세션 정보를 확인 할 수 있다.
<sec:authorize access="isAuthenticated()">
<sec:authentication property="principal" var="principal"/>
</sec:authorize>
authorize 옵션에서 isAuthenticated() 메소드는 사용자가 익명이 아닐 경우 true를 반환하여 태그안의 로직을 수행시킨다.
그리고 authentication 옵션을 사용하여 SecurityContext Authentication에서 얻은 현재 개체에 직접 액세스 하여 사용자 정보를 변수에 담아 페이지에서 사용할 수 있다.
이제 다음은 위와 같은 옵션으로 사용자의 정보를 제대로 가져왔는지 여부에 따라
navbar를 다르게 표현하기 위해 jstl 태그를 사용하겠다.
<c:choose>
<c:when test="${empty principal}">
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active"><a class="nav-link" href="/auth/loginForm">로그인</a></li>
<li class="nav-item"><a class="nav-link" href="/auth/joinForm">회원가입</a></li>
</ul>
</div>
</c:when>
<c:otherwise>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active"><a class="nav-link" href="#">글쓰기</a></li>
<li class="nav-item active"><a class="nav-link" href="#">회원정보</a></li>
<li class="nav-item"><a class="nav-link" href="/logout">로그아웃</a></li>
</ul>
</div>
</c:otherwise>
</c:choose>
jstl문법의 prefix를 c로 설정하였기 때문에 <c:옵션명>과 같은 형태로 jstl문법을 사용할 수 있다.
먼저 위에서 principal 변수에 객체가 담기지 않았을 경우 로그인, 회원가입 navbar를 표시하고,
객체가 담겼다면 글쓰기, 회원정보, 로그아웃을 표시하겠다.
이 때 로그아웃의 링크는 security에서의 default 주소가 /logout 이기 때문에
그대로 설정해주면 로그아웃 기능을 쉽게 구현 가능하다.
7. 로그인 시도 및 비밀번호 해쉬화 확인
먼저 회원가입을 시도하고 DB를 통해 사용자 정보를 확인하면 password가 해쉬화가 정상적으로 된 것을 확인 할 수 있다.
이제 로그인을 시도하면 home화면으로 넘어가면서 navbar가 바뀐 것을 확인 할 수 있다.
이렇게 security tag와 jstl tag를 통해 쉽게 세션값을 가져와 사용할 수 있으며
앞으로 글쓰기, 회원수정 등에 있어서도 사용할 예정이다.
'개인 프로젝트 > 블로그 만들기' 카테고리의 다른 글
나의 블로그 만들기 프로젝트 (7일차) - 글 목록 뿌리기 (0) | 2023.04.05 |
---|---|
나의 블로그 만들기 프로젝트 (6일차) - 글쓰기 완료 (0) | 2023.04.05 |
나의 블로그 만들기 프로젝트 (4일차) - 회원가입 처리 (0) | 2023.04.01 |
나의 블로그 만들기 프로젝트 (3일차) - index 페이지 로딩 (0) | 2023.03.31 |
나의 블로그 만들기 프로젝트 (2일차) - Entity 만들기 (0) | 2023.03.30 |