1) SpringBoot
1-1) 복습 예제
1-1-1) 비밀번호 암호화 / 로그인 / 로그아웃 기능 구현
1-2) 추가 정리사항(경로)
1) SpringBoot
1-1) 복습 예제
[web5] 안에 build.gradle 파일 내용 아래와 같이 작성
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'net.datasa'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
[src/main/resources] - [templates] 안에 application.properties 파일 내용 아래와 같이 작성
spring.application.name=web5
#접속 포트번호
server.port=8888
#Context Path
server.servlet.context-path=/
#Logback 사용. 전체를 대상으로 로깅 레벨 지정
#error>warn>info>debug>trace
logging.level.root=info
#특정 패키지를 대상으로 로깅 레벨 지정
logging.level.net.datasa.web5=debug
# MySQL 데이터베이스 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Seoul&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
# JPA 설정
# DB 구조와 Entity가 안 맞을 때 에러를 내주는 역할을 하는 것이 바로 및의 "~ddl-auto=validate" 설정이다.
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jackson.time-zone=Asia/Seoul
1-1-1) 로그인 / 로그아웃 기능 구현 기능 구현
(1) [src/main/resources] - [templates] 안에 home.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>web5</title>
</head>
<body>
<h1>[ web5 ]</h1>
<div sec:authorize="not isAuthenticated()">
<p>
<a th:href="@{/member/joinForm}">회원가입</a>
</p>
<p>
<a th:href="@{/member/loginForm}">로그인</a>
</p>
</div>
<div sec:authorize="isAuthenticated()">
<p>
<a th:href="@{/member/logout}">로그아웃</a>
</p>
<p>
<a href="@{/member/info}">개인정보 수정</a>
</p>
</div>
<p>
<a href="">게시판</a>
</p>
</body>
</html>
(2) [src/main/java] - [net.datasa.web5.security] 안에 WebSecurityConfig.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
//로그인 없이 접근 가능한 경로
private static final String[] PUBLIC_URLS = {
"/" //메인화면
, "/member/joinForm" //로그인 없이 접근할 수 있는 페이지
, "/member/join"
, "/member/idCheck"
, "/images/**"
, "/css/**"
, "/js/**"
};
// Bean : 메모리에 만들어진 객체(즉, 객체를 생성해서 메모리에 로드시켜 놓는 역할을 한다!)
@Bean
protected SecurityFilterChain config(HttpSecurity http) throws Exception {
http
//요청에 대한 권한 설정
.authorizeHttpRequests(author -> author
.requestMatchers(PUBLIC_URLS).permitAll() //모두 접근 허용
// 모든 요청은 인증이 필요하다는 코드이며, 바로 위의 코드만 인증 없이 모두 접근 가능하다!
.anyRequest().authenticated() //그 외의 모든 요청은 인증 필요
)
//HTTP Basic 인증을 사용하도록 설정
.httpBasic(Customizer.withDefaults())
//폼 로그인 설정
.formLogin(formLogin -> formLogin
.loginPage("/member/loginForm") //로그인폼 페이지 경로
.usernameParameter("memberId") //폼의 ID 파라미터 이름
.passwordParameter("memberPassword") //폼의 비밀번호 파라미터 이름
.loginProcessingUrl("/member/login") //로그인폼 제출하여 처리할 경로
.defaultSuccessUrl("/") //로그인 성공 시 이동할 경로
.permitAll() //로그인 페이지는 모두 접근 허용
)
//로그아웃 설정
.logout(logout -> logout
.logoutUrl("/member/logout") //로그아웃 처리 경로
.logoutSuccessUrl("/") //로그아웃 성공 시 이동할 경로
);
http
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
//비밀번호 암호화를 위한 인코더를 빈으로 등록
@Bean
public BCryptPasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
(3) [src/main/java] - [net.datasa.web5.controller] 안에 MemberController.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.MemberDTO;
import net.datasa.web5.service.MemberService;
/**
* 회원정보 관련 Controller
* */
@Slf4j
@RequestMapping("member")
@RequiredArgsConstructor
@Controller
public class MemberController {
private final MemberService memberService;
/**
* 회원가입 양식으로 이동
* */
@GetMapping("joinForm")
public String join() {
return "memberView/joinForm";
}
@PostMapping("join")
public String join(@ModelAttribute MemberDTO member) {
log.debug("전달된 회원정보 : {}", member);
// 서비스로 전달하여 저장
memberService.memberJoin(member);
return "redirect:/";
}
@GetMapping("idCheck")
public String idCheck() {
return "memberView/idCheck";
}
@PostMapping("idCheck")
public String idCheck(
@RequestParam("searchId") String searchId,
Model model) {
// ID 중복확인 폼에서 전달된 검색할 아이디를 받아서 log 출력
log.debug("전달된 검색 ID : {}", searchId);
// 서비스의 메소드로 검색할 아이디를 전달해서 조회
// 해당 아이디를 쓰는 회원이 있으면 false, 없으면 true 리턴 받음
boolean result = memberService.idCheck(searchId);
log.debug("전달된 ID 검색 결과 : {}", result);
// 검색한 아이디와 조회결과를 모델에 저장
model.addAttribute("searchId", searchId);
model.addAttribute("result", result);
// 검색 페이지로 다시 이동
return "memberView/idCheck";
}
// 로그인 폼
@GetMapping("loginForm")
public String login() {
return "memberView/loginForm";
}
}
(4) [src/main/java] - [net.datasa.web5.service] 안에 MemberService.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import net.datasa.web5.domain.dto.MemberDTO;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.repository.MemberRepository;
/**
* 회원정보 관련 처리 서비스
* */
@RequiredArgsConstructor
@Service
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
/**
* 가입 처리
* */
public void memberJoin(MemberDTO dto) {
MemberEntity entity = MemberEntity.builder()
// DTO로 전달받은 값들을 Entity에 세팅
.memberId(dto.getMemberId())
.memberPassword(passwordEncoder.encode(dto.getMemberPassword()))
.memberName(dto.getMemberName())
.email(dto.getEmail())
.phone(dto.getPhone())
.address(dto.getAddress())
// 기타 추가 데이터를 Entity에 세팅
.enabled(true)
.rolename("ROLE_USER")
.build();
// DB에 저장
memberRepository.save(entity);
}
/**
* 전달받은 아이디를 DB에서 조회하여 사용중인지 여부 리턴
* @param searchId 조회할 아이디
* @return 있으면 사용불가 false, 없으면 사용가능 true
* */
public boolean idCheck(String searchId) {
return !memberRepository.existsById(searchId);
/**
* // 바로 위의 코드와 동일한 기능을 수행하는 코드이다.
* // findById() : Primary Key 기준으로 검색을 하는 메서드(findById() : Optional 객체를 반환함)
* // orElse(null) : 결과가 없으면 "null" 값을 대입하는 메서드
* if (memberRepository.findById(searchId).isPresent()) {
return false;
} else {
return true;
}
*/
}
}
(5) [src/main/java] - [net.datasa.web5.security] 안에 AuthenticatedUser.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.security;
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticatedUser implements UserDetails {
// 객체 직렬화
private static final long serialVersionUID = 1562050567301951305L;
String id;
String password;
String roleName;
boolean enabled;
String name;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// "ROLE_USER", "ROLE_ADMIN"
return Collections.singletonList(new SimpleGrantedAuthority(roleName));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return id;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
(6) [src/main/java] - [net.datasa.web5.security] 안에 AuthenticatedUserDetailsService.java 파일 생성 후 아래와 같이 작성
package net.datasa.web5.security;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.repository.MemberRepository;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthenticatedUserDetailsService implements UserDetailsService {
private final BCryptPasswordEncoder passwordEncoder;
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 전달받은 아이디로 회원정보 DB에서 조회
// 없으면 예외
MemberEntity entity = memberRepository.findById(username)
.orElseThrow(() -> new EntityNotFoundException("회원정보가 없습니다."));
// 있으면 그 정보로 UserDetails 객체 생성하여 리턴함
AuthenticatedUser user = AuthenticatedUser.builder()
.id(username)
.password(entity.getMemberPassword())
.name(entity.getMemberName())
.roleName(entity.getRolename())
.enabled(entity.getEnabled())
.build();
log.debug("인증정보: {}", user);
return user;
}
}
(7) [src/main/resources] - [templates.memberView] 안에 loginForm.html 파일 생성 후 아래와 같이 작성
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>로그인</title>
<style>
#loginArea {
width: 700px;
margin: 0 auto;
text-align: center;
}
#loginForm {
display: flex;
flex-direction: column;
justify-content: center;
}
#formArea {
display: flex;
justify-content: center;
}
table, tr, td {
border: 2px solid black;
border-collapse: collapse;
padding: 10px;
margin: 20px 0 15px 0;
}
th {
width: 100px;
background-color: grey;
color: white;
}
td {
width: 300px;
text-align: left;
}
td > .memberInfo {
height: 25px;
}
#errorText {
color: red;
font-weight: bold;
height:50px;
}
</style>
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script>
$(document).ready(function() {
$("#loginForm").submit(check);
});
function check() {
let id = $("#memberId").val();
let pw = $("#memberPassword").val();
if (id.length == 0) {
alert("ID를 반드시 입력해주세요!!");
$("#memberId").focus();
$("#memberId").val('');
return false;
}
if (pw.length == 0) {
alert("비밀번호를 반드시 입력해주세요!!");
$("#memberPassword").focus();
$("#memberPassword").val('');
return false;
}
return true;
}
</script>
</head>
<body>
<div id="loginArea">
<h1>[ 로그인 ]</h1>
<form id="loginForm" th:action="@{/member/login}" method="post">
<div id="formArea">
<table>
<tr>
<th>
<label for="memberId">ID</label>
</th>
<td>
<input type="text" name="memberId" id="memberId" class="memberInfo" placeholder="ID 입력하세요" />
</td>
</tr>
<tr>
<th>
<label for="memberPassword">비밀번호</label>
</th>
<td>
<input type="password" name="memberPassword" id="memberPassword" class="memberInfo" placeholder="비밀번호 입력하세요" />
</td>
</tr>
</table>
</div>
<!-- 없는 ID이거나 비밀번호가 틀려서 로그인에 실패했을 경우 에러 문구 출력 -->
<div th:if="${param.error}" id="errorText">
ID 또는 비밀번호가 틀립니다.
</div>
<div id="btnArea">
<input type="submit" value="로그인" />
<input type="reset" value="취소" />
</div>
</form>
</div>
</body>
</html>
(5) 결과 화면
첫 접속 화면
"로그인" 문구 클릭 시 화면
- ID와 비밀번호 입력란에 이미 가입한 ID와 비밀번호를 입력할 시에만 로그인이 가능하도록 처리함
- 회원 ID가 없거나 비밀번호를 잘못 입력한 뒤 "로그인" 버튼을 클릭하면 로그인이 실패하면서 에러 문구(빨간색 글자)를 출력함
- 이미 가입한 ID와 그에 맞는 올바른 비밀번호를 입력 후 "로그인" 버튼을 클릭하면 다시 메인 페이지로 돌아옴
"로그아웃" 문구 클릭 시 화면
- 바로 위의 이미지에서 "로그아웃" 버튼을 클릭하면 위와 같이 로그인이 된 상태에서 로그아웃 상태로 페이지 내용을 바꿈
1-2) 추가 정리사항(경로)
(예시 Case)
root : /shop
현재 : /member/joinForm
Target : /member/join
"join"
"/member/join" >> 이 경우, "/shop/member/join"이 아닌 "/member/join"이 되어 버리기 때문에 경로 문제가 발생함!
Thymeleaf를 사용한 Case
@{join} >> /shop/member/join
@{/member/join} >> /shop/member/join
>> 따라서 경로 입력 시에는 Thymeleaf를 사용하는 것을 추천한다. 상대경로는 잘 못 쓰게 되면 에러 발생할 확률이 높다.
'SpringBoot' 카테고리의 다른 글
SpringBoot(25) - 복습 예제(글쓰기) (0) | 2024.07.30 |
---|---|
SpringBoot(24) - 복습 예제(개인정보 수정), 추가 정리사항(AuthenticatedUser, @Transactional) (0) | 2024.07.29 |
SpringBoot(22) - 복습 예제(회원가입 시 ID 중복 체크) (0) | 2024.07.25 |
SpringBoot(21) - 복습 예제(회원가입 기능 구현) (0) | 2024.07.24 |
SpringBoot(20) - Security 예제(Thymeleaf), 복습 예제(회원가입) (0) | 2024.07.23 |