2023. 10. 23. 19:37ㆍSpringboot
1. Requirements
- Spring boot version 2.4.5
2. Stacks
Springboot
- Loombook
- Spring Data JPA
- Spring Boot DevTools
- Spring Security
- Spring Web
- Spring boot version 2.4.5
Mybatis
3. CSRF token
이전 코드에는 csrf token을 비활성화하여 만들어져 웹 애플리케이션의 보안에서 문제점이 많았습니다.
비활성화시에 대한 문제점입니다.
1) CSRF 공격 위험: CSRF 공격은 악의적인 웹사이트가 사용자의 브라우저를 통해 인증된 세션을 사용하여 사용자의 동의 없이 서버에 요청을 보내는 공격입니다. CSRF 토큰을 사용하지 않으면 이러한 공격을 쉽게 수행할 수 있습니다.
2) 사용자의 권한 남용: CSRF 공격을 통해 공격자는 사용자의 인증된 세션을 남용하여 서버의 보호된 기능을 실행할 수 있습니다.
예를 들어, 사용자가 의도하지 않은 상태에서 계정 비밀번호를 변경하거나 중요한 데이터를 삭제할 수 있습니다.
기백 엔드포인트 보호 미흡: CSRF 공격으로부터 보호되지 않은 애플리케이션은 사용자와 관련된 중요한 데이터 또는 작업을 변경하는 백엔드 엔드포인트에 노출됩니다. 이로 인해 민감한 데이터의 무단 수정 가능성이 증가하며, 데이터의 무단 삭제 또는 수정이 발생할 수 있습니다.
3) 보안 위반 및 개인 정보 노출: CSRF 공격으로 공격자가 사용자의 인증 정보를 훔칠 수 있는 상황이 발생할 수 있습니다.
예를 들어, 사용자의 세션 쿠키를 이용하여 공격자가 사용자 계정에 로그인할 수 있으므로, 민감한 정보에 액세스할 수 있습니다.
4) 신뢰성 하락: CSRF 공격으로 인해 사용자가 애플리케이션의 신뢰성을 상실할 수 있으며,
사용자는 애플리케이션을 사용하지 않거나 신뢰하지 않게 될 수 있습니다.
이에 대한 해결점과 링크입니다.
1. signin.jsp
<!-- 수많은 코드들...-->
<!-- form만 가져왔습니다-->
<!--로그인 인풋-->
<form class="login__input" action="/auth/signin" method="post">
<input type="text" name="username" placeholder="유저네임" required="required" />
<input type="password" name="password" placeholder="비밀번호" required="required" />
<button>로그인</button>
</form>
<!--로그인 인풋end-->
[스프링 Security] CSRF 토큰 이야기 - 그래서 개발자는 뭘 하면 되죠
CSRF 토큰을 통한 보호 이 글은 Synchronizer Token Pattern에 기반한 CSRF 활용의 빠른 지침이다. Q1. 클라이언트는 어떻게 CSRF 토큰을 얻나요 방법1. 서버가 HTML 렌더링 시 meta 태그에 토큰 집어 넣어 주기
blog.paimon.studio
4. Code
1. signin.jsp
localhost:8080/auth/sigin으로 접속했을 때 나오는 첫 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
</head>
<body>
<div class="container">
<main class="loginMain">
<!--로그인섹션-->
<section class="login">
<!--로그인박스-->
<article class="login__form__container">
<!--로그인 폼-->
<div class="login__form">
<h1><img src="/tmp.jpg" alt=""></h1>
<!--로그인 인풋-->
<!-- 수많은 코드들...-->
<form class="login__input" action="/auth/signin" method="post">
<input type="text" name="username" placeholder="유저네임" required="required" />
<input type="password" name="password" placeholder="비밀번호" required="required" />
<button>로그인</button>
</form>
<!--로그인 인풋end-->
<!-- 또는 -->
<div class="login__horizon">
<div class="br"></div>
<div class="or">또는</div>
<div class="br"></div>
</div>
<!-- 또는end -->
<!-- Oauth 소셜로그인 -->
<div class="login__facebook">
<button>
<i class="fab fa-facebook-square"></i>
<span>Facebook으로 로그인</span>
</button>
</div>
<!-- Oauth 소셜로그인end -->
</div>
<!--계정이 없으신가요?-->
<div class="login__register">
<span>계정이 없으신가요?</span>
<a href="/auth/signup">가입하기</a>
</div>
<!--계정이 없으신가요?end-->
</article>
</section>
</main>
</div>
</body>
</html>
2. signup.jsp
- localhost:8080/auth/signup
- auth/signin의 회원가입 버튼을 누를 시 나오는 페이지
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test</title>
<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />
</head>
<body>
<div class="container">
<main class="loginMain">
<!--회원가입섹션-->
<section class="login">
<article class="login__form__container">
<!--회원가입 폼-->
<div class="login__form">
<!--로고-->
<h1><img src="/images/tmp.jpg" alt=""></h1>
<!--로고end-->
<!--회원가입 인풋-->
<form class="login__input" action="/auth/signup" method = "post">
<input type="text" name="userId" placeholder="유저네임" required="required" maxlength = "30"/>
<input type="password" name="password" placeholder="패스워드" required="required" />
<input type="email" name="email" placeholder="이메일" required="required" />
<input type="text" name="name" placeholder="이름" required="required" />
<button>가입</button>
</form>
<!--회원가입 인풋end-->
</div>
<!--회원가입 폼end-->
<!--계정이 있으신가요?-->
<div class="login__register">
<span>계정이 있으신가요?</span>
<a href="/auth/signin">로그인</a>
</div>
<!--계정이 있으신가요?end-->
</article>
</section>
</main>
</div>
</body>
</html>
3. application.yml
- springboot를 실행할 때 기본적으로 필요한 요소를 적음
- datasource는 mariadb를 직접 지정해야 사용할 수 있음
server:
port: 8080(자신이 7070으로 설정했다면 localhost:7070으로 들어가야함)
servlet:
context-path: /
encoding:
charset: utf-8
enabled: true
spring:
mvc:
view:
prefix: (src/main밑에 있는 폴더명에 저장)
suffix: .jsp
datasource:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://localhost:(mybatis 연결포트)/(자신이 연결한 데이터베이스 이름)?serverTimezone=Asia/Seoul
username: (자신이 만든 아이디)
password: (자신이 만든 비밀번호)
jpa:
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
servlet:
multipart:
enabled: true
max-file-size: 2MB
security:
user:
name: test
password: 1234
4. AuthController.java
- @Controller : *. jsp페이지를 직접 받는 컨트롤러, 주로 파일을 리턴 받습니다.
- @RequiredArgsConstrutor : private final키워드의 생성자를 사용할 수 있게 합니다.
- 화면을 직접 받으며 @Valid와 BindingResult를 사용하여 유효성검사를 시행합니다.
- 회원가입을 위해 유효성검사를 마친 signupDto를 service의 비즈니스 로직 수행을 user객체를 보냅니다.
package com.pjh.web;
import java.util.HashMap;
import java.util.Map;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.pjh.domain.user.User;
import com.pjh.service.AuthService;
import com.pjh.web.dto.SignupDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signin")
public String login() {
return "/auth/signin";
}
@GetMapping("/auth/signup")
public String join() {
return "/auth/signup";
}
@PostMapping("/auth/join")
public String login(
@Valid SignupDto signupDto,
BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
Map<String, String> errorMap = new HashMap<>();
for(FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(),error.getDefaultMessage());
}
//throw new CustomValidationApiException("sorry", errorMap);
}
authService.회원가입(signupDto.toEntity());
return "/auth/signin";
}
/*로그인 페이지 <%include ../layout/*.jsp%> 설정 시에 spring dependencies 설정에 security tag-libs 설정 후 재부팅해야함 */
}
5. SignupDto.java
- signup.jsp에서 받는 form의 필드들을 전달받아 service로 보내는 형식을 가집니다.
- toEntity()의 builder(). build() 형태를 통해 순서가 뒤 바뀌어도 유저 객체에 전달되게 합니다.
package com.pjh.web.dto;
import javax.validation.constraints.NotBlank;
import com.pjh.domain.user.User;
import lombok.Data;
@Data
public class SignupDto {
@NotBlank
private String username;
@NotBlank
private String password;
@NotBlank
private String email;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.email(email)
.build();
}
}
6. AuthService.java
- @Service 비즈니스 로직을 구현을 위해 생성하는 service 어노테이션입니다.
- @Transactional DB에 트랜잭션(일의 최소 단위) 처리를 springboot에서 구현한 형태로 수행할 수 있게 합니다.
- BCrytPasswordEncoder : DB에 저장된 패스워드를 bcryt 화하여 저장합니다.
패스워드를 암호화해 주는 메서드입니다. encde() 메서드는 SHA-1,
8바이트로 결합된 해쉬, 랜덤 하게 생성된 솔트(salt)를 지원합니다.
ex) 비밀번호 : 1234 -> db에 저장되는 비밀번호 :
$2a$10$0i10aVWcB9SDN6UYT2hav.m3phbL6rFhlqSKTIIHsqyoFmfHdrGbC
package com.pjh.service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pjh.domain.RoleType;
import com.pjh.domain.user.User;
import com.pjh.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AuthService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCrytBCryptPasswordEncoder;
@Transactional
// 트랜잭션
// ex) 송금 서비스 a가 b에게 송금 (a_db update)
// b의 금액이 변화해야함 (d_db update)
// a_db update와 b_db update 두가지 일을 한가지 일로 묶어서 진행하며
// 이를 하기 전에 아무도 건들지 말라는 표시
public User 회원가입(User user) {
User userEntity = userRepository.save(user);
// userRepository에 user 객체를 저장
String rawPassword = user.getPassword();
System.out.println(user.getEmail()+" " +user.getPassword()+" "+user.getUsername());
String bCrytPassword = bCrytBCryptPasswordEncoder.encode(rawPassword);
user.setPassword(bCrytPassword);
user.setRole(RoleType.USER);
return userEntity;
// userEntity는 db에 최종적으로 저장한 형태를 가져오는 것이 아닌 이전에 저장한 signupDto의 4가지 형태를 저장한 것을 반환
}
}
7. UserRepository
- DB와 직접 맞닿아 있는 인터페이스입니다.
- DB에 처리할 SQL문을 직접 사용할 수 있고 extends 하여 JpaRepository의 crud형태의 기본적인 연산이 지원됩니다.
package com.pjh.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
// DAO
// 자동으로 Bean 등록
public interface UserRepository extends JpaRepository<User, Integer>{
// table User을 관리하는 인터페이스, PK는 integer
User findByUsername(String username);
}
8. SecurityConfig.java
- spring security부분에 필요한 필수 요소입니다.
- SecurityConfig부분이 없다면 spring security에서 localhost:8080로 가려고 한다면 redirect를 localhost:8080/login으로
보냅니다. 이는 앞서 올린 3. 구상 도을 보시면 됩니다.
- csrf 토큰 : 비정상적인 접근에 대한 처리를 막기 위해 만든 spring security입니다.
http.csrf.disable() 추후 수정예정 -> 유효성 검사면에 안 좋음
package com.pjh.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity // 해당 파일로 시큐리티를 활성화
@Configuration // IoC
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public BCryptPasswordEncoder encode() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super() 삭제 -> 기존의 시큐리티가 가지고 있는 기능이 다 비활성화되어짐
// http.csrf().disable();
// csrf 비활성화
http.authorizeRequests()
.antMatchers("/","/main/**").permitAll()
// 조금있다가 바꿔줄 예정
//.antMatchers("/","/user/**","/image/**","./subscribe/**","/commit/**","/api/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/auth/login") // get 방식 활용시
.loginProcessingUrl("/auth/login") // post 방식 활용시 spring security가 자동으로 함, loginController를 따로 안만듦
.defaultSuccessUrl("/auth/main"); // 성공시에 /auth/main으로 이동
// 처음에 개발할때는 .antMatchers("/","/user/**...") 제외하기
// authenticated() -> 인증 필요
// anyRequest().permitAll() -> 인증이 필요없는 것은 모두 허용한다.
// and() -> 그리고
// formLogin().loginPage("/") -> 폼로그인 페이지인 "/~~"쪽으로 강제 이동
// 성공한다면 default로 "/"쪽으로 이동
}
}
9. PrincipalDetails.java
- spring security부분에 세션을 저장하는 방법입니다.
- 사용자가 로그인을 할 때의 controller에서 service부분에서 비즈니스 로직을 실행하지 않고 principalDetails의
세션을 사용하여 로그인을 하게 됩니다.
package com.pjh.config.auth;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.pjh.domain.user.User;
import lombok.Data;
@Data
public class PrincipalDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private User user;
public PrincipalDetails(User user) {
this.user=user;
// 세션 처리
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한을 가져오는 함수 -> user-role
// ex) 사람당 0~수많은 숫자의 권한을 다양하게 가질 수 있기 때문에 collection타입으로 리턴
Collection<GrantedAuthority> collector = new ArrayList<>();
collector.add(()-> {
return user.getRole();
});
return collector;
}
@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;
}
}
10. PrincipalDetailsService.java
- spring security부분에 세션을 저장하는 방법입니다.
package com.pjh.config.auth;
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 com.pjh.domain.user.User;
import com.pjh.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService{
private final UserRepository userRepository;
// 1. 패스워드는 알아서 스프링 시큐리티가 판단하기 때문에 판단을 안해도 됨.
// 2. 리턴이 잘되면 자동으로 UserDetails 타입을 세션을 만듦.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
//System.out.println("==============실행!============");
if(userEntity == null) {
return null;
}else {
// System.out.println("userEntity is null? : "+userEntity.getUsername());
return new PrincipalDetails(userEntity);
// 세션에 저장될때 userEntity의 정보를 가지고 있음.
}
}
}
'Springboot' 카테고리의 다른 글
스프링 이미지 업로드(2) (0) | 2023.11.07 |
---|---|
스프링 메모장 (1) | 2023.11.01 |
스프링 이미지 업로드(1) (1) | 2023.10.30 |
스프링 회원 수정 (1) | 2023.10.23 |
스프링 회원가입(1) (2) | 2023.10.20 |