각진 세상에 둥근 춤을 추자

[Spring Boot] Spring Security 회원 등록 (인증, 인가) + BCryptPasswordEncoder 본문

Spring

[Spring Boot] Spring Security 회원 등록 (인증, 인가) + BCryptPasswordEncoder

circle.j 2023. 1. 13. 19:34

이전 글에 이어서 

[Spring Boot] Spring Security 처리 과정 + 로그인 예제 (인증, 인가)

 

[Spring Boot] Spring Security 처리 과정 + 로그인 예제 (인증, 인가)

인증(Authentication)과 인가(Authorization) 인증(Authentication): 해당 사용자가 본인이 맞는지 확인 인가(Authorization): 해당 사용자가 요청하는 자원을 실행할 수 있는 권한이 있는가 확인 Principal(접근 주체

this-circle-jeong.tistory.com

 

이전 게시글에 이어 회원 등록 기능을 추가함과 동시에 코드를 좀더 편리한 방식으로 수정해 본다. 

 


1. Register 페이지 생성

 

프로젝트 - src/main/resources - templates - user2 -  register.html 생성

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="UTF-8">
		<title>user2::register</title>
	</head>
	<body>
		<h3>user2 회원가입</h3>
		<a th:href="@{/}">메인이동</a>
		<a th:href="@{/user2/login}">로그인</a>
		<form th:action="@{/user2/register}" method="post">
			<table border="1">
				<tr>
					<td>아이디</td>
					<td><input type="text" name="uid"/></td>
				</tr>
				<tr>
					<td>비밀번호</td>
					<td><input type="password" name="pass"/></td>
				</tr>
				<tr>
					<td>이름</td>
					<td><input type="text" name="name"/></td>
				</tr>
				<tr>
					<td>휴대폰</td>
					<td><input type="text" name="hp"/></td>
				</tr>
				<tr>
					<td>나이</td>
					<td><input type="number" name="age"/></td>
				</tr>
				<tr>
					<td colspan="2" align="right"><input type="submit" value="회원가입"/></td>
				</tr>
			</table>
		</form>
		
	</body>
</html>

 


2. VO

 

프로젝트 - src/main/java - kr.co.user - vo - User2VO.java

DB의 칼럼(pass, grade, rdate) 을 추가한 후, VO에서 추가한 칼럼을 생성한다.

 

package kr.co.ch08.vo;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.CreationTimestamp;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Table(name = "user2")
public class User2VO {
	@Id
	private String uid;
	private String pass;
	private String name;
	private int grade;
	private String hp;
	private int age;
	
	@CreationTimestamp
	private LocalDateTime rdate;
}

날짜는 기존의 sql문 NOW()가 아닌 LocalDateTime을 이용해 선언한다. 

 


3. User2Controller

 

프로젝트 - src/main/java - kr.co.user - controller  - User2Controller.java 

GetMapping, PostMapping한 register 페이지에 대한 내용을 덧붙인다. 

package kr.co.ch08.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import kr.co.ch08.service.User2Service;
import kr.co.ch08.vo.User2VO;

@Controller
public class User2Controller {
	
	@Autowired
	private User2Service service;

	@GetMapping("/user2/login")
	public String login() {
		return "/user2/login";
	}

	@GetMapping("/user2/loginSuccess")
	public String loginSuccess() {
		return "/user2/loginSuccess";
	}
	
	@GetMapping("/user2/register")
	public void register() {
		
	}
	
	@PostMapping("/user2/register")
	public String register(User2VO vo) {
		service.insertUser(vo);
		return "redirect:/user2/login";
	}
	
	
}

 

 


4. User2Service

 

앞서 User2Controller에서 PostMapping 내용에서 작성한 service인 insertUser를 작성한다.

이때 repository를 통해 해당 내용을 저장하기 전, BCryptPasswordEncoder를 통해 작성한 비밀번호를 암호화해 준다.

 

프로젝트 - src/main/java - kr.co.user - service 패키지 - User2Service.java 

package kr.co.ch08.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import kr.co.ch08.repository.User2Repo;
import kr.co.ch08.vo.User2VO;

@Service
public class User2Service {

	@Autowired
	private User2Repo repo;
	
	public void insertUser(User2VO vo) {
		
		// BcryptPassword = SHA2 + Salted (강력 암호화)
		BCryptPasswordEncoder passEncoder = new BCryptPasswordEncoder();
		vo.setPass(passEncoder.encode(vo.getPass()));
		
		repo.save(vo);
	}
	
	public User2VO selectUser2(String uid, String pass) {
		return repo.findUser2VOByUidAndPass(uid, pass);
	}
	
}

 

 


5. Security 

 

프로젝트 - src/main/java - kr.co.user - security 패키지 생성 - SecurityConfig.java

비로그인 시 로그인 성공 화면 접근 불가능 기능 + 로그인 인증처리 서비스 등록 (BcryptPassword)

package kr.co.ch08.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		
		// 인가(접근권한) 설정 (index : 모든 링크(사용자)에 대해 허용을 해 준 상태, 권한관리필터)
		http.authorizeHttpRequests().antMatchers("/").permitAll();
		// admin 하위의 모든 자원 -> "ADMIN"에게 부여
		http.authorizeHttpRequests().antMatchers("/admin/**").hasRole("ADMIN");
		// member 하위의 모든 자원 -> "ADMIN", "MEMBER" 에게 부여
		http.authorizeHttpRequests().antMatchers("/member/**").hasAnyRole("ADMIN", "MEMBER");
		// GUEST는 무권한 -> 생략
		// loginSuccess 접근 -> "ADMIN"만 접근 허용
		http.authorizeHttpRequests().antMatchers("/user2/loginSuccess").hasAnyRole("3", "4", "5");
		
		// 사이트 위변조 요청 방지
		http.csrf().disable();
		
		// 로그인 설정
		http.formLogin()
		.loginPage("/user2/login")
		.defaultSuccessUrl("/user2/loginSuccess")
		.failureUrl("/user2/login?success=100")
		.usernameParameter("uid")
		.passwordParameter("pass");
		
		// 로그아웃 설정
		http.logout()
		.invalidateHttpSession(true)
		.logoutRequestMatcher(new AntPathRequestMatcher("/user2/logout"))
		.logoutSuccessUrl("/user2/login?success=200");
	}
	
	@Autowired
	private SecurityUserService service;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		
		// 로그인 인증처리 서비스 등록 (기본 SHA2)
		//auth.userDetailsService(service).passwordEncoder(new MessageDigestPasswordEncoder("SHA-256"));
		
		// 로그인 인증처리 서비스 등록 (BcryptPassword)
		auth.userDetailsService(service).passwordEncoder(new BCryptPasswordEncoder());
	}
	
}

 

 

프로젝트 - src/main/java - kr.co.user - security 패키지  - SecurityUserService.java

[기존] 

// Security 기본 사용자 객체 생성 : 인증 거치는 과정 (사용자가 폼에 입력한 정보가 맞는지 확인) -> userDts를 세션에 등록
UserDetails userDts = User.builder()
                          .username(user.getUid())
                          .password(user.getPass())
                          .roles("MEMBER")	//마지막에 권한 주기
                          .build();
return userDts;
*/

기존의 방식으로 userDts를 사용하게 되면 userDts에 저장된 정보는 아이디와 비밀번호로 한정되어, 만약 다른 페이지에서 다른 칼럼(이름, 번호 등)을 출력할 수 없다. 

이를 변경하기 위해 userDts를 커스텀할 필요가 있다. 

 

[변경]

// MyUserDetails -> @Builder : Builder 방식으로 생성 (build 초기화, build 패턴)
UserDetails myUser = MyUserDetails.builder()
                    .uid(user.getUid())
                    .pass(user.getPass())
                    .name(user.getName())
                    .grade(user.getGrade())
                    .hp(user.getHp())
                    .age(user.getAge())
                    .rdate(user.getRdate())
                    .build();

return myUser;

 

package kr.co.ch08.security;

import org.springframework.beans.factory.annotation.Autowired;
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.Service;

import kr.co.ch08.repository.User2Repo;
import kr.co.ch08.vo.User2VO;

@Service
public class SecurityUserService implements UserDetailsService{

	@Autowired
	private User2Repo repo;
	
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		// 스프링 시큐리티 인증 동작방식은 아이디/패스워드를 한 번에 조회하는 방식이 아닌 
		// 아이디만 이용해서 사용자 정보를 로딩하고 이후 패스워드를 검증하는 방식이다.
		
		// security: 사용자 계정 먼저 확인 (username)
		User2VO user = repo.findById(username).get();
		
		if(user ==  null) {
			throw new UsernameNotFoundException(username);
		}
		
		/*
		// Security 기본 사용자 객체 생성 : 인증 거치는 과정 (사용자가 폼에 입력한 정보가 맞는지 확인) -> userDts를 세션에 등록
		UserDetails userDts = User.builder()
                                          .username(user.getUid())
                                          .password(user.getPass())
                                          .roles("MEMBER")	//마지막에 권한 주기
                                          .build();
		return userDts;
		*/
		
		// MyUserDetails -> @Builder : Builder 방식으로 생성 (build 초기화, build 패턴)
		UserDetails myUser = MyUserDetails.builder()
                                                .uid(user.getUid())
                                                .pass(user.getPass())
                                                .name(user.getName())
                                                .grade(user.getGrade())
                                                .hp(user.getHp())
                                                .age(user.getAge())
                                                .rdate(user.getRdate())
                                                .build();

		return myUser;
	}
	
}

 

프로젝트 - src/main/java - kr.co.user - security 패키지  - MyUserDetails.java 생성

(UserDetails 커스텀)

package kr.co.ch08.security;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

//userDts는 아이디와 패스워드만 포함 -> 이름, 번호 등 다른 페이지에 출력하기 위해서는 커스텀 필요 -> MyUserDetails
// 로그인하면 MyUserDetails 생성
@Setter
@Getter
@Builder
public class MyUserDetails implements UserDetails{

	private static final long serialVersionUID = 1L;
	
	// 필드 정의
	private String uid;
	private String pass;
	private String name;
	private int grade;
	private String hp;
	private int age;
	private LocalDateTime rdate;

	public void roles(String... grade) {
		
	}
	
	// 사용자 권한 목록 - 로그인한 계정이 갖는 권한 목록 
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<GrantedAuthority> authorities = new ArrayList<>();
		authorities.add(new SimpleGrantedAuthority("ROLE_"+grade));
		return authorities;
	}

	// 계정이 갖는 비밀번호
	@Override
	public String getPassword() {
		return pass;
	}

	// 계정이 갖는 아이디
	@Override
	public String getUsername() {
		return uid;
	}

	// 계정 만료 여부 
	// true - 만료 안됨, false - 만료(로그인 불가)
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 계정 잠김 여부 
	// true - 잠김 안됨, false - 잠김(로그인 불가)
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	// 계정 비밀번호 만료 여부
	// true - 만료 안됨, false - 만료(로그인 불가)
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	// 계정 활성화 여부
	// true - 활성화, false - 비활성화(로그인 불가)
	@Override
	public boolean isEnabled() {
		return true;
	}

}

 


6. loginSuccess.html

 

마지막으로 로그인 완료 시 페이지를 앞서 작성한 코딩에 맞게 수정한다. 

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
	  
	<head>
		<meta charset="UTF-8">
		<title>user2::loginSuccess</title>
	</head>
	<body>
		<h3>user2 loginSuccess</h3>
		
		<!-- isAuthenticated(): 로그인 했을 경우 출력 (만약 로그인 안 하고 페이지 강제 이동 시 에러) -->
		<p sec:authorize="isAuthenticated()">
			아이디 : <span>[[${#authentication.principal.uid}]]</span><br>
			이름 : <span>[[${#authentication.principal.name}]]</span><br>
			휴대폰 : <span sec:authentication="principal.hp">휴대폰번호</span><br>
			나이 : <span sec:authentication="principal.age">나이</span><br>
			날짜 : <span sec:authentication="principal.rdate">가입일</span><br>
			<a th:href="@{/user2/logout}">로그아웃</a>
		</p>
		
		<!-- 로그인을 안 했을 경우 출력 -->
		<p sec:authorize="isAnonymous()">
			로그인을 하셔야 합니다.<br><br>
			<a th:href="@{/user2/login}">로그인</a>
		</p>
		
		
	</body>
</html>

 

 


7. 실행

 

비밀번호는 전부 1234이다. 

비암호화 비밀번호   SHA2 암호화 비밀번호   BCrypt 암호화 비밀번호 

 

우선 임의로 grade에 숫자를 작성한 후, grade가 입력된 아이디로 로그인 실행한다. 

 

http://localhost:8080/Ch08/user2/register

 

http://localhost:8080/Ch08/user2/login