들어가며...
지난 글에서는 Repository 부분을 구현하였습니다. 보통 Repository나 Dao(Data Access Object)의 경우는 SQL과 매핑되는 인터페이스들을 정의하고 이렇게 매핑된 SQL문들을 이용하여 실제 필요한 데이터들에 후처리 또는 Policy(정책)들을 적용시키고자 할 때 Servie Layer를 두개 됩니다.
간단한 작업의 경우에는 Repository에서 반환되는 데이터만으로도 충분하지만 번거롭더라도 되도록이면 Repository(또는 Dao)와 Service 를 분리하여 구현하는 습관을 가지는 것이 좋다고 봅니다.
이번 글에서는 Account와 Role과 관련된 Repository를 이용하는 Service Layer를 구현하고 Spring Security의 UserDetailsService interface를 부분을 구현해 보도록 하겠습니다.
- Table of Contents
- UserService 구현
- UserDetailsService 구현
UserService 구현
원래는 UserService 부분에 회원가입 또는 로그인 요청에 대한 로직을 구현하는 것이 좀 더 좋은 방법일 듯 하지만 이번 프로젝트에서는 Repository와 Service를 나누어 개발하는 것에 초점을 두고 Account, Role Repository의 함수 중 필요한 부분만 User Service 에 구현 하는 것으로 하겠습니다.
● UserService.java
package com.demo.security.auth;
import com.demo.security.auth.model.Account;
public interface UserService {
public Account getUserByEmail(String email) throws Exception;
public Account getUserByUsername(String username) throws Exception;
public Account setUser(Account user) throws Exception;
}
● UserServiceImpl.java
package com.demo.security.auth.service;
import java.util.Arrays;
import java.util.HashSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.demo.security.auth.UserService;
import com.demo.security.auth.eo.ERole;
import com.demo.security.auth.model.Account;
import com.demo.security.auth.model.Role;
import com.demo.security.auth.repository.RoleRepository;
import com.demo.security.auth.repository.AccountRepository;
@Service(value = "userServiceImpl")
public class UserServiceImpl implements UserService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private BCryptPasswordEncoder bcryptPasswordEncoder;
////////////////////////////////////////////////////////////////////////////////
//< public functions (override)
/**
* Find a user information by email
*/
@Override
public Account getUserByEmail(String email) throws Exception {
return accountRepository.findByEmail(email);
}
/**
* Find a user information by user name
*/
@Override
public Account getUserByUsername(String username) throws Exception {
return accountRepository.findByUsername(username);
}
/**
* Save the user information
*/
@Override
public Account setUser(Account user) throws Exception {
//< encoding the password
user.setPassword(bcryptPasswordEncoder.encode(user.getPassword()));
//< set the active flag
user.setIsActive(true);
//< set the user role
Role userRole = null;
if(user.getUsername().equals("admin")) {
userRole = roleRepository.findByRole(ERole.ADMIN.getValue());
}
else if(user.getUsername().equals("user")) {
userRole = roleRepository.findByRole(ERole.MANAGER.getValue());
}
else {
userRole = roleRepository.findByRole(ERole.GUEST.getValue());
}
//< set the user roles
user.setRoles(new HashSet<Role>(Arrays.asList(userRole)));
//< save the user information and return result
return accountRepository.save(user);
}
}
크게 설명할 부분은 없으나 setUser부분을 간략하게 설명하면 다음과 같습니다.
- 요청 시 전달된 패스워드의 인코딩 처리 : 사용자의 패스워드 값을 그대로 데이터베이스에 저장해서는 안됩니다. encoding 방식은 원하는 방식으로 얼마든지 customizing할 수 있습니다.
- 사용자의 권한(Authority) 정보 저장 : 여러 방식이 있겠지만 보통은 최초 프로젝트 생성 시 Admin 계정을 미리 생성하고 이후 회원가입을 통해 등록되는 사용자는 최소한의 권한만 할당토록 한 후 Admin 또는 운영자 권한의 사용자가 권한을 수정하는 방식을 사용하게 될 것입니다. 이는 서비스의 종류에 따라 다양한 방법이 있을 것이기 때문에 정답을 없습니다. 다만, 이번 프로젝트에서는 "admin"으로 회원가입하는 사용자는 "ADMIN"권한을 "user"로 회원가입하는 사용자는 "MANAGER"권한을 그 외 사용자는 모두 "GUEST" 권한으로 할당토록 하였습니다.
● ERole.java
package com.demo.security.auth.eo;
public enum ERole {
ADMIN("ROLE_ADMIN"), MANAGER("ROLE_MANAGER"), GUEST("ROLE_GUEST");
private String value;
///////////////////////////////////////////////////////////////////////////////////////
//< constructors
/**
* Basic Constructor
*/
private ERole(String value) {
this.setValue(value);
}
///////////////////////////////////////////////////////////////////////////////////////
//< getter and setter
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
본 프로젝트에서 사용할 권한을 Enum Object로 정의하였습니다.
UserDetailsService 구현 (IMPORTANT ONE)
UserDetailsService는 Spring Security 내부적으로 사용자의 인증을 수행할 때 사용하는 인터페이스로 "loadUserByUsername" 함수 하나를 구현하여야 합니다. 구현한 내용을 설명하기 전에 전체 소스코드부터 살펴보면 아래와 같습니다.
● CustomUserDetailsService
package com.demo.security.auth.service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import javax.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.demo.security.auth.UserService;
import com.demo.security.auth.model.Account;
import com.demo.security.auth.model.Role;
@Service
public class CustomUserDetailsService implements UserDetailsService {
protected Logger log = LoggerFactory.getLogger(this.getClass());
@Resource(name = "userServiceImpl")
private UserService userService;
////////////////////////////////////////////////////////////////////////////////
//< public functions (override)
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("[ykson] call the loadUserByUsername()");
//< get the user information
Account user = null;
try {
user = userService.getUserByUsername(username);
} catch (Exception e) {
log.error(e.getMessage());
throw new UsernameNotFoundException(e.getMessage());
}
//< set the user authorities
return buildUserForAuthentication(user, getUserAuthority(user.getRoles()));
}
////////////////////////////////////////////////////////////////////////////////
//< private functions
private List<GrantedAuthority> getUserAuthority(Set<Role> userRole) {
Set<GrantedAuthority> roles = new HashSet<GrantedAuthority>();
for(Role role : userRole) {
roles.add(new SimpleGrantedAuthority(role.getRole()));
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(roles);
return grantedAuthorities;
}
private UserDetails buildUserForAuthentication(Account user, List<GrantedAuthority> authorities) {
return new User(user.getUsername(), user.getPassword(), user.getIsActive(), true, true, true, authorities);
}
}
별도의 User 및 Role 데이터베이스를 사용하지 않을 경우에는 번거로운 작업 없이 Spring Security가 내부적으로 사용하는 User, Role을 사용하면 되겠지만 보통의 프로젝트에서 사용자와 권한은 자체적으로 관리하게 됩니다.
이럴 경우 개발자가 정의한 사용자 정보를 Spring Security에서 사용하는 User class정보와 매핑하여 이를 반환하면 권한 체크를 해주게 됩니다. 위의 코드도 자체적으로 정의한 사용자 정보를 Spring Security에서 사용하는 User Object에 맞게 변환하여 이를 반환하는 부분을 구현한 것입니다.
현재는
new User(user.getUsername(), user.getPassword(), user.getIsActive(), true, true, true, authorities);
생성자를 사용하였지만 중요한 항목은 사용자명, 패스워드, 권한정보이며 간략하게 사용하고자 하면 세 항목만 전달하는 생성자를 사용하여도 무방할 것입니다.
package org.springframework.security.core.userdetails;
public class User implements UserDetails, CredentialsContainer {
/** 중략 **/
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
/** 중략 **/
}
마무리...
지금까지 Repository 부분에 추가 로직 부분을 구현할 UserService Layer 및 Spring Security에서 인터페이스로 정의한 UserDetailsService 부분을 구현하면서 살펴보았습니다.
이후에 Spring Security는 로그인 성공 여부에 따라 사용자가 후처리를 할 수 있도록 "AuthenticationFailureHandler", "AuthenticationSuccessHandler"를 제공합니다.
두개의 인터페이스를 implement하여 결과에 따른 후처리 과정을 좀 더 세부적으로 처리할 수 있습니다. 다음 글에서는 두 개의 Handler에 대해서 살펴보도록 하겠습니다.
U2ful은 ♥입니다. @U2ful Corp.