common 폴더
module 폴더
config 폴더
resources/ webapp 폴더
SecurityConfig.java
추가
package com.example.my.config.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
// h2를 볼려고
httpSecurity.authorizeHttpRequests(config -> {
try {
config
.antMatchers("/h2/**")
.permitAll()
.and()
.headers().frameOptions().sameOrigin();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// 시큐리티 기본 상태 - 인가 Authorization(인증 Authentication + 권한 Authority)
httpSecurity.authorizeHttpRequests(config -> config
// 패턴에 해당하는 주소는 허용
.antMatchers("/auth/login","/auth/join","/api/*/auth/**")
.permitAll()
// 패턴에 해당하는 주소는 권한이 있어야만 들어갈 수 있음
.antMatchers("/todoList")
.hasRole("USER")
// 모든 페이지를 인증하게 만듬
.anyRequest()
.authenticated());
// formLogin과 관련된 내용
httpSecurity.formLogin(config -> config
// 우리가 직접 만든 로그인 페이지를 사용한다.
.loginPage("/auth/login")
// loginProc라고 생각하면 됨
.loginProcessingUrl("/login-process")
// 흔히 말하는 로그인 아이디를 시큐리티에서 username이라고 한다.
// 우리가 해당하는 파라미터를 커스텀할 수 있다.
.usernameParameter("id")
// 비밀번호 파라미터도 커스텀가능하다.
.passwordParameter("pw")
// 로그인 실패 핸들러
.failureHandler(new CustomAuthFailureHandler())
// 로그인 성공 시 이동 페이지
// 두번째 매개변수는 로그인 성공 시 항상 세팅 페이지로 이동하게 함
.defaultSuccessUrl("/todoList", true));
return httpSecurity.build();
}
}
login.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="kr" id="loginPage">
<head>
<meta charset="UTF-8"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.15.2/css/all.css"
/>
<title>로그인</title>
</head>
<body>
<section style="background-color: #508bfc; min-height: 100vh">
<div class="container py-5 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-2-strong" style="border-radius: 1rem">
<div class="card-body p-5 text-center">
<h3 class="mb-3">
로그인
</h3>
<div class="input-group mb-3">
<span id="idAddOn" class="input-group-text">
아이디
</span>
<input
type="text"
id="id"
class="form-control"
aria-describedby="idAddOn"
/>
</div>
<div class="input-group mb-3">
<span id="pwAddOn" class="input-group-text">비밀번호</span>
<input
type="password"
id="pw"
class="form-control"
aria-describedby="pwAddOn"
/>
</div>
<div class="form-check d-flex justify-content-start mb-4">
<input
class="form-check-input"
type="checkbox"
id="rememberMe"
/>
<label class="form-check-label" for="rememberMe">
아이디 기억하기
</label>
</div>
<button
class="btn btn-primary"
type="button"
style="width: 100%"
onclick="requestLogin()"
>
로그인
</button>
<hr class="my-4"/>
<a href="/auth/join">아이디가 없으신가요? 회원가입</a>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
<script type="text/javascript">
document.querySelector("#pw").addEventListener("keyup", (event) => {
if (event.keyCode === 13) {
requestLogin();
}
});
const requestLogin = () => {
if (!validateFields()) {
return;
}
const idElement = document.getElementById("id");
const pwElement = document.getElementById("pw");
const rememberMeElement = document.getElementById("rememberMe");
if(rememberMeElement.checked){
localStorage.setItem("rememberId", idElement.value);
}
const formTag = document.createElement("form");
formTag.action = "/login-process";
formTag.method = "POST";
const idInputTag = document.createElement("input");
idInputTag.type = "hidden";
idInputTag.name = "id";
idInputTag.value = idElement.value;
formTag.appendChild(idInputTag);
const pwInputTag = document.createElement("input");
pwInputTag.type = "hidden";
pwInputTag.name = "pw";
pwInputTag.value = pwElement.value;
formTag.appendChild(pwInputTag);
document.body.appendChild(formTag);
formTag.submit();
};
const validateFields = () => {
const idElement = document.getElementById("id");
const pwElement = document.getElementById("pw");
if (idElement.value === "") {
alert("아이디를 입력해주세요.");
idElement.focus();
return false;
}
if (pwElement.value === "") {
alert("비밀번호를 입력해주세요.");
pwElement.focus();
return false;
}
return true;
};
const setLoginPage = () => {
const idElement = document.getElementById("id");
idElement.focus();
const rememberId = localStorage.getItem("rememberId");
if (rememberId !== null) {
const rememberMeElement = document.getElementById("rememberMe");
idElement.value = rememberId;
rememberMeElement.checked = true;
}
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const message = urlParams.get('message');
if (message != null && message != ""){
alert(message);
}
};
</script>
<script defer>
setLoginPage();
</script>
</html>
CustomUserDetails.java
package com.example.my.config.security;
import com.example.my.module.user.entity.UserEntity;
import com.example.my.module.user.entity.UserRoleEntity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
// 스프링 시큐리티에서 사용하는 유저 정보 저장소 => UserDetails
@RequiredArgsConstructor
@Getter
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
private final List<UserRoleEntity> userRoleEntityList;
//유저가 가진 모든 권한 가져오기
//유저는 여러가지 권한을 가질 수 있다.
//카페 개설자 ADMIN
//이용자 USER
//댓글을 10번 적어서 새싹회원->열심회원
//이용자 중에 스태프로 선정됨
//해당 이용자는 USER, 열심회원, STAFF 셋의 권한을 모두 가짐
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//1. userRoleEntityList에서 role(String)만 가져오기
//2. role로 GrantedAuthority 객체 만들기
//3. 리스트에 집어넣기
Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
for (UserRoleEntity userRoleEntity: userRoleEntityList) {
grantedAuthoritiesList.add(new GrantedAuthority() {
//익명 클래스
//인터페이스가 구현해 달라고 한 것들만 구현하면 익명 클래스를 만들 수 있다.
@Override
public String getAuthority() {
//스프링 시큐리티에서는 권한 앞에 ROLE_ 를 붙인다.
return "ROLE_" + userRoleEntity.getRole();
}
});
}
//결과값 리턴
return grantedAuthoritiesList;
}
// 비밀번호 가져오기
@Override
public String getPassword() {
return userEntity.getPw();
}
// 아이디 가져오기
@Override
public String getUsername() {
return userEntity.getId();
}
// 계정이 만료되지 않았는지 체크
@Override
public boolean isAccountNonExpired() {
// 만료될 서비스가 아니라서 true
return true;
}
// 계정이
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// 세션 방식이라 true
return true;
}
// 일시정지
@Override
public boolean isEnabled() {
// 일시정지, 신고
return true;
}
}
CustomUserDetailsService.java
package com.example.my.config.security;
import com.example.my.module.user.entity.UserEntity;
import com.example.my.module.user.entity.UserRoleEntity;
import com.example.my.module.user.repository.UserRepository;
import com.example.my.module.user.repository.UserRoleRepository;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
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.List;
// UserDatailsService가 기존에 있음
// 시큐리티 디펜던시 추가했을 때 로그인 돼었음 user라는 아이디로
// Service라고 붙이면 UserDetailsService 중에서 하나만 뜬다.
// Bean또는 Component는 interface 기준으로 하나만 떠야한다.
@Service
@Primary // 중복되는 component 중에서 1순위로 IOC컨테이너에 등록된다.
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
// DI 방법
// 1. @Autowired
// 2. setter
// 3. 생성자
private final UserRepository userRepository;
private final UserRoleRepository userRoleRepository;
// 로그인 단계
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// username으로 DB에 해당하는 userEntity가 있는지 확인
// userEntity가 null이면 아이디를 잘못 친것이니 에러를 터트린다
// userEntity가 null이 아니면
// UserEntity와 UserRoleEntity로 UserDetails를 만들어서 리턴한다.
UserEntity userEntity = userRepository.findById(username);
if(userEntity == null) {
throw new UsernameNotFoundException("아이디를 정확히 입력해주세요.");
}
List<UserRoleEntity> userRoleEntitiesList = userRoleRepository.findByUserIdx(userEntity.getIdx());
// if(userRoleEntityList.size() < 1){
// throw new AuthenticationCredentialsNotFoundException("권한이 없습니다,");
// }
return new CustomUserDetails(userEntity,userRoleEntitiesList);
}
}
CustomAuthFailureHandler.java
package com.example.my.config.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
// form로그인을 했기 때문에
// 페이지를 리턴해줘야한다.
// 그래서 SimpleUrlAuthenticationFailureHandler를 상속받았다.
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
// 로그인 실패 시 처리하는 용도
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String errorMessage;
if (exception instanceof UsernameNotFoundException) {
errorMessage = exception.getMessage();
} else {
errorMessage = "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요.";
}
String encodedErrorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
setDefaultFailureUrl("/auth/login?error=true&message=" + encodedErrorMessage);
super.onAuthenticationFailure(request, response, exception);
}
}
요점 정리(스프링 시큐리티)
- 수많은 필터가 있다.
- 그 중에 우리가 주로 쓰는 것은 Authentication
- Authentication에서 주로 커스텀하는 것은 UserDetails / UserDetailsService
- 컨트롤러에서 @AuthenticationPrinspal를 사용하면 UserDetails를 가져올 수 있다. (JSP의 Session 대용)
- antMatchers는 경로를 지정한다.
- permitAll은 필터 없이 해당 경로로 이동할 수 있게 한다.
- hasRole은 해당 권한이 있는 사람만 이동할 수 있게 한다.
git
https://github.com/youngmoon97/Spring