스프링/시큐리티

스프링부트 시큐리티 02 - 시큐리티 기본(회원가입/로그인/권한처리)

H-V 2021. 9. 7. 22:21

유투버 '데어프로그래밍'님 강의 참조

 

01 - 로그인 페이지 설정

 

1) IndexController에 @Responsebody를 다 지운 후 템플릿 세팅

	@GetMapping("/loginForm")
	public String loginForm() {
		return "loginForm";
	}
  • loginForm.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
	<h1>Login Page</h1>
	<hr>
	<form>
		<input type="text" name="username" placeholder="Username" /><br /> <input
			type="password" name="password" placeholder="Password" /><br />
		<button>Login</button>
	</form>
</body>
</html>

굿!

 

 

02 - 로그인 진행을 위한 모델 세팅 및 DB테스트

 

  • 모델 세팅

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	private String username;
	private String password;
	private String email;
	private String role;
	private String provider;
	private String providerId;
	@CreationTimestamp
	private Timestamp createDate;
}

 

 

  • 로그인 진행을 위한 회원가입 세팅
	@GetMapping("/joinForm")
	public String joinForm() {
		return "joinForm";
	}
  • loginForm.html 에 회원가입 링크 추가
<a href="/joinForm">Not a member yet? Sign up!</a>

  • JoinForm 추가
<body>
<h1>Sign-Up Page</h1>
<hr>
<form action="/join" method="post">
	<input type="text" name="username" placeholder="Username" /><br /> 
	<input type="password" name="password" placeholder="Password" /><br />
	<input type="email" name="email" placeholder="Email" /><br />
	<button>Sign-up</button>
</form>
</body>
  • 3개의 값을 들고 컨트롤러에서 회원가입 처리
	@PostMapping("/join")
	public @ResponseBody String join(User user) {
		System.out.println(user);
		return "joined Successfully!";
	}

굿!

 

 

  • DB에 넣기위한 Repository 설정
public interface UserRepository extends JpaRepository<User, Integer>{}

→ JpaRepository 가 기본 CRUD 기능을 들고 있고 상속으로 어노테이션 생략 가능

 

  • 패스워드 암호화를 꼭 해야만 시큐리티에서 넘어간다!
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}
    ...

→ @Bean을 통해 메서드의 리턴되는 오브젝트를 IoC로 등록하게 됨

 

  • IndexController
@Controller
public class IndexController {
	
	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder;
    
    ..
    
	@PostMapping("/join")
	public @ResponseBody String join(User user) {
		System.out.println(user);
		user.setRole("ROLE_USER");
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword);
		user.setPassword(encPassword);
		userRepository.save(user);
		return "redirect:/loginForm";
	}
}

 

→ 테스트!

 

 

 

 

  • 로그인 해보기

  • SecurityConfig에 코드 추가해 시큐리티가 로그인을 낚아채서 진행하도록 설정
    즉 컨트롤러에 로그인 함수를 안만들어도 됨
.loginProcessingUrl("/login")
.defaultSuccessUrl("/");
  • loginForm에서 로그인 처리 되도록 POST형식 추가
<form action="/login" method="post">
  • 시큐리티에서 로그인을 낚아채서 자기가 하도록 클래스를 따로 설정 → PrincipalDetails
public class PrincipalDetails implements UserDetails {

	private User user;
	
	public PrincipalDetails(User user) {
		this.user = user;
	}
	
	//해당 User 권한 리턴
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> collect = new ArrayList<GrantedAuthority>();
		collect.add(new GrantedAuthority() {
			@Override
			public String getAuthority() {
				return user.getRole();
			}
		});
		return collect;
	}

	
	@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;
	}
}
  • 시큐리티가 로그인 주소 요청이오면 낚아채는데 로그인 진행이 정상적으로 진행되면 시큐리티만의 세션이 생김
    (Security ContextHolder)
  • 이때 세션에 들어 갈 수 있는 오브젝트 정보는 'Authentication' 타입이어야 함
  • Authentication안에는 User정보가 있어야하는데 User정보를 들고있는 객체의 타입은 반드시 'UserDetails' 타입의 객체이어야 함. 그래서 Implements를 통해 PrincipalDetails(UserDetails)가 되는 것
  • 그래서 User 객체를 꺼낼때 PrincipalDetails에 접근해 가능 한 것. 
  • 나머지 코드는 스프링에서 정해논것이니 따라치기만 하면 되고, 해당 유저 권한 리턴시에는 컬렉션으로 만들어놨고 현재 권한은 String 타입이므로 타입을 맞추기위한 코드라고 보면 됨
  • 계정에 대한 유효성은 휴먼계정을 만든다던지 할때 False로 바꿔서 사용

 

  • 정리하면 Security Session에 정보를 저장할때 정보는 반드시 Authentication 객체여야하고 이 객체안에 User정보를 저장할때는 반드시 UserDetails 타입이어야 한다는 것!
    즉 현재 Authentication에 넣을 유저 객체를 만든 것

 

 

 

  • 이제는 Authentication 객체를 만들어서 세션에 넣을 수 있도록 만들어야 한다 → PrincipalDetailsSerivce
@Service
public class PrincipalDetailsSerivce implements UserDetailsService{

	@Autowired
	private UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User userEntity = userRepository.findByUsername(username);
		if(userEntity != null) {
			return new PrincipalDetails(userEntity);
		}
		return null;
	}
}
  • 로그인 요청이 오면 스프링이 데이터로 들어온 username을 들고 UserDetailsService 타입으로 IoC되어있는 loadUserByUsername함수를 실행 함 → 이때 username이 파라미터로 들어감
  • 이후 레파지토리를 통해서 username 체크를 하게되고 정상이면 PrincipalDetails(UserDetails 타입)에 user정보를 넣어서 리턴하게 됨

 

  • 즉 지금까지 프로세스는 Authentication 객체를 만들어 사용하기 위한 것! 위의 두가지 프로레스를 통해 시큐리티 세션에 Authentication 객체가 들어가지게 되는 것

 

 

→ 테스트

굿!

  • 이제 /user 라고 주소를 쳐서 들어가도 로그인 성공하면 잘 들어가 진다!

 

 

 

  • 시큐리티 권한 처리 해보기 (Manager/Admin)
  • 권한을 주기 위해 매니저/어드민 아이디 생성

  • 권한을 MySQL을 통해 각각 부여
update user set role = 'ROLE_MANAGER' where id=2;
update user set role = 'ROLE_ADMIN' where id=3;
commit;

굿!

  • SecurityConfig에 설정되있는데로 어드민은 모든 곳 가능, 메니저는 어드민 제외 가능하게 된다. 

 

  • 특정 페이지에 인증 및 권한을 걸고 싶을 때!
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Secured("ROLE_ADMIN")
	@GetMapping("/info")
	public @ResponseBody String info() {
		return "Personal Information";
	}

→ 로그인을 요구하게되지만 ROLE이 ADMIN이 아닌사람은 못들어가게 된다! 참 편리하다

 

 

  • 또 다른 시큐리티 기능
@EnableGlobalMethodSecurity(prePostEnabled = true)
	@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
	@GetMapping("/data")
	public @ResponseBody String data() {
		return "Data Information";
	}

→ Secured과 같지만 여러개를 지정 할 수 있음. @PreAuthorize는 함수 실행 전에 실행 됨 (@PostAuthorieze는 잘 안씀)