1. 회원가입 처리
회원가입을 해본다. 강의에서 정확한 소스 코드를 찾을 수 없어서 내 맘대로 작성한 코드라 UI나 코드 효용성이 별로일 수 있다. 그리고 기록을 위해서 대부분의 코드를 붙여놓겠다.
UserController
/users url로 Get, Post Mapping을 한다. 회원등록을 할 수 있는 GetMapping 페이지에는 model 객체에 roles를 전달해준다.
회원등록을 실제 진행하는 PostMapping에서는 사용자의 입력값을 AccountDto 형태로 받아온다. userService에서 userRepository.save를 할건데, 해당 레포지토리는 Account Entity 형태로만 저장이 가능하므로 modelMapper를 이용한다.
@Controller
@AllArgsConstructor
public class UserController {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@GetMapping("/mypage")
public String myPage() {
return "user/mypage";
}
@GetMapping("/users")
public String createUser(Model model) {
Roles[] roles = Roles.values();
model.addAttribute("roles", roles);
return "user/login/register";
}
@PostMapping("/users")
public String createUser(AccountDto accountDto) {
ModelMapper mapper = new ModelMapper();
Account account = mapper.map(accountDto, Account.class);
account.setPassword(passwordEncoder.encode(account.getPassword()));
userService.createUser(account);
return "redirect:/";
}
}
Role Enum
public enum Roles {
USER, MANAGER, ADMIN
}
AccountEntity, AccountDto
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Account {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
private String email;
private String age;
private String role;
}
@Data
public class AccountDto {
private String username;
private String password;
private String email;
private String age;
private String role;
}
ModelMapper
ModelMapper는 mapstruct 라이브러리와 비슷하게 객체를 복사해준다. 여기서 사용한 메서드는 map(source, target) 메서드로 AccountDto를 AccountEntity로 변환 매핑했다.
사용을 위해서는 pom.xml에 modelmapper를 의존성 추가해주어야 한다.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.0</version>
</dependency>
UserSerivceImpl
@Service
@Transactional(readOnly = true)
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Transactional
@Override
public void createUser(Account account) {
userRepository.save(account);
}
}
UserRepository
jpaRepository를 상속받아서 쓴다.
public interface UserRepository extends JpaRepository<Account, Long> {
}
register.html
Controller에서 GetMapping의 Path를 "user/login/register"로 등록했기 때문에, 해당 파일의 경로를 이와 같이 지정해야 한다. resources/templates/user/login/register.html 파일을 아래와 같이 작성한다.
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/header::userHead"></head>
<body>
<div th:replace="layout/top::header"></div>
<body>
<script type="text/javascript">
</script>
<div class="container">
<div class="tbl_wrp">
<form class="form-horizontal" th:action="@{/users}" method="post">
<div class="form-group">
<label for="username" class="col-sm-2 control-label">아이디</label>
<div class="col-sm-10">
<input type="text" class="form-control input-large" name="username" id="username"
placeholder="username" required>
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-2 control-label">비밀번호</label>
<div class="col-sm-10">
<input type="password" class="form-control input-large" name="password" id="password"
placeholder="Password" data-minlength="6" required>
</div>
</div>
<div class="form-group">
<label for="email" class="col-sm-2 control-label">이메일</label>
<div class="col-sm-10">
<input type="email" class="form-control input-large" name="email" id="email" placeholder="이메일"
required>
</div>
</div>
<div class="form-group">
<label for="age" class="col-sm-2 control-label">나이</label>
<div class="col-sm-10">
<input type="text" class="form-control input-large" name="age" id="age" placeholder="나이" required>
</div>
</div>
<div class="selected">
<label for="age" class="col-sm-2 control-label">권한</label>
<div class="col-sm-10">
<select class="select_type01" th:name="role">
<option value="선택"> 선택</option>
<option th:each="role : ${roles}"
th:value="${role?.name()}"
th:utext="${role?.name()}"></option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-1 col-sm-10">
<button type="Submit" class="btn btn-dark btn-lg">가입하기</button>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
2. DB 연동 인증 처리 : UserDetailsService 활용
SecurityConfig 내용 변경 : AuthenticationManagerBuilder
DB 연동 처리를 확실히 하기 위해서, SecurityConfig에서 memory를 이용해서 인증처리했던 부분은 삭제한다.
///삭제----------------------------------------------------
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = passwordEncoder().encode("1111");
auth.inMemoryAuthentication().withUser("user").password(password).roles("USER");
auth.inMemoryAuthentication().withUser("manager").password(password).roles("MANAGER");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("ADMIN");
}
///삭제----------------------------------------------------
그리고 스프링 시큐리티가 인증처리를 하게 하기 위해서, 위와 같은 메서드를 Override 하되, userDetailsService 메서드를 사용한다.
@Configuration
@EnableWebSecurity
@Slf4j
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
userDetailsService
해당 메서드는 UserDetailsService 인터페이스를 구현한 구현체를 인자로 받는다. 즉 스프링 시큐리티가 인증을 처리하게 하기 위해서 UserDetailsService의 구현체를 만들어줘야한다. 이를 위해 CustomUserDetailsService를 작성한다.
customUserDetailsService
위에서 언급한 스프링 시큐리티의 인자조건을 맞추기 위해서, CustomUserDetailsService는 UserDetailsService를 상속해야만 한다. 그러면 기본적으로 loaduserByUsername 메서드를 Override 하라는 에러가 발생한다. 이 메서드를 Override하여 우리가 원하는 형태로 바꿔주면 된다.
다만, 마지막에 반환하는 AccountContext 객체를 반환해야하는데, AccountContext는 그냥 이름 붙이기 나름이고, 실제 이 객체는 User(org.springframework.security.core.userdetails) 객체를 상속한 객체일 뿐이다. 스프링 시큐리티가 요구하는 스펙을 지키는 것이다. 아래에서 조금 더 살펴볼 것이다.
account를 JPA를 이용해서 조회해온다. 아래처럼 findByUsername과 같은 Spring Data JPA 문법을 사용해도 되고, 일반적으로 JPA를 사용할 때 사용하듯이 데이터를 DTO 형태로 불러와도 된다. 이렇게 하면 여러 테이블에서 더 많은 정보를 불러올 수도 있을 것이다. 커스터마이징하기 나름이다.
권한 정보는 GrantedAutority 타입으로 담아야한다. 그래서 account로부터 Role을 받아와서 SimpleGrantedAutority 객체에 넣어주었다.
추가로, @Serivce 어노테이션을 사용할 때, 해당 클래스의 BeanName을 따로 지정해주는 것이 좋을 수 있다.
(@Service("userrDetailsService"))
왜냐하면 SecurityConfig에서 userDetailsService 메서드의 인자로 사용되는 userDetailsService를 주입받을 때, 지금 예시와 같은 경우는 UserDetailsService 타입에 해당하는 Bean이 CustomUserDetailsService로 1개 뿐이지만, 만약 여러 개가 된다면 스프링은 빈의 "타입"을 비교해보고, 여러 개라면 "이름"값을 참조하기 때문이다. 스프링 기초편에서 배운 내용이다.
@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//jpa를 사용하여 account 테이블에서 계정 정보 조회
Account account = userRepository.findByUsername(username);
if (account == null) {
throw new UsernameNotFoundException("UsernameNotFound");
}
List<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority(account.getRole())); //계정이 갖고 있는 권한을 부여한다.
AccountContext accountContext = new AccountContext(account, roles);
return accountContext;
}
}
AccountContext
AccountContext는 앞서 언급한 바와 같이 User 타입을 상속받은 커스텀 객체일뿐이다. 이 User 타입은 UserDetails를 implements하고 있는데, 이 UserDetails 타입으로 계정에 관한 정보를 넣어주어야 스프링 시큐리티가 요구하는 스펙(인증 메서드를 사용하기 위한 인자 조건)을 맞출 수 있게 된다.
다만 User 클래스를 상속받으면 아래 코드에서 User 객체 생성자를 생성할때와 같이 username, password값을 넘겨주어야 하는데, 여기서는 Account라는 직접 만든 클래스를 이용하므로 account를 인자로 받고, User 생성자를 만들어주는 super 부분에서는 account.getUsername(), account.getPassword() 메서드를 활용한 점도 주목하자.
public class AccountContext extends User {
private final Account account;
public AccountContext(Account account, Collection<? extends GrantedAuthority> authorities) {
super(account.getUsername(), account.getPassword(), authorities);
this.account = account;
}
public Account getAccount() {
return account;
}
}
User 클래스의 생성자 부분
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
이렇게 하면 이제 DB에 있는 정보로 로그인이 가능해진다.
참조
1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의
'Programming-[Backend] > Spring Security' 카테고리의 다른 글
[스프링 시큐리티] 13. 인증 부가/ 성공 처리 기능 : WebAuthenticationDetails, AuthenticationSuccessHandler (0) | 2022.01.18 |
---|---|
[스프링 시큐리티] 12. DB 연동 인증 처리 : AuthenticationProvider, 로그인 및 로그아웃 페이지 처리 (0) | 2022.01.17 |
[스프링 시큐리티] 10. 프로젝트 생성, PostgreSQL, PasswordEncoder, WebIgnore (0) | 2022.01.13 |
[스프링 시큐리티][작성중][메모] 9. 주요 아키텍처 이해 (0) | 2022.01.09 |
[스프링 시큐리티] 8. 위조 방지 : CsrfFilter (0) | 2022.01.09 |