본문 바로가기

개발/Java & Spring

[Spring] Spring security 를 사용해보자!- 사용자 추가부터 로그인까지! (Spring boot, Gradle, MyBatis, MySQL)-개발일기A[3]

 

Spring security의 존재도 모르고 

로그인을 어떻게 처리할까 암호화 API를 찾아가지고 ssl공부를 해야하나 어쩌고저쩌고 혼자 고민이 많았는데 ㅋㅋㅋ

하긴 이런게 없을리가 없지!

 

검색 해보면 자료는 많이 나오는데, 생각보다 MyBatis랑 같이 한 예제는 거의 없어서

그냥 레퍼런스 쭉 보고 따라하고 강의 쭉 보고 해보니까 생각보다 별거 아니잖아 ! 

삽질 하루만에 성공!

 

**Spring Boot 기반입니다**

 

참고 사이트는

https://spring.io/guides/gs/securing-web/  - spring security를 처음부터 꼼꼼하게 학습할 시간은 없고 일단 적용부터 해봐야겠다 하시는 분들은 여기 예제를 보고 한번 따라해보면 어떻게 흘러가는지 정도는 이해 가능할 것 같아요! 금방해요! (제가 이랬습니당)

 

그리고 정말 많은 도움이 되었던,

 

https://youtu.be/fG21HKnYt6g - 보니까 어떻게 내 프로젝트에 적용해야할지 슬슬 감이 잡혔다 ㅠㅠ 여행다녀오는 기차에서 딱 한시간 투자했는데(따라한 것도 아니고 그냥 집중해서 보기만했다) 집오니까 마법같이 모든게 해결되더라요 ,, 이 글을 보실지는 모르겠지만 감삼다! (아 추가로 이분 강의 정말 좋아욤! 레퍼런스 보는 법부터 시작해서 강의 주제뿐 아니라 꿀팁들 쏙쏙 얻어가는 느낌? 안지 얼마 안되었지만 앞으로 즐겨보게될듯!)

 

 

이제부터 차근차근 시작해볼게요! 

 

사담을 생략하고 싶으시다면 굵은글씨만!

 

 

 

저는 실전에 적용하면서 학습을 했기 때문에,

Login, Login 성공 시 이동할 jsp 파일이 있다고 가정하겠습니다.

 

 

 

1) DB 구성

 

User Table - 기본적인 인증에 필요한 속성만 담은 테이블 입니다. 이렇게 6개 (+뒤에 나올 authority)까지가 spring security에서 제공하는 검사? 점검? 타입입니다! 여기에 다른 속성을 추가하셔도 되는데, 저는 일단 spring-security 만 구현해보고 싶어서 여기에 따로 유저 정보를 담는 테이블을 구현해서 조인하는 방식으로 구현하고 있어요~

 

isCredentialNonExpired -> isCredentialsNonExpired 로 수정해주세요!

 

 

Authority Table - 각 유저별로 할당되는 권한을 보관할 Table입니다. 저 같은 경우는 엄청 큰 프로젝트가 아니기 때문에 한 사용자가 하나의 권한만 가지게 해도 무관하지만 보통은 한사람이 여러개의 권한을 가지는 것이 일반적이기 때문에 이렇게 테이블로 따로 보관할거예요! 나중에 조인을 하거나 해서 사용하겠죠?

 

일단 이렇게 두 테이블만 있으면 회원 추가, 로그인은 구현할 수 있을 것 같네요!

 

2) Spring-security 의존 추가 

 

gradle

 

 compile("org.springframework.boot:spring-boot-starter-security")

 

maven

 

<dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-security</artifactid>
 </dependency>

 

한 뒤, 프로젝트 오른쪽 클릭 -> Gradle STS Refresh ALL 클릭해주세요! 

그런 다음 톰캣 실행하고 해당 웹페이지를 들어가면 

 

 

이런 화면이 나왔다면 제대로 설정이 된겁니다! 당황하지 마시고 콘솔창을 보면 어떤 긴 패스워드 하나가 띄워져있습니다!

 

user: user/ password: 콘솔창의 패스워드

를 입력하면 자신이 이전에 설정해둔 / 경로의 페이지가 나오거나 없으면 404 에러가 뜨겠죵 

 

저 화면이 스프링 시큐리티에서 기본적으로 제공하는 로그인 뷰입니다. 저 뷰는 나중에 우리가 만든 뷰로 바꿔줄거예요!

일단 계속 진행합니다

 

3) User Table과 매핑되는 클래스와 매퍼를 생성해주세요! MyBatis 관련 게시글은 이전 게시글을 참고!

    아! 여기서 주의할점은 User라는 이름으로 제공되는 클래스가 있으니, User 대신에 다른 클래스명을 사용할 것! 일반적으로 Account를 사용한다고 한다

 

Account.java

import java.util.Collection;

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

import lombok.Data;

@Data
public class Account implements UserDetails{
	private String id;
	private String password;
	private boolean isAccountNonExpired; 
	private boolean isAccountNonLocked;
	private boolean isCredentialsNonExpired; 
	private boolean isEnabled;
	private Collection <extends GrantedAuthority> authorities;
	
	@Override
	public Collection <extends GrantedAuthority>  getAuthorities() {
		// TODO Auto-generated method stub
		return this.authorities;
	}
	@Override
	public String getPassword() {
		// TODO Auto-generated method stub
		return this.password;
	}
	@Override
	public String getUsername() {
		// TODO Auto-generated method stub
		return this.id;
	}
	@Override
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return this.isAccountNonExpired;
	}
	@Override
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return this.isAccountNonLocked;
	}
	@Override
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return this.isCredentialsNonExpired;
	}
	@Override
	public boolean isEnabled() {
		// TODO Auto-generated method stub
		return this.isEnabled;
	}

}

@Data 는 무시하시고 삭제해주세요

 

여기서 구현하고 있는 UserDetails interface를 기반으로 우리가 User Table을 생성한 것이다!

내 테이블은 이대로 적용했으니 따로 수정할 건 없겠고, 만약 테이블에 다른 속성을 추가했다면 함께 추가해야겠즹! 

나는 사용자 군이 세 분류로 나뉘기 때문에 이렇게 유저클래스 하나를 상속받는 세 하위 클래스를 만들었기 때문에 그럴 필요가 없었다

 

 

4) UserMapper 생성

 

AccountMapper.java

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.fitnessmanagement.user.Account;

@Mapper
public interface AccountMapper {
    @Select("SELECT * FROM USER WHERE id=#{id}")
    Account readAccount(String id);

    @Select("SELECT authority_name FROM AUTHORITY WHERE username=#{id}")
    List readAutorities(String id);

    @Insert("INSERT INTO USER VALUES(#{account.id},#{account.password},#{account.isAccountNonExpired},#{account.isAccountNonLocked},#{account.isCredentialsNonExpired},#{account.isEnabled})")
    void insertUser(@Param("account") Account account);

    @Insert("INSERT INTO AUTHORITY VALUES(#{id},#{autority})")
    void insertUserAutority(@Param("id") String id, @Param("autority") String autority);

    @Select("SELECT* FROM USER")
    List readAllUsers();
}

 

매퍼까지 설정했으니 우선 사용자를 추가해보겠다!

나중에 사용자 추가 폼에 적용해서 하겠지만, 우선 admin 계정이 필요하기 때문에 임의로 추가하도록 한다

 

5) AccountService.java 

 

 

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

import org.hibernate.annotations.common.util.impl.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;


@Service
public class AccountService implements UserDetailsService{
	
	
	@Autowired
	AccountRepository accounts;
	
	@Autowired
	PasswordEncoder passwordEncoder;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		Account account=accounts.findById(username);
		account.setAuthorities(getAuthorities(username));
		
		UserDetails userDetails=new UserDetails() {
			
			@Override
			public boolean isEnabled() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isCredentialsNonExpired() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isAccountNonLocked() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isAccountNonExpired() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public String getUsername() {
				// TODO Auto-generated method stub
				return account.getId();
			}
			
			@Override
			public String getPassword() {
				// TODO Auto-generated method stub
				return account.getPassword();
			}
			
			@Override
			public Collection getAuthorities() {
				// TODO Auto-generated method stub
				
				return account.getAuthorities();
			}
		};
		return account;
	}
	public Account save(Account account,String role) {
		// TODO Auto-generated method stub

		account.setPassword(passwordEncoder.encode(account.getPassword()));
		account.setAccountNonExpired(true);
		account.setAccountNonLocked(true);
		account.setCredentialsNonExpired(true);
		account.setEnabled(true);
		return accounts.save(account, role);
	}

}

우선 사용자 추가가 목적이니까 save만 확인한다!

repository는 사용자를 저장해둘 목적으로 구현할 것이고,

 

PasswordEncoder는 말 그대로 암호를 암호화해주는 spring security에서 제공해주는 인터페이스이다.

정말 고마운 인터페이스 아닌가!? 

 

이걸 구현을 안해주고  평문 암호로 사용자를 추가한 뒤 로그인을 하면 널 에러가 뜨는데 물론 따로 설정을 해주면 평문으로도 로그인이 가능하기는 하지만

암호화를 해주는게 맞는거니까 구현을 해주도록 한다

 

@Autowired 처리를 해줬으니 빈으로 만들어주어야겠지!

 

스프링 시큐리티의 가장 중요한 설정파일을 만들어 준다

 

6) SecurityConfig.java

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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/login","/service","/resources/**","/create").permitAll()
                .antMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
           .formLogin()
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
	
	@Bean
	public PasswordEncoder passwordEncoder(){
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
}

첫 번째 메소드가 중요한 메소드이다.

 

첫번째 authorizeRequest() 아래는 각 경로에 따른 권한을 지정할 수 있다.

내 프로젝트 경로 위주이고, 여기서 아마 동일하게 할 것은

/login, /, /resources/** 이지 않을까 싶다!

 

로그인 권한은 누구나 가져야하고, /resources/** 는 css, js 등 뷰 구현과 관련된 파일의 권한도 풀어줘야 하기 때문에 permitAll() 처리를 해준다. 나머지는 각자의 프로젝트에 맞게~

 

두 번째, hasRole은 말 그대로 괄호 안의 권한을 가진 유저만 해당 경로에 접근할 수 있도록 설정하는 것이다.

여기서 자동으로 앞에 "ROLE_"이 삽입된다는 것을 기억해야 한다.

 

즉, Authority Table에 사용자의 권한을 삽입할 때 "ROLE_권한명" 형식으로 삽입해야한다는 것!

 

그리고 formLogin() 아래는 .loginPage(), .defaultSuccessPage() 등으로 내가 직접 구현한 로그인 폼, 로그인 성공 시 이동할 경로를 설정할 수 있다. 이때 로그인 폼의 아이디, 패스워드 부분 아이디는 username, password로 일치시켜주어야 한다.

 

 

아래 PasswordEncoder 빈을 생성하는 코드는 그냥 가져다 쓰기로 한다.

(일반적인 인코딩 형식인  BCryptPasswordEncoder 을 반환해준다) 

 

7) 사용자를 저장할 repository 를 생성한다! 여기서 생성도하고 나중에 불러오기도 할 예정!

AccountRepository.java

 

 

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class AccountRepository {
	
	@Autowired
	AccountMapper accountMapper;
	
	
	public Account save(Account account,String role){
		accountMapper.insertUser(account);
		accountMapper.insertUserAutority(account.getId(), role);
		return account;
	}

	public Account findById(String username) {
		// TODO Auto-generated method stub
		return accountMapper.readAccount(username);
	}
	
	public List<string>findauthoritiesbyid(string username){return=accountmapper.readautorities(username);}

 

아래 두 메소드는 일단 패스하고, save 메소드에만 집중하도록 한다.

사용자 계정과 권한을 인자로 받아 DB에 넣어줄 것이고, 위에서 작성한 매퍼와 비교하면 이해할 수 있다.

 

8) 이제 사용자를 추가할 경로를 추가해준다!

 

AccountController.java

 

 

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {
	
	@Autowired
	AccountService accountService;

	@Autowired
	AccountMapper accountMapper;
	
	
	//ADMIN 계정 부여
	@GetMapping("/create")
	public Account create(){
		Account account=new Account();
		account.setId("admin");
		account.setPassword("1234");
		accountService.save(account, "ROLE_ADMIN");
		return account;
	}
	
	//서비스 권한 부여


}

/create 경로를 통해 admin 계정을 하나 추가할 계획이다! 

이건 서비스를 제공하는 내가 사용할 계정이고, 이 계정으로 나는 내 서비스에 접근할 사용자를 추가해줄 건데 아직 안함 ㅋ_ㅋ 

 

집중할 부분은 "ROLE_ADMIN" ! 

앞서 말했던 ROLE_ 형식이 바로 이걸 말한거다!

 

/create에 접속하면

 

이렇게 암호화된 패스워드가 자동으로 삽입된다! 

authorities가 null로 되어있는데 내가 account리턴 때 권한을 넣고 리턴한게 아니라서 그런거지 디비엔 제대로 들어가있음다!  

 

이렇게 사용자 추가는 완료! 

 

 

이제 여기서 생성한 내 어드민 계정으로 로그인을 하기 전에! 

 

9) AccountService.java를 다시 확인해보자!

 

 

 

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		Account account=accounts.findById(username);
		account.setAuthorities(getAuthorities(username));
		
		UserDetails userDetails=new UserDetails() {
			
			@Override
			public boolean isEnabled() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isCredentialsNonExpired() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isAccountNonLocked() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public boolean isAccountNonExpired() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public String getUsername() {
				// TODO Auto-generated method stub
				return account.getId();
			}
			
			@Override
			public String getPassword() {
				// TODO Auto-generated method stub
				return account.getPassword();
			}
			
			@Override
			public Collection <? extends GrantedAuthority> getAuthorities() {
			// TODO Auto-generated method stub
				
				return account.getAuthorities();
			}
		};
		return account;
	}

}
	public Collection<GrantedAuthority> getAuthorities(String username) 
	{ 
		List<String> string_authorities = accounts.findAuthoritiesByID(username);
		List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 
		for (String authority : string_authorities) 
		{ 
			authorities.add(new SimpleGrantedAuthority(authority)); 
		} 
		return authorities; 
	}

 

사용자가 로그인(인증)을 시도할 때 거치는 절차이다.

username(id)를 통해 사용자를 얻어온 후 패스워드가 일치하면 여기서 반환하는 Account가 가지고 있는 권한을 부여해주는 것이다.

내가 구현한 Account는 UserDetails와 똑같아서 사실 저렇게 할 필요는 없겠지만 일단 저렇게 했다! 

return true; 값은 테스팅 용으로 저렇게 한건데 실제로는 account의 값을 리턴해줘야겠지! 

 

그리고 밑에 getAutorities 함수를 추가해줍니당 

 

 

이제 이 상태로 다시 /login 경로에 접근하면 로그인이 됩니당당

 

-끗-

 

이 상태로 특정 경로에 admin 권한을 가진 사람만 접근할 수 있도록 설정해준 뒤, admin 권한이 아닌 다른 권한을 가진 유저를 생성하고 로그인 한 뒤 admin 경로에 접근하면,

접근이 안되는걸 확인할 수 있다!