JAVA 17 Version
Spring 3.1.0 Version
Gradle
Jar
JPA - Maria DB
+ Security
+ AWS EC2 서버 , RDS, Bucket
*프로젝트 생성
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.green'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '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.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
01. Entity 생성
*참고 DB : 실제 설계한 DB와 차이 있음
(1) MyUser - 회원
- @Entity
- pk(no) , email , password, nickName, 소셜 유저 여부
- pk 에는 Sequence 연결 : @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen_user")
- sequence 생성 : 이름은 seq_user 이고 1001부터 시작하고 1증가 : @SequenceGenerator(name = "gen_user", // 제너레이터 이름 sequenceName = "seq_user", initialValue = 1001, allocationSize = 1)
- email 은 null x , 유일성 체크 : @Column(nullable = false, unique = true)
- 소셜계정은 password 가 필요없으므로 null허용 으로 했음 : @Column(nullable = true)
package com.green.nowon.domain.entity;
import java.util.HashSet;
import java.util.Set;
import com.green.nowon.security.MyRole;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class MyUserEntity extends BaseDateEntity{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen_user") //시퀀스 생성
private long no; //pk
@Column(nullable = false, unique = true)// null허용 x, 유니코드
private String email;
private String pass;
private String nickName;
private boolean isSocail;//소셜유저 여부에 따라 권한이 달라짐
(2) DateEntity - 날짜
- user 클래스와 상속 함으로 엮어준다.-> extends
- 추상클래스로 설정하여, 단독으로 설정하지 못하게 엮어준다
- Localdatetime type으로 설정하여 각 @어노테이션 적용
- @MappedSuperClass : 보통 생성자, 생성시간, 수정자, 수정시간을 다수의 엔티티가 공통으로 가지는 상황에서 적용하며, 상속관계 매핑, 엔티티, 테이블과 전혀 관계없이 부모클래스를 상속받는 자식 클래스에 매핑 정보만 제공한다. 직접 생성해서 사용할 일이 없으므로 추상클래스로 생성하는 것이 권장되고, 조회나 검색이 불가하다.
package com.green.nowon.domain.entity;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.MappedSuperclass;
@MappedSuperclass //슈퍼클래스로 설정
public abstract class BaseDateEntity { //추상클래스 - > 단독으로 클래스 생성 불가
@CreationTimestamp
private LocalDateTime createdDate;
@UpdateTimestamp
private LocalDateTime updatedDate;
}
(3) MyRole - enum 생성
- MyUser에 private Set<MyRole> roleSet; 으로 하여 MyRole Enum 생성
- 일반적으로 대문자로 씀
- 회원, 판매자, 관리자 구성
- ("ROLE_USER", "일반유저") 는 꼭 해야하는 것은 아니지만, 몇개 되지않아 진행하는 것으로 추후 시큐리티에서 roleName 으로 사용될 예정
- 문자열로 각각 필드에 구성 후 -> 초기화 필요 : private final String roleName , final String koName 후 @RequiredArgsConstructor
- 초기화 한것을 Enum 으로 사용하기 위하여 getMethod로 생성하기
package com.green.nowon.security;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum MyRole {
// 일반적으로 대문자를 쓴다.
USER("ROLE_USER","일반유저"),//0
SELLER("ROLE_SELLER","판매자"),//1
ADMIN("ROLE_ADMIN","관리자");//2
private final String roleName;//ROLE_USER 등
private final String koName; //일반유저 등
public String roleName() {return roleName;} //이넘처럼 사용하기 위하도록 get메서드 생성
public String koName() {return koName;}//이넘처럼 사용하기 위하도록 get메서드 생성
}
(4) Myuser 에 Myrole 종속하기
- @ElenmentCollection 을 roleset에 설정하여 MyUser 에 종속되게 설정
- MyUser 와 MyRole 테이블 네임 설정 @Table(name="") | @CollectionTable(name = "")
- MyRole이 테이블에 저장될 때 문자열로 저장되도록 : @Enumerated(EnumType.String)
- Null 반환 될 수있기에 = new HashSet<> 설정 단, 여기서 HashSet 은 Builder 와 충돌이있기에 @Builder.Default 설정
- 편의메서드 생성
package com.green.nowon.domain.entity;
import java.util.HashSet;
import java.util.Set;
import com.green.nowon.security.MyRole;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@SequenceGenerator(name = "gen_user", // 제너레이터 이름
sequenceName = "seq_user", initialValue = 1001, allocationSize = 1)//1001부터 1 상승
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "MyUser")
@Entity
public class MyUserEntity extends BaseDateEntity{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen_user") //시퀀스 생성
private long no; //pk
@Column(nullable = false, unique = true)// null허용 x, 유니코드
private String email;
private String pass;
private String nickName;
private boolean isSocail;//소셜유저 여부에 따라 권한이 달라짐
///role
@Builder.Default
@Enumerated(EnumType.STRING) //테이블 저장타입 설정
@CollectionTable(name = "role")
@ElementCollection(fetch = FetchType.EAGER) //1:N // 해당 Entity 에 종속되는 테이블
private Set<MyRole> roleSet = new HashSet<>(); //null이 없도록 //Builder 와 만나면 오류나는 HashSet이기에 @Builder.defalut 설정
public MyUserEntity addRole(MyRole role) {
//편의 메서드
roleSet.add(role);
return this;
}
}
02. Security 설정
(1) Config Class 생성
- @Configuration @EnableWebSecurity
- 아래 틀 붙여넣기
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()); return http.build(); } |
- index 페이지, 회원가입, 로그인 페이지 보안해제
- Token 사용여부에 따라 : .csrf(csrf->csrf.disable()); 적용 => 우리는 사용할거니까 주석 처리
- login 람다식 구성 넣어놓기 : http.formLogin(form->{});
package com.green.nowon.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/signup", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> {})
//.csrf(csrf->csrf.disable()); //jwt
;
return http.build();
}
}
(2) Index, 회원가입(signup) 페이지 만들기
- index
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="/signup"> 회원가입 </a>
<hr>
<h1>인덱스 페이지 </h1>
</body>
</html>
-> 회원가입
- <form 태그> : action , method="post" 설정
- input 안에 name 설정 여기 name 은 DTO 와 맞추어야함 (DTO 아직 안만들었음)
- Token 넣어주기
- Jpa 사용법 but 우리는 thymeleaf 니까 맞춰서 수정
jpa 문법: <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> |
thymeleaf 문법:
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> |
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>회원가입 페이지 </h1>
<form action="/signup" method="post">
<p>
<input type="text" name="email" placeholder="email" />
</p>
<p>
<input type="password" name="pass" placeholder="password" />
</p>
<p>
<input type="text" name="nickName" placeholder="nickname" /><!-- null 허용 -->
</p>
<p> <!-- thymeleaf 사용법 -->
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>회원가입</button>
</p>
</form>
</body>
</html>
03. Controller 설정
(1) 인덱스 - 회원가입 뷰 연결 Controller 생성
- @Controller
- SignUp GetMapping으로 구성
package com.green.nowon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CommonController {
@GetMapping("/signup") // 적용할 URL
public String signup() {
return "user/signup"; // 실제 위치
}
}
(2) 회원가입 실행 Controller 생성
- @Controller
- 회원가입 실행할 메서드 생성 + @PostMapping + 파라미터에 DTO 를 넣어야함 : UserSaveDTO dto
- returnn 값은 인덱스로 이동하도록 : return "redirect:/"
- 서비스(인터페이스) 생성 하기 : private final UserService service + @RequireArgsConstructor 해서 초기화
- saveProcess 메서드 생성
package com.green.nowon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.green.nowon.domain.dto.UserSaveDTO;
import com.green.nowon.service.UserService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService service; //초기화
// 회원가입
@PostMapping("/user")
public String signup(UserSaveDTO dto) {
// sigup 태그에 들어오는 것을 UserSaveDto에 저장
//@RequestBody는 안써도됨
service.saveProcess(dto);
return "redirect:/";
}
}
04. DTO 및 Repository 생성
(1) USerSaveDTO 생성 : 회원가입 시 저장할
- 회원가입 Form 태그와 동일하게 변수 설정
- @Setter
package com.green.nowon.domain.dto;
import lombok.Setter;
@Setter
public class UserSaveDTO {
// 회원가입 html form태그 name 일치!
private String email;
private String pass;
private String nickName;
}
(2) Entitiy 접근하기 위한 Repository.interface 구성
- extends JpaRepository< 연결할 엔티티 , Long >
package com.green.nowon.domain.entity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MyUserEntityRepository extends JpaRepository<MyUserEntity, Long> {
}
05. Password Encoder
- 메인에 password Encoder
package com.green.nowon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
public class SpringSecurity6ProcApplication { //메인
public static void main(String[] args) {
SpringApplication.run(SpringSecurity6ProcApplication.class, args);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(13); // 강도 13
}
}
06. UserService 생성
(1) interface Service 와 Class ServiceProcess 생성하여 ServiceProcess 에 implements UserSerive
(2) saveProcess 메서드 생성 및 오버라이드 하여 오버라이드된 saveProcess에 UserSaveDTO dto 파라미터 넣기
package com.green.nowon.service;
import com.green.nowon.domain.dto.UserSaveDTO;
public interface UserService {
void saveProcess(UserSaveDTO dto);
}
*UserServiceProcess
- Repository 선언하기 : private final MyUserEntityRepository dao; + @Require~~
- passwordencoder 선언하기 : private final PasswordEncoder pe;
- saveProdss 에 회원정보 및 인코딩된 passwordrk 저장 할 수 있도록 toEntity(pe) 와 role 도 저장되도록 .addRole(MyRole.USER));
(3) toEntity 메서드 DTO에서 만들기
- 회원가입 form 태그의 name 과 동일하게 변수명
- toEntity 메서드 생성하여, 타입은 MyUerEntity 로 변경하고 패스워드인코딩을 위해 파라미터는 PasswordEncoder pe로 하기
- Builde 처리하기, 여기서 Pass는 암호화를 위해 (pe.encode(pass))
- @Setter
package com.green.nowon.domain.dto;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.green.nowon.domain.entity.MyUserEntity;
import lombok.Setter;
@Setter
public class UserSaveDTO {
// 회원가입 html form태그 name 일치!
private String email;
private String pass;
private String nickName;
public MyUserEntity toEntity(PasswordEncoder pe) {
// TODO Auto-generated method stub
return MyUserEntity.builder()
.email(email)
.nickName(nickName)
.pass(pe.encode(pass)) //암호화
.build();
}
}
07. 회원가입 후 로그인
(1) index 수정
<!DOCTYPE html>
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<span sec:authentication="name"></span>
<a href="/signin"> 로그인 </a>
<a href="/signup"> 회원가입 </a>
<hr>
<h1>인덱스 페이지 </h1>
</body>
</html>
(2) login 페이지 생성 후 컨트롤러로 연결
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>로그인 페이지 </h1>
<form action="/signin" method="post">
<p>
<input type="text" name="email" placeholder="email" />
</p>
<p>
<input type="password" name="pass" placeholder="password" />
</p>
<p>
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>로그인</button>
</p>
</form>
</body>
</html>
(3) login controller
package com.green.nowon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CommonController {
@GetMapping("/signup") // 적용할 URL
public String signup() {
return "user/signup"; // 실제 위치
}
@GetMapping("/signin")
public String signin() {
return "user/signin";
}
}
(4) 보안 수정
package com.green.nowon.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/signup", "/signin").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/signin")
.usernameParameter("email") //default : username
.passwordParameter("pass")//default : password
.permitAll()
//userDetailsService가 인증작업을 해준다.
);
//.csrf(csrf->csrf.disable()); //jwt
return http.build();
}
}
08. CustomUserDetailService 생성 후 UserDetailService 적용하기 : 로그인
(1) CustomUserDetailService 클래스 생성 후 implements UserDetailService 한 후 오버라이드 +@Component
(2) CustomUserDetailService 에 Repository 선언해주기 +@Require~~~
(3) 생성된 오버라이드 메서드에 String 변수를 username -> email 로변경
- FindByEmailAndIsSocail(email, false) 메서드생성 후 예외처리를 람다식으로 처리 한후 MyUserEntity result에 담기: dao. FindByEmailAndIsSocail(email, false) .orElseThrow( () -> new new UsernameNotFoundException) ===> 유저를 찾지 못하였음
- return new User(email, password, role ) : User 은 이미 스프링에 존재하는 클래스이며, email은 이미 파라미터로 반환되고 있어 그대로 가져오고, password 는 result안에 있는 pass 로, role은 이넘이기에 stream과 map 과 collect 로 묶어서 가지고 온다.ㅣ
package com.green.nowon.security;
import java.util.stream.Collector;
import java.util.stream.Collectors;
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.Component;
import com.green.nowon.domain.entity.MyUserEntity;
import com.green.nowon.domain.entity.MyUserEntityRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {
// 사용자 DB에 USer 가 존재하면 UserDetails 정보를 넘겨주면 된다.
private final MyUserEntityRepository dao;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
System.out.println(">>>>>>>>" + email );
MyUserEntity result = dao.findByEmailAndIsSocail(email, false).orElseThrow(()
-> new UsernameNotFoundException("Bad User")); //Entity 컬럼명 기준 //And=where 절 //false = 0 과 일치한다는 뜻
return new User(email, result.getPass(), result.getRoleSet().stream()
.map(role -> new SimpleGrantedAuthority(role.roleName()))
.collect(Collectors.toSet())
);
}
}
(4) Repository FindByEmail
- 파라미터 변경 boolean isSocail
- Return type은 Optional<MyUserEntity> 변경
package com.green.nowon.domain.entity;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MyUserEntityRepository extends JpaRepository<MyUserEntity, Long> {
Optional<MyUserEntity> findByEmailAndIsSocail(String email, boolean isSocail);
}
09. 로그아웃
(1) Security Config 에 로그아웃 설정
- 람다식 표현
- .logoutUrl 과 .logoutSuccessUrl
- csrf 가 적용되어있으면, get이 아닌 Post로 설정해야 로그아웃이 가능함
package com.green.nowon.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/signup", "/signin").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/signin")
.usernameParameter("email") //default : username
.passwordParameter("pass")//default : password
.permitAll()
//userDetailsService가 인증작업을 해준다.
)
.logout(logout-> logout
.logoutUrl("/logout")//csrf 적용 시 Post로 요청해야 로그아웃이 가능하다
.logoutSuccessUrl("/")
)
;
//.csrf(csrf->csrf.disable()); //jwt
return http.build();
}
}
(2) 인덱스에 로그아웃 버튼 생성
- Post 이기에 Form 태그를 통해서 넣어주기
- Token 도 똑같이 ! 넣어주기
(3) 로그인/ 로그아웃에 따라 메뉴 창이 달라지도록 수정
방법 1) th:if
방법 2) sec:authorize="isAnonymous()" 과 sec:authorize="isAuthenticated()"
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<th:block th:if="${#authorization.expr('isAnonymous()')}"> <!-- 로그인 되지 않았을때 -->
<span>비회원</span>
</th:block>
<th:block sec:authorize="isAnonymous()"> <!-- 로그인 되지 않았을때 -->
<a href="/signin"> 로그인 </a>
<a href="/signup"> 회원가입 </a>
</th:block>
<th:block sec:authorize="isAuthenticated()"> <!-- 로그인 되었을때 -->
<span sec:authentication="name">로그인id</span>
<form action="/logout" method="POST" style="display: inline-block;">
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>로그아웃</button>
</form>
</th:block>
<hr>
<h1>인덱스 페이지 </h1>
</body>
</html>
++++
NickName 표기 하는 방법
** 먼저 콘솔에 Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. 경고창이 떠서 - > 아래와 같이 properties를 수정
#180 보다 2~3초 더 짧게 설정
spring.datasource.hikari.max-lifetime=177000
NickName 띄우기 - UserDetail 수정
- MyUserDetails Class 생성 후 Extends User --->User = security.core.userdetails.User로 import
- 버전 맞추기 : serialversionUID = 1L;
- 닉네임 필드로 넣기 : private String nick ; +@Getter
(1) 메서드 생성하기
- Public MyUserDetials(String username, String password, Collection<? ~~~ > 의 메서드 생성 후 복사 붙여넣기
- 위는 This 아래는 super
- 위 메서드의 파라미터에는 Entity 를 넣고 This 파라미터에는 CustomUserDetialService 의 파라미터 갖고와서 붙여넣은 후 Entity 로 맞게 수정
- this로 닉네임도 넣어주기
- Test 용으로 private String email | this.email = entity.getEmail() 도 넣어주었음
package com.green.nowon.security;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import com.green.nowon.domain.entity.MyUserEntity;
import lombok.Getter;
@Getter
public class MyUserDetails extends User {
private static final long serialVersionUID = 1L;
private String nick;
private String email;
MyUserDetails(MyUserEntity entity) { //추가정보
this(entity.getEmail(), entity.getPass(), entity.getRoleSet().stream()
.map(role -> new SimpleGrantedAuthority(role.roleName()))
.collect(Collectors.toSet())); //시큐리티가 필요로 하는 것들
this.nick=entity.getNickName();
this.email=entity.getEmail();
}
private MyUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities); //필수정보
}
}
(2) 기존 CustomUserDetailsService 를 아래와 같이 수정
package com.green.nowon.security;
import java.util.stream.Collectors;
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.Component;
import com.green.nowon.domain.entity.MyUserEntity;
import com.green.nowon.domain.entity.MyUserEntityRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {
// 사용자 DB에 USer 가 존재하면 UserDetails 정보를 넘겨주면 된다.
private final MyUserEntityRepository dao;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return new MyUserDetails(dao.findByEmailAndIsSocail(email, false)
.orElseThrow(()
-> new UsernameNotFoundException("Bad User")));
//Entity 컬럼명 기준 //And=where 절 //false = 0 과 일치한다는 뜻
}
}
(3) Index 수정
- 닉네임이 표기 될 수 있도록 : sec:authentication="principal.nick"
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<th:block th:if="${#authorization.expr('isAnonymous()')}"> <!-- 로그인 되지 않았을때 -->
<span>비회원</span>
</th:block>
<th:block sec:authorize="isAnonymous()"> <!-- 로그인 되지 않았을때 -->
<a href="/signin"> 로그인 </a>
<a href="/signup"> 회원가입 </a>
</th:block>
<th:block sec:authorize="isAuthenticated()"> <!-- 로그인 되었을때 -->
<span>
<span sec:authentication="name"> 로그인ID(Email) </span>
<<!-- sec:authentication="name" 과 같은 표현
span sec:authentication="principal.username"> 로그인ID(Email) </span> -->
(<span sec:authentication="principal.nick"> Nickname </span>)
</span>
<form action="/logout" method="POST" style="display: inline-block;">
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>로그아웃</button>
</form>
</th:block>
<hr>
<h1>인덱스 페이지 </h1>
</body>
</html>
최종!
Entity
package com.green.nowon.domain.entity;
import java.util.HashSet;
import java.util.Set;
import com.green.nowon.security.MyRole;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@SequenceGenerator(name = "gen_user", // 제너레이터 이름
sequenceName = "seq_user", initialValue = 1001, allocationSize = 1)//1001부터 1 상승
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "MyUser")
@Entity
public class MyUserEntity extends BaseDateEntity{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen_user") //시퀀스 생성
private long no; //pk
@Column(nullable = false, unique = true)// null허용 x, 유니코드
private String email;
private String pass;
private String nickName;
private boolean isSocail;//소셜유저 여부에 따라 권한이 달라짐
///role
@Builder.Default
@Enumerated(EnumType.STRING) //테이블 저장타입 설정
@CollectionTable(name = "role")
@ElementCollection(fetch = FetchType.EAGER) //1:N // 해당 Entity 에 종속되는 테이블
private Set<MyRole> roleSet = new HashSet<>(); //null이 없도록 //Builder 와 만나면 오류나는 HashSet이기에 @Builder.defalut 설정
public MyUserEntity addRole(MyRole role) {
//편의 메서드
roleSet.add(role);
return this;
}
}
package com.green.nowon.domain.entity;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.MappedSuperclass;
@MappedSuperclass //슈퍼클래스로 설정
public abstract class BaseDateEntity { //추상클래스 - > 단독으로 클래스 생성 불가
@CreationTimestamp
private LocalDateTime createdDate;
@UpdateTimestamp
private LocalDateTime updatedDate;
}
package com.green.nowon.security;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum MyRole {
// 일반적으로 대문자를 쓴다.
USER("ROLE_USER","일반유저"),//0
SELLER("ROLE_SELLER","판매자"),//1
ADMIN("ROLE_ADMIN","관리자");//2
private final String roleName;//ROLE_USER 등
private final String koName; //일반유저 등
public String roleName() {return roleName;} //이넘처럼 사용하기 위하도록 get메서드 생성
public String koName() {return koName;}//이넘처럼 사용하기 위하도록 get메서드 생성
}
DTO
package com.green.nowon.domain.dto;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.green.nowon.domain.entity.MyUserEntity;
import lombok.Setter;
@Setter
public class UserSaveDTO {
// 회원가입 html form태그 name 일치!
private String email;
private String pass;
private String nickName;
public MyUserEntity toEntity(PasswordEncoder pe) {
return MyUserEntity.builder()
.email(email)
.nickName(nickName)
.pass(pe.encode(pass)) //암호화
.build();
}
}
Repository
package com.green.nowon.domain.entity;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MyUserEntityRepository extends JpaRepository<MyUserEntity, Long> {
Optional<MyUserEntity> findByEmailAndIsSocail(String email, boolean isSocail);
}
Html
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<th:block th:if="${#authorization.expr('isAnonymous()')}"> <!-- 로그인 되지 않았을때 -->
<span>비회원</span>
</th:block>
<th:block sec:authorize="isAnonymous()"> <!-- 로그인 되지 않았을때 -->
<a href="/signin"> 로그인 </a>
<a href="/signup"> 회원가입 </a>
</th:block>
<th:block sec:authorize="isAuthenticated()"> <!-- 로그인 되었을때 -->
<span>
<span sec:authentication="name"> 로그인ID(Email) </span>
<<!-- sec:authentication="name" 과 같은 표현
span sec:authentication="principal.username"> 로그인ID(Email) </span> -->
(<span sec:authentication="principal.nick"> Nickname </span>)
</span>
<form action="/logout" method="POST" style="display: inline-block;">
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>로그아웃</button>
</form>
</th:block>
<hr>
<h1>인덱스 페이지 </h1>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>회원가입 페이지 </h1>
<form action="/signup" method="post">
<p>
<input type="text" name="email" placeholder="email" />
</p>
<p>
<input type="password" name="pass" placeholder="password" />
</p>
<p>
<input type="text" name="nickName" placeholder="nickname" /><!-- null 허용 -->
</p>
<p> <!-- thymeleaf 사용법 -->
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>회원가입</button>
</p>
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="//www.thymeleaf.org" >
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>로그인 페이지 </h1>
<form action="/signin" method="post">
<p>
<input type="text" name="email" placeholder="email" />
</p>
<p>
<input type="password" name="pass" placeholder="password" />
</p>
<p>
<input type="hidden" th:if ="${_csrf}"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button>로그인</button>
</p>
</form>
</body>
</html>
Controller
package com.green.nowon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class CommonController {
@GetMapping("/signup") // 적용할 URL
public String signup() {
return "user/signup"; // 실제 위치
}
@GetMapping("/signin")
public String signin() {
return "user/signin";
}
}
package com.green.nowon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.green.nowon.domain.dto.UserSaveDTO;
import com.green.nowon.service.UserService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService service; //초기화
// 회원가입
@PostMapping("/signup")
public String signup(UserSaveDTO dto) {// sigup 태그에 들어오는 것을 UserSaveDto에 저장
//@RequestBody는 안써도됨
service.saveProcess(dto);
return "redirect:/";
}
}
Security
package com.green.nowon.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/signup", "/signin").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/signin")
.usernameParameter("email") //default : username
.passwordParameter("pass")//default : password
.permitAll()
//userDetailsService가 인증작업을 해준다.
)
.logout(logout-> logout
.logoutUrl("/logout")//csrf 적용 시 Post로 요청해야 로그아웃이 가능하다
.logoutSuccessUrl("/")
)
;
//.csrf(csrf->csrf.disable()); //jwt
return http.build();
}
}
package com.green.nowon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootApplication
public class SpringSecurity6ProcApplication { //메인
public static void main(String[] args) {
SpringApplication.run(SpringSecurity6ProcApplication.class, args);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(13); // 강도 13
}
}
Service
package com.green.nowon.service;
import com.green.nowon.domain.dto.UserSaveDTO;
public interface UserService {
void saveProcess(UserSaveDTO dto);
}
package com.green.nowon.service.impl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.green.nowon.domain.dto.UserSaveDTO;
import com.green.nowon.domain.entity.MyUserEntityRepository;
import com.green.nowon.security.MyRole;
import com.green.nowon.service.UserService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserServiceProcess implements UserService {
private final MyUserEntityRepository dao;
private final PasswordEncoder pe;
@Override
public void saveProcess(UserSaveDTO dto) {
//Entity 접근 필요
dao.save(dto.toEntity(pe).addRole(MyRole.USER));
}
}
UserDetialService
package com.green.nowon.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.stereotype.Component;
import com.green.nowon.domain.entity.MyUserEntityRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {
// 사용자 DB에 USer 가 존재하면 UserDetails 정보를 넘겨주면 된다.
private final MyUserEntityRepository dao;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return new MyUserDetails(dao.findByEmailAndIsSocail(email, false)
.orElseThrow(()
-> new UsernameNotFoundException("Bad User")));
//Entity 컬럼명 기준 //And=where 절 //false = 0 과 일치한다는 뜻
}
}
package com.green.nowon.security;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import com.green.nowon.domain.entity.MyUserEntity;
import lombok.Getter;
@Getter
public class MyUserDetails extends User {
private static final long serialVersionUID = 1L;
private String nick;
private String email;
MyUserDetails(MyUserEntity entity) { //추가정보
this(entity.getEmail(), entity.getPass(), entity.getRoleSet().stream()
.map(role -> new SimpleGrantedAuthority(role.roleName()))
.collect(Collectors.toSet())); //시큐리티가 필요로 하는 것들
this.nick=entity.getNickName();
this.email=entity.getEmail();
}
private MyUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities); //필수정보
}
}
배포하기 + JAR
Jar는 내장톰캣이므로, 별도 압축해제 등 처리과정이 없음
(1) 프로젝트 오류 없는지 검토 후 Build 처리 후 Build/libs 에 .jar 파일 생성한다.
(2) Filezilla 를 통해 jar파일을 웹서버에 올린다 ( 나는 ec2 를 사용하였음 )
(3) EC2 서버 접속하여 백그라운드 중에도 서버를 실행하기 위하여 아래 명령어 작성
nohup java -jar 파일이름 &
(4) 실행중인 서버 검색
sudo netstat -tnlp
(5) 서버 종료
kill -term 번호
'Spring' 카테고리의 다른 글
쉘 스크립트 작성 (0) | 2023.06.15 |
---|---|
EC2 환경에서 배포시 포트번호:8080 을 없애기 위한 포트포워딩 설정 (0) | 2023.06.15 |
[SpringBoot] Security 회원가입/로그인/로그아웃 틀 (0) | 2023.06.13 |
[Spring-boot] 메일 발송 구현(Gmail) (0) | 2023.06.12 |
[springBoot] security 적용하기 1 - 회원가입 (0) | 2023.05.22 |