Spring Security는
Spring에서 제공하는 강력한 보안기능으로 인증과 인가에 따라 접근을 제어
✅ 인증
- 본인확인 절차 (=로그인)
- Home 접근은 누구나 가능하지만 글을 작성할 땐 로그인 된 사용자 인증 필요
✅ 인가
- 인증된 사용자들 마다의 권한 등급 부여
- 인증된 자에게 Role을 부여해 권한을 관리 ‘User’,’Admin’ 등
- 네이버 카페를 이용하다 보면 회원등급에 따라 카테고리를 다르게 이용하는 그런 기능
그래서 열심히 댓글달고 출첵해서 등업신청했던 기억이,,, 🤭
스프링 시큐리티 설정 시
모든 요청은 인증을 요구하게 된다


✅ 미인증자의 요청 시 ‘/login’ redirect
- 로그인, 로그아웃 페이지는 Spring Security 기본 제공
-
- 기본 ID도 하나 제공되는데
- id는 user / 비밀번호는 app실행 시마다 콘솔 창에 띄워진다
-

- application.properties 에서 기본id 설정도 가능
💡 spring.security.user.name=user spring.security.user.password=1234
- 홈 화면 등 인증 없이 접근 가능한 페이지는 아래에서 SecurityFilterChain 코딩할 것
✅ 로그인 사용자의 인증과 권한
- 로그인 된 사용자에겐 ‘ROLE’을 부여해( ’USER’,’MEMBER’ 등 ) 접근을 허용
- 또 일부 페이지에는 인가방식으로 인증된 사용자 중 접근 권한을 부여
- 그 경우 ‘ROLE’ 이 ‘ADMIN’, ‘VIP’ 등이 될 수 있겠다
🔒Spring Security 사용하기🔒
✅ [ Build.gradle ] dependencies 추가
💡 implementation 'org.springframework.boot:spring-boot-starter-security’
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5’
- Spring Security 사용을 위한 의존성 추가
- Thymeleaf에서 Spring Security 웹 연동을 위한 의존성 추가
- 사용자의 이름, 권한, 인증 여부 등에 따른 view 설정 가능
- <sec:authentication="name"> 인증자 이름 반환
- <sec:authorize="hasRole('ROLE_USER')"> ’ROLE’이 ‘USER’일 경우
- 사용자의 이름, 권한, 인증 여부 등에 따른 view 설정 가능
❗이제 모든 페이지 접근에 인증을 요구 ❗
security
의존성 추가 만으로도 이제 모든 요청에 인증을 요구한다 Wow
아직 권한과 예외설정을 하지 않아서
접근요청 시 Security가 제공하는 Login페이지로 이동한다


security 제공 ID
id : user
password : 콘솔창에 뜸
첫번째로 홈화면 등 인증을 요하지 않는 모두 공개 페이지들을 설정하고
두번째로는 회원가입을 구현해보자
🔒Spring Security 회원가입 만들기🔒
코딩시작 👩🏻💻
✅ 디렉터리 전체 구조
생성 목록

- [ config > SecurityConfig ]
- [ controller > MemberController ]
- [ domain > entity > Member ]
- [domain > repository > MemberRepository(interface) ]
- [ dto > MemberDto ]
- [ dto > Role(enum) ]
- [ service > MemberService ]
- html은 member 디렉토리 생성 후
[ template>member ] 안에 join.html 생성
보이는 순서대로 작성했는데
1번 SecurityConfig부터 만들어 보자
1. SecurityConfig
Spring Security 인증에 관한 설정 페이지
현재는 모든 요청에 인증을 요구하나
설정을 추가하면 인증을 요구하지 않는 페이지를 설정할 수 있다
@Configuration으로 설정 페이지 임을 명시하고
@Bean으로 객체를 등록한다
Security의 SecurityFilterChain을 이용해서 접근 허용과 제한을 관리할건데
먼저 코드를 보고 아래 설명을 이해하자
[ SecurityConfig ]
package com.rim.aboardcado.config;
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
@EnableWebSecurity
public class SecurityConfig {
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 요청에 대한 보안인증 체크 선언
.mvcMatchers("/css/**","/image/**","/error").permitAll()
.mvcMatchers("/","/join","/login","/search/**").permitAll()
.mvcMatchers("/post").hasRole("USER")
.anyRequest().authenticated() // 모든 요청에 체크 (권한 상관X)
;
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
✅ @Configuration
- ‘Configuration’은 ‘환경설정’이란 뜻으로 설정 역할을 하는 Class를 지정
- 해당 class 하위에 @Bean을 사용하면 Bean등록 가능
- Bean등록은 XML에도 가능하지만 @Configuration을 이용한 자바 설정파일을 이용하는 추세
✅ @Bean
- Spring IoC 컨테이너가 관리하는 자바 객체를 빈(Bean)으로 사용자가 생성 및 등록
- 메소드 레벨에서 선언하며, 반환되는 객체를 개발자가 수동으로 등록하는 @어노테이션
- @Configuration으로 ComponentScan의 대상이 되기 때문에
- @Component는 붙이지 않아도 된다
✅ SecurityFilterChain
Spring Security는 필터를 통해서 인증과 권한을 체크
👇아래 필터들이 순서대로 실행되면서
👇Spring Security에서 인증 및 권한부여관련 처리와 보안관련된 처리를 한다
👉아무것도 설정하지 않은 SecurityFilterChain기본 세팅 필터
- WebAsyncManagerIntegrationFilter스프링웹과 SecurityContext간의 통합을 제공
- SecurityContextPersistenceFilterSecurityContext 객체 생성 저장, 인증성공→ 세션에 저장
- HeaderWriterFilter응답 헤더에 시큐리티 관련 헤더를 추가하는 필터
- CsrfFiltercsrf공격을 방어하는 필터, form Post전송 시 사용
- 모든 요청에 랜덤하게 생성된 csrf토큰을 요구→ 서버토큰과 비교, 일치해야 통과
- LogoutFilter로그아웃관련 처리를 하는 필터
- UsernamePasswordAuthenticationFilter아이디/비밀번호 로그인 시 인증 관련 필터
- DefaultLoginPageGeneratingFilter기본 로그인 페이지 생성 필터
- DefaultLogoutPageGeneratingFilter기본 로그아웃 페이지 생성필터
- BasicAuthencationFilterHttpBasic 인증을 처리
- RequestCacheAwareFilter현재 요청에 캐시 요청이 있는지 확인 및 처리
- SecurityContextHolderAwareRequestFilter시큐리티 관련 메서드들을 스프링 시큐리티 기반으로 구현체를 연결
- AnonymousAuthenticationFilterSecurityContext에 Authentication이 Null일경우 AnonymousAuthentication 생성
- SessionManagementFilter세션관련 변조방지, 유효하지 않는 세션 체크, 동시성 제어, 세션생성전략 설정
- ExceptionTranslationFilterAuthenticationException, AccessDeniedException 발생시 처리
- FilterSecurityInterceptor마지막으로 실행되는 필터이며 리소스에 대한 접근이 가능한지 체크→ 처리
기본 세팅 필터들만 봐도 앞 서 봤던 security 제공 로그인, 로그아웃 기능과
앞으로 html에서 사용할 csrf 토큰 등 눈에 띄는 기능들이 보인다
ABoardcado는 이 기능들을 이용해서 회원가입, 로그인을 구현해볼 것이다
📈 충격번외 - 지금도 스프링은 변화하고 있다 📈
✅ 코드설명
- authorizeRequests()
- 요청에 대한 보안인증 체크 선언
- mvcMatchers("/css/**", "/js/**", "/image/**").permitAll()
- 해당 URL에 대해서는 인증 요구X, 모두 허용(permit)
- 미인증자도 css, 이미지 등 static resource은 공개
- 로그인 전이라고 화면이 html 날것으로 공개되면 머쓱,,
- “/’ 홈화면과 이제 만들 회원가입 “/join”도 허용해뒀다
- 미인증자도 css, 이미지 등 static resource은 공개
- 해당 URL에 대해서는 인증 요구X, 모두 허용(permit)
- mvcMatchers("/post").hasRole("USER")
- 해당 URL에 대해서는 인증된 “USER” Role을 가지고 있어야 함
- hasRole(”ADMIN”)이라면 “ADMIN” Role 필요
- 해당 URL에 대해서는 인증된 “USER” Role을 가지고 있어야 함
- anyRequest().authenticated()
- 열거 URL 외 모든 요청에 인증 체크 ( Role 상관X )
✅ BCryptPasswordEncoder
- 비밀번호를 암호화 하기위한 Bean
- 비밀번호를 DB에 그대로 저장하면, DB가 해킹당했을 때 회원정보가 그대로 노출된다
BCryptPasswordEncoder 의 해시함수를 이용해 비밀번호를 암호화하여 저장
2. MemberDto
[ MemberDto ]
package com.rim.aboardcado.dto;
import lombok.Builder;
import lombok.Getter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
@Getter
@Builder
public class MemberDto {
private final PasswordEncoder passwordEncoder;
private String name;
private String email;
private String password;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
// MemberDto -> Member
public Member toEntity(MemberDto memberDto) {
return Member.builder()
.name(name)
.password(passwordEncoder.encode(password))
.email(email)
.role(Role.USER)
.build();
}
}
✅ PasswordEncoder
- password를 DB 저장 시에 암호화하여 저장
- SecurityConfig에서 @Bean으로 등록했다
3. Role
package com.rim.aboardcado.dto;
public enum Role {
USER, ADMIN
}
✅ Spring Security에서 ‘인가’에 해당하는 등급을 등록
✅ enum Type
4. Member (Entity)
package com.rim.aboardcado.domain.entity;
import com.rim.aboardcado.dto.Role;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table
@Getter
@Builder
@ToString
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(unique = true)
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
✅ @Table
- Entity와 매핑할 table. 즉 DB에서 schema 를 매핑
- application.properties 에서 Data Source 설정했던 걸 기억하자
- 🥑ABoardcado 는 update로 설정. DB테이블과 매핑 Entity를 비교해 변경사항 수정
- class명과 동일하다면 생략 가능 (위 코드 생략가능)
✅ @Getter
- 변수 값을 호출하는 getter() 메서드 자동 생성
✅ @Builder
- 필드 초기화 작업을 도와줌
- Setter 와 비슷한 역할 this.name = name; 의 기능으로 보면된다
- 위 코드처럼 @NoArgsConstructor 로 생성된 생성자에 설정하는 @Builder 는해당 생성자를 사용하는 Builder만 생성되어, 의미있는 객채만 생성 (무분별X)
✅ @Entity
- JPA가 관리하는 Entity. DB 테이블과 매핑
- 생성자 필수
- SpringBoot에서는 @Autowired, 빈 방식보다 생성자로 빈 주입을 권장
✅ @EntityListeners(AuditingEntityListener.class)
- 해당 class에 Auditing 기능을 포함 선언
JPA에서는 JPA Audit 을 제공하는데
Audit 은 ‘감시하다’의 뜻으로 시간기록에 대해 자동으로 값을 생성@CreatedDate, @LastModifiedDate의 기능을 사용하기 위해 선언✅ JPA Auditing 활성화는 main class에서 @EnableJpaAuditing 추가 설정 ❗

✅ @NoArgsConstructor(access = AccessLevel.PROTECTED)
- 파라미터가 없는 기본 생성자 생성
- PROTECTED 무분별한 생성을 막기 위함
✅ @AllArgsConstructor
- 모든 필드 값을 파라미터로 받는 생성자 생성
- @Builder를 사용할 때, 생성한 생성자가 아무것도 없다면
@AllArgsConstructor(access = AccessLevel.PACKAGE)가 자동 적용→ 위 코드는 @NoArgsConstructor 기본생성자를 넣었기때문에 명시해줘야 함
✅ @Id
- JPA Entity 객체의 식별자로 사용
✅ @GeneratedValue(strategy = GenerationType.IDENTITY)
- 기본 key 생성을 DB에 위임 ( = AUTO_INCREMENT )
- DB 마다 IDENTITY(MySQL), SEQUENCE 등 맞는 방식으로 해야하는데 AUTO는 알아서 적합배정
✅ @Column
- 객체를 DB table의 컬럼에 매핑. nullable, unique, length 등 속성 입력가능
- column 속성은 웹에서 유효성 검사를 해주지 않기 때문에 다음편에서 Validation 추가
✅ @CreatedDate, @LastModifiedDate
- JPA에서 생성, 수정된 시간 정보 자동 생성
5. MemberRepository
package com.rim.aboardcado.domain.repository;
import com.rim.aboardcado.domain.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email);
}
✅ interface 생성, JpaRepository 상속
매핑할
< Entity, Id type >
Member 엔티티와 매핑하고 id의 타입인 Long 적어주기
✅ JPA를 상속하면 CRUD 메소드 지원
JPA로 회원가입, 수정, 탈퇴가 다 가능
✅ ‘ By ‘ 를 기준으로 Where절에 대응
메소드명을 해석해 처리하기 때문에 상속하면 끝 🙌
findById(id);findByEmail(email); 등
✅ DB에서 email로 찾은 Member Entity 반환
6. MemberService
package com.rim.aboardcado.service;
import com.rim.aboardcado.domain.entity.Member;
import com.rim.aboardcado.domain.repository.MemberRepository;
import com.rim.aboardcado.dto.MemberDto;
import com.rim.aboardcado.dto.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
// 회원가입
public Member saveMember(MemberDto memberDto) {
// email 중복체크
Member findMember = memberRepository.findByEmail(memberDto.getEmail());
if (findMember != null) {
throw new IllegalStateException("이미 가입된 회원입니다.");
}
return memberRepository.save(toEntity(memberDto));
}
// MemberDto -> Member
public Member toEntity(MemberDto memberDto) {
return Member.builder()
.name(memberDto.getName())
.password(passwordEncoder.encode(memberDto.getPassword()))
.email(memberDto.getEmail())
.role(Role.USER)
.build();
}
}
✅ findMember()
- memberDto로 들어온 email로 DB에 data가 있는지 조회
- 있으면 이미 가입된 이메일 alert
✅ toEntity
- DB를 조작하는 Entity를 직접 사용하는 것은 여러 위험이 따르니
- Dto를 두고 data를 변환해서 처리하는데
- 마지막 저장은 toEntity로 Entity로 변환해 DB에 전달
✅ @Service
- Service 비즈니스 로직임을 선언
- Controller에서 로직을 호출하여 사용
✅ @AllArgsConstructor
- 모든 필드값을 포함한 생성자 생성
✅ @Transaction
- 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용
- 실행, 종료(commit), 예외(rollback)작업 도중 오류가 발생했을 때 이전 모든 작업들이 성공하였더라도 작업 전 시점으로 되돌린다
- class, method() 에 적용이 가능하고, method() (소분류)가 우선 적용
- JPA Service에서 필수
7. MemberController
package com.rim.aboardcado.controller;
import com.rim.aboardcado.dto.MemberDto;
import com.rim.aboardcado.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/join")
public String join( ) {
return "member/join";
}
@PostMapping("/join")
public String join(MemberDto memberDto) {
memberService.saveMember(memberDto);
return "redirect:/";
}
}
✅ Controller는 Http 요청에 따른 Service로직과 View를 호출하는데
Board에서와 동일하게 save()는 JPA가 제공하는 기능이기 때문에 특별한 게 없다 😗
8. join.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<div layout:fragment="content">
<div class="container">
<h1 style="color:#5a5a5a"> Hello:) I'm ABoardcado!</h1>
<br>
<h6 style="color:green"> <b>Create account</b></h6>
<hr>
<!--회원가입 폼-->
<form action="/join" method="post">
<div class="form-group row">
<label for="inputName" class="col-sm-2 col-form-label"><strong>이름</strong></label>
<div class="col-sm-10">
<input type="text" name="name" class="form-control" id="inputName" />
</div>
</div>
<div class="form-group row">
<label for="inputPassword" class="col-sm-2 col-form-label"><strong>비밀번호</strong></label>
<div class="col-sm-10">
<input type="password" name="password" class="form-control" id="inputPassword" />
</div>
</div>
<div class="form-group row">
<label for="inputEmail" class="col-sm-2 col-form-label"><strong>Email</strong></label>
<div class="col-sm-10">
<input type="text" name="email" class="form-control" id="inputEmail"></input>
</div>
</div>
<div class="row">
<div class="col-auto mr-auto"></div>
<div class="col-auto">
<input class="btn btn-success" type="submit" role="button" value="signup" />
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}">
<a class="btn btn-danger" th:href="@{'/'}" role="button">취소</a>
</div>
</div>
</form>
</div>
</div>
</html>
🔐 CSRF ?
Cross Site Request Forgery
Spring Security를 사용할 경우 기본적으로 CSRF를 방어하기 위해 모든 method=”POST” 전송에는 CSRF토큰 값이 필요
CSRF 토큰은 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰.
사용자의 세션에 임의의 값을 저장해 요청마다 그 값을 포함하여 전송
→ 서버는 세션에 저장된 값과 요청이 온 값이 일치하는지 확인해 CSRF 공격을 방어
✅ CSRF공격은
웹 어플리케이션 취약점 중 하나로 인터넷 사용자가 자신의 의지와 무관하게 해커가 의도한 행위(등록, 수정, 삭제 등)을 특정 웹사이트에 요청하게 만드는 공격
✅ CSRF 공격이 이루어지는 조건
- 위조 요청을 전송하는 서비스에 (피해자가) 로그인한 상태
- (피해자가) 해커가 만든 피싱 사이트에 접속
위 두가지 조건은 생각보다 쉽게 적용된다
우리는 카카오, 구글, 인스타 등 자동로그인을 해놓은 경우가 많고
피싱 사이트를 접속하지 않아도 해커가 XSS공격을 성공한 정상 사이트를 통해서도 공격이 된다
☝️ 예를 들어 위조된 toss사이트가 있다고 치면 ☝️
사용자는 toss인줄 알고 돈을 송금할 것이다
하지만 송금 폼에는 해커가 의도한 계좌번호가 적혀있고 그 곳으로 돈이 송금될 것이다
<form action="http://toss/api/sendMoney" method="post">
<input type="hidden" name="accountNumber" value="해커가 의도한 계좌번호" />
<input type="number" name="fake" value="계좌번호를 입력해주세요" />
<input type="submit" value="송금하기"/>
</form>
이건 물론 막연한 예시지만 이런 식으로의 피해가 발생하고있다 (스미싱 문자, SNS 광고글 등)
이런 상황을 방지하게 위한 대표적 2가지 방법이있다.
✅ CSRF 공격 방어 방법
- Security Token 사용 (CSRF Token) (🥑ABoardcado 사용)
- Referrer 검증
일반 적으로 CSRF 공격 방어는 조회성(method=”GET”)에는 보통은 방어하지 않고
데이터 조작이 이루어지느 등록, 수정, 삭제(”POST”,”PATCH”,”DELETE”)에 적용
Referrer 검증
Back-end에서 request의 referrer를 확인해 domain ( *.toss.com )이 일치하는 지 검증하는 방법
일반적으로 referrer 검증만으로 대부분의 CSRF 공격을 방어할 수 있다.
하지만 같은 도메인 내의 페이지에 XSS 취약점이 있는 경우 CSRF 공격에 취약해진다.
domain 단위 검증에서 좀 더 세밀하게 페이지 단위까지 일치하는지 검증을 하면 도메인 내의 타 페이지에서의 XSS 취약점에 의한 CSRF 공격을 방어할 수 있다

아직 login은 구현하지 않았기 때문에
DB Workbench에서 확인해보니
🌟 짜잔 🌟

성공적으로 완료!!!!!
비밀번호는 “비밀이지롱” 이라고 입력했지만 passwordEncoder가 암호화하여 저장!
후후 근데 로그인으로 넘어가기 전에 유효성검사 먼저 추가해야한다
지금은 Null으로도 NullNull하게 가입가능;;;;;ㅜㅜ
다음편 @Valid 레츠고 💨
'Project > 🥑JPA + SpringSecurity 게시판' 카테고리의 다른 글
| [JPA + SpringSecurity 게시판] #11 JPA 검색 만들기 (0) | 2022.06.13 |
|---|---|
| [JPA + SpringSecurity 게시판] #10 Pageable 페이징 처리 (0) | 2022.05.31 |
| [JPA + SpringSecurity 게시판] #9 글 삭제하기 (0) | 2022.05.18 |
| [JPA + SpringSecurity 게시판] #8 글 수정하기 (0) | 2022.05.10 |
| [JPA + SpringSecurity 게시판] #7 글 상세보기 (0) | 2022.05.02 |