Spring

[SpringBoot] Security - 회원가입 DB 인증

shb 2022. 5. 16. 16:37

회원 가입하기
회원가입 → DB저장 → 인증
스프링 시큐리티에선 
1. 하나의 계정에 복수개의 권한 부여 가능. 즉! 계정:권한 => 1:N 관계
2. DB 저장시 password 는 반드시 ‘암호화’ 해서 저장되어야 한다
a. 암호화는 스프링 시큐리티의 PasswordEncoder 사용
3. 인증을 위해 DB 조회시 필요한 ‘동작’들이 제공되어야 한다.

 

* DB 테이블 작성 
[회원계정] 1:n [권한]
ERD 폴더와 sql 생성

* UserDTO 생성

 

* DummyData 만들기

 

* 로그인 폼에 회원가입 폼 링크추가
loginForm.jsp

 

* /join 핸들러 추가
IndexController.java

    @GetMapping("/join")
    public String join() {
        return "joinForm";
    }

회원가입 폼 작성
{view_root}/joinForm.jsp  생성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr>
<form action="/joinOk" method="POST">
    <input type="text" name="id" placeholder="아이디 입력"/><br>
    <input type="password" name="pw" placeholder="패스워드 입력"/><br>
    <input type="email" name="email" placeholder="이메일 입력"/><br>
    <input type="submit" value="회원가입">
</form>
</body>
</html>

* /joinOk 핸들러 추가
IndexController.java

    @PostMapping("/joinOk")
    public String joinOk(UserDTO user) {
        System.out.println("/joinOk: " + user);

        // password 는 암호화 하여 저장
        String rawPassword = user.getPw();
        String encPassword = passwordEncoder.encode(rawPassword);
        user.setPw(encPassword);
        
        userService.addMember(user);
        
        return "redirect:/login";
    }

* UserDAO.java 작성
{base-pacakge}/domain
인터페이스 작성

package com.lec.spring.domain;

import java.util.List;

public interface UserDAO {
    // 사용자(user) 추가
	int addUser(UserDTO user);
	  
    // 특정 id(username)의 사용자 에 권한(auth) 추가
	int addAuth(String id, String auth);
   
    // 사용자(user) 삭제
	int deleteUser(UserDTO user);
   
    // 특정 id(username)의 사용자의 특정 권한(auth) 삭제
	int deleteAuth(String id, String auth);
   
    // 특정 id(username)의 사용자의 권한(들) 전부 삭제
	int deleteAuths(String id);
   
    // 특정 id(username)의 사용자 조회
	UserDTO findById(String id);
   
    // 특정 id(username)의 사용자의 권한(들) 뽑기
	List<String> selectAuthoritiesById(String id);
	
}

* UserDAO.xml 
MyBatis 매퍼 파일
{base-pacakge}/domain

<?xml version="1.0" encoding="UTF-8"?>
http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lec.spring.domain.UserDAO">

	<insert id="addUser" parameterType="com.lec.spring.domain.UserDTO"
		flushCache="true">
		INSERT INTO test_member2(mb_id, mb_pw, mb_email)
		VALUES(#{id}, #{pw}, #{email})
	</insert>
	
	<insert id="addAuth" flushCache="true">
		INSERT INTO test_authority2
		VALUES(#{param1}, #{param2})
	</insert>
	
	<delete id="deleteUser" parameterType="com.lec.spring.domain.UserDTO"
		flushCache="true">
		DELETE FROM test_member2
		WHERE mb_id = #{id}
	</delete>
	
	<delete id="deleteAuth" flushCache="true">
		DELETE FROM test_authority2
		WHERE mb_id = #{param1} AND mb_auth = #{param2}
	</delete>
	
	<delete id="deleteAuths" flushCache="true">
		DELETE FROM test_authority2
		WHERE md_id = #{param1}
	</delete>
	
	<select id="findById" resultType="com.lec.spring.domain.UserDTO">
	   SELECT
            mb_uid uid,
            mb_id id,
            mb_pw pw,
            mb_email email,
            mb_enabled enabled,
            mb_regdate regdate
        FROM test_member2
        WHERE mb_id = #{id} 
	</select>
	
	<select id="selectAuthoritiesById" resultType="String">
		SELECT mb_auth
		FROM test_authority2
		WHERE mb_id = #{id}		
	</select>
	
</mapper>

* UserDAOImpl.java 작성
{base-pacakge}/domain

package com.lec.spring.domain;

import java.util.List;

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserDAOImpl implements UserDAO {

	private UserDAO mapper;
	
	@Autowired
	public UserDAOImpl(SqlSession sqlSession) {
		mapper = sqlSession.getMapper(UserDAO.class);
	}
	
	
	@Override
	public int addUser(UserDTO user) {
		return mapper.addUser(user);
	}

	@Override
	public int addAuth(String id, String auth) {
		return mapper.addAuth(id, auth);
	}

	@Override
	public int deleteUser(UserDTO user) {
		return mapper.deleteUser(user);
	}

	@Override
	public int deleteAuth(String id, String auth) {
		return mapper.deleteAuth(id, auth);
	}

	@Override
	public int deleteAuths(String id) {
		return mapper.deleteAuths(id);
	}

	@Override
	public UserDTO findById(String id) {
		return mapper.findById(id);
	}

	@Override
	public List<String> selectAuthoritiesById(String id) {
		return mapper.selectAuthoritiesById(id);
	}

}

* UserService.java 작성
{base-pacakge}/service

package com.lec.spring.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.lec.spring.domain.UserDAO;
import com.lec.spring.domain.UserDTO;

@Service
public class UserService {

	@Autowired
	UserDAO dao;
	
    // 회원가입
    // ROLE_MEMBER 권한 부여
	@Transactional
	public int addMember(UserDTO user) {
		int cnt = dao.addUser(user);
		dao.addAuth(user.getId(), "ROLE_MEMBER");
		return cnt;
	}

    // 회원삭제
	@Transactional
	public int deleteMember(UserDTO user) {
		dao.deleteAuths(user.getId());   // 권한(들) 먼저 삭제  (차라리 DDL 에 ON DELETE CASCADE 하자..)
		int cnt = dao.deleteUser(user);
		return cnt;
	}

    // 특정 id(username) 의 정보 가져오기
	public UserDTO findById(String id) {
		return dao.findById(id);
	}

    // 특정 id 의 권한(들) 정보 가져오기
	public List<String> selectAuthoritiesById(String id){
		return dao.selectAuthoritiesById(id);
	}
	
}

* PasswordEncoder 
스프링 시큐리티는 기본적으로 PasswordEncoder 객체를 통해 ‘password’ 는 반.드.시  암호화 하여 다루도록 되어 있다.

public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
    // PasswordEncoder 를 bean 으로 IoC 에 등록
    // IoC 에 등록된다, IoC 내에선 어디서든 가져다가 사용할수 있다.
	@Bean
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}

* IndexController 수정

@Controller
public class IndexController {
	
	@Autowired
	UserService userService;
	
	@Autowired
	private PasswordEncoder passwordEncoder;

* 로그인 진행, DB를 통한 인증

UserDetails 작성 
{bast-package}/config/PrincipalDetails.java

package com.lec.spring.config;

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

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

import com.lec.spring.domain.UserDTO;
import com.lec.spring.service.UserService;

//시큐리티가 /loginOk 주소요청이 오면 낚아채서 로그인을 진행시킨다.
//로그인 진행이 완료되면 '시큐리티 session' 에 넣어주게 된다. 
//우리가 익히 알고 있는 같은 session 공간이긴 한데..
//시큐리티가 자신이 사용하기 위한 공간을 가집니다. 
//=> Security ContextHolder 라는 키값에다가 session 정보를 저장합니다.
//여기에 들어갈수 있는 객체는 Authentication 객체이어야 한다.
//Authentication 안에 User 정보가 있어야 됨. 
//User 정보 객체는 ==> UserDetails 타입 객체이어야 한다.

//따라서 로그인한 User 정보를 꺼내려면
//Security Session 에서 
// => Authentication 객체를 꺼내고, 그 안에서
//      => UserDetails 정보를 꺼내면 된다.
public class PrincipalDetails implements UserDetails {

	private UserService userService;
	
	public void setUserService(UserService userService) {
		this.userService = userService;
	}
	
	private UserDTO user;
	public UserDTO getUser() {
		return user;
	}
	
	
	public PrincipalDetails(UserDTO user) {
		System.out.println("PrincipalDetails(user) 생성: " + user);
		this.user = user;
	}
	

    // 해당 User 의 '권한(들)'을 리턴
    // 현재 로그인한 사용자의 권한정보가 필요할때마다 호출된다. 혹은 필요할때마다 직접 호출해 사용할수도 있다
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		System.out.println("getAuthorities() 호출");
		Collection<GrantedAuthority> collect = new ArrayList<>();
		
		List<String> list = userService.selectAuthoritiesById(user.getId());  // DB 에서 권한들 읽어옴.
		
		for(String auth : list) {
			collect.add(new GrantedAuthority() {
				
				@Override
				public String getAuthority() {					
					return auth;
				}
				
				@Override
				public String toString() {				
					return auth;
				}
			});
		}
	
		
		return collect;
	}

	// 로그인 한 사용자의 password 는?
	@Override
	public String getPassword() {		
		return user.getPw();
	}

	// 로그인 한 사용자의 username 은?
	@Override
	public String getUsername() {
		return user.getId();
	}

	// 계정이 만료된건 아닌지?
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 계정이 잠긴건 아니지?
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	// 계정 credential 이 만료된건 아니지?
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	// 활성화 되었니?
	@Override
	public boolean isEnabled() {
		if("1".equals(user.getEnabled())) return true;
		return false;
	}

}


* UserDetailsService 생성
{base-package}/config/PrincipalDetailsService.java

package com.lec.spring.config;

import org.springframework.beans.factory.annotation.Autowired;
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.lec.spring.domain.UserDTO;
import com.lec.spring.service.UserService;

//UserDetailsService
//컨테이너에 등록한다.
//시큐리티 설정에서 loginProcessingUrl(url) 로 걸어 놓았기 때문에
//로그인시 위 url 로 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어 있는
//loadUserByUsername() 가 실행되고
//인증성공하면 결과를 UserDetails 로 리턴
@Service
public class PrincipalDetailsService implements UserDetailsService {

	@Autowired
	private UserService userService;
	
	
    // UserDetails 를 리턴한다,  --> 누구한테 리턴하나?
    // 시큐리티 session ( <= Authentication( <= 리턴된 UserDetails ) )a
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		System.out.println("loadUserByUsername(" + username + ")");
		
		UserDTO user = userService.findById(username);  // DB 조회 
		
		// 해당 username 의 user 가 DB 에 있었다면
		// UserDetails 을 생성하여 리턴
		if(user != null) {
			PrincipalDetails details = new PrincipalDetails(user);
			details.setUserService(userService);
			return details;
		}
		
        // 해당 username 의 user 가 없다면!
        // UsernameNotFoundException을 throw 해주도록 한다
		throw new UsernameNotFoundException(username);
	}

}

sesstion attribute 에 SPRING_SECURITY_CONTEXT 라는 이름으로 현재 로그인 정보가 담겨 있다.
그리고 그 정보는 Authentication 객체다.

'Spring' 카테고리의 다른 글

[SpringBoot] Security  (0) 2022.05.16
[SpringBoot] MyBatis  (0) 2022.05.13
[Spring Boot] Validation  (0) 2022.05.12
[SpringBoot] Request Parameter  (0) 2022.05.12
[SpringBoot] RequestMapping  (0) 2022.05.12