LostCatBox

SpringProject-Board-CH02

Word count: 2.2kReading time: 13 min
2022/12/24 Share

Spring 게시판 프로젝트 2편 (회원가입, 로그인)

Created Time: July 13, 2022 7:08 PM
Last Edited Time: September 2, 2022 1:01 PM
Tags: Java, Spring, Computer

로그인 구현

Spring Security란?

Spring Security는 스프링 기반 에플리케이션의 보안을 담당 해주는 스프링 하위 프레임워크이다. 이를 활용하면 개발자가 직접 보안 관련 로직을 짜는 수고를 덜 수가 있다.

  • 용어정리(인증후 권한부여됨.)

    • 접근 주체(Principal)

      보호되어 있는 리소스에 접근하고자 하는 대상

    • 인증(Authentication)
      ‘유저’를 확인
      하는 작업, 접근한 대상이 어떤 종류의 유저인지 확인하는 과정
      클라이언트가 자신이 주장하는 사용자와 같은 사용자인지를 확인하는 과정

    • 권한(Authorization)
      어떤 리소스에 대한 접근 제한을 의미, 모든 리소스는 각각 권한이 걸려있으며, 인가 과정에서 최소한의 권한을 확인하는 것
      권한부여, 클라이언트가 하고자 하는 작업이 해당 클라이언트에게 허가된 작업인지 확인

제공하는기능

출처

build.gradle

1
2
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
  • spring-boot-starter-security : 스프링 시큐리티를 사용하기 위해 추가
  • thymeleaf-extras-springsecurity5 : 뷰 단에서 현재 로그인된 사용자의 정보를 가져오기 위해 추가(Thymeleaf에서 Spring Security를 이용하기 위해)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--ROLE_USER 권한을 갖는다면 이 글이 보임-->
<h1 sec:authorize="hasRole('ADMIN')">Has admin Role</h1>

<!--ROLE_ADMIN 권한을 갖는다면 이 글이 보임-->
<h1 sec:authorize="hasRole('USER')">Has user Role</h1>

<!--어떤 권한이건 상관없이 인증이 되었다면 이 글이 보임-->
<div sec:authorize="isAuthenticated()">
Only Authenticated user can see this Text
</div>

<!--인증시 사용된 객체에 대한 정보-->
<b>Authenticated DTO:</b>
<div sec:authentication="principal"></div>

<!--인증시 사용된 객체의 Username (ID)-->
<b>Authenticated username:</b>
<div sec:authentication="name"></div>

<!--객체의 권한-->
<b>Authenticated user role:</b>
<div sec:authentication="principal.authorities"></div>

Config 파일 작성

  • WebSecurityConfigurerAdapter를 상속받아 구현하면된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RequiredArgsConstructor
@EnableWebSecurity // 1
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { // 2

private final UserService userService; // 3

@Override
public void configure(WebSecurity web) { // 4
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
}

@Override
protected void configure(HttpSecurity http) throws Exception { // 5
http
.authorizeRequests() // 6
.antMatchers("/login", "/signup", "/user").permitAll() // 누구나 접근 허용
.antMatchers("/").hasRole("USER") // USER, ADMIN만 접근 가능
.antMatchers("/admin").hasRole("ADMIN") // ADMIN만 접근 가능
.anyRequest().authenticated() // 나머지 요청들은 권한의 종류에 상관 없이 권한이 있어야 접근 가능
.and()
.formLogin() // 7
.loginPage("/login") // 로그인 페이지 링크
.defaultSuccessUrl("/") // 로그인 성공 후 리다이렉트 주소
.and()
.logout() // 8
.logoutSuccessUrl("/login") // 로그아웃 성공시 리다이렉트 주소
.invalidateHttpSession(true) // 세션 날리기
;
}

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception { // 9
auth.userDetailsService(userService)
// 해당 서비스(userService)에서는 UserDetailsService를 implements해서
// loadUserByUsername() 구현해야함 (서비스 참고)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
  1. Spring Security를 활성화한다는 의미의 어노테이션입니다.
  2. WebSecurityConfigurerAdapter는 Spring Security의 설정파일로서의 역할을 하기 위해 상속해야 하는 클래스입니다.
  3. 후에 사용할 유저 정보를 가져올 클래스입니다. 아직 만들어지지 않았습니다.
  4. WebSecurityConfigurerAdapter를 상속받으면 오버라이드할 수 있습니다. 인증을 무시할 경로들을 설정해놓을 수 있습니다.
    • static 하위 폴더 (css, js, img)는 무조건 접근이 가능해야하기 때문에 인증을 무시해야합니다.
  5. WebSecurityConfigurerAdapter를 상속받으면 오버라이드할 수 있습니다.
    • http 관련 인증 설정이 가능합니다
  6. 접근에 대한 인증 설정이 가능합니다.
    • anyMatchers를 통해 경로 설정과 권한 설정이 가능합니다.
      • permitAll() : 누구나 접근이 가능
      • hasRole() : 특정 권한이 있는 사람만 접근 가능
      • authenticated() : 권한이 있으면 무조건 접근 가능
      • anyRequestanyMatchers에서 설정하지 않은 나머지 경로를 의미합니다.
  7. 로그인에 관한 설정을 의미합니다.
    • loginPage() : 로그인 페이지 링크 설정
    • defaultSuccessUrl() : 로그인 성공 후 리다이렉트할 주소
  8. 로그아웃에 관한 설정을 의미합니다.
    • logoutSccessUrl() : 로그아웃 성공 후 리다이렉트할 주소
    • invalidateHttpSession() : 로그아웃 이후 세션 전체 삭제 여부
  9. 로그인할 때 필요한 정보를 가져오는 곳입니다.
    • 유저 정보를 가져오는 서비스를 userService (아직 만들어지지 않음)으로 지정합니다.
    • 패스워드 인코더는 아까 빈으로 등록해놓은 passwordEncoder()를 사용합니다. (BCrypt)

이 외에도 더 많은 속성들이 있지만, 대표적으로 많이 쓰이는 설정들 위주로 정리했습니다. 이제 UserService를 만들기 위해 필요한 User와 User 정보를 가져올 UserRepository를 만들어보도록 하겠습니다.

User Entity작성

  • User 엔티티는 UserDetails를 상속받아서 구현합니다. UserDetails에서 필수로 구현해야 하는 메소드는 아래와 같습니다.(로그인시 반환되는것으로 권한, 이름(id),비번 등 모두 구현되어있어야함)

Untitled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class User implements UserDetails {

@Id
@Column(name = "id")
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;

@Column(name = "email", unique = true)
private String email;

@Column(nullable = false)
private String nickname;

@Column(name = "password")
private String password;

@Column(name = "role")
private String role;

@Builder
public User(String email, String nickname, String password, String role) {
this.email = email;
this.nickname = nickname;
this.password = password;
this.role = role;
}

// 사용자의 권한을 콜렉션 형태로 반환
// 단, 클래스 자료형은 GrantedAuthority를 구현해야함
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
for (String sprite_role : role.split(",")) {
roles.add(new SimpleGrantedAuthority(sprite_role));
}
return roles;
}

// 사용자의 id를 반환 (unique한 값)
@Override
public String getUsername() {
return email;
}

// 사용자의 password를 반환
@Override
public String getPassword() {
return password;
}

// 계정 만료 여부 반환
@Override
public boolean isAccountNonExpired() {
// 만료되었는지 확인하는 로직
return true; // true -> 만료되지 않았음
}

// 계정 잠금 여부 반환
@Override
public boolean isAccountNonLocked() {
// 계정 잠금되었는지 확인하는 로직
return true; // true -> 잠금되지 않았음
}

// 패스워드의 만료 여부 반환
@Override
public boolean isCredentialsNonExpired() {
// 패스워드가 만료되었는지 확인하는 로직
return true; // true -> 만료되지 않았음
}

// 계정 사용 가능 여부 반환
@Override
public boolean isEnabled() {
// 계정이 사용 가능한지 확인하는 로직
return true; // true -> 사용 가능
}
}
  • 여기서 추가로 설명할 부분은 getAuthorities()인데, 이 메소드는 사용자의 권한을 콜렉션 형태로 반환해야하고, 콜렉션의 자료형은 무조건적으로 GrantedAuthority를 구현해야합니다. 저는 권한이 중복되면 안 되기 때문에 Set<GrantedAuthority>을 사용했습니다.
  • GrantedAuthority(스프링시큐리티의 자료형)으로 반환해줘야함
  • ADMIN은 관리자의 권한(ADMIN)뿐만 아니라 일반 유저(USER)의 권한도 가지고 있기 때문에, ADMIN의 auth는 “ROLE_ADMIN,ROLE_USER”와 같은 형태로 전달이 될 것이고, 쉼표(,) 기준으로 잘라서 ROLE_ADMIN과 ROLE_USER를 roles에 추가해줍니다. 아까 루트 패스(“/“)에 권한을 USER에게만 주었지만, ADMIN은 두 개의 권한(USER, ADMIN)을 가지고 있기 때문에 접근이 가능합니다.
  • 이제 User 엔티티를 모두 구현했으니, 정보를 가져오기 위해 필요한 Repository를 구현해보겠습니다.

User Repository작성

1
2
3
public interface UserRepository extends JpaRepository <UserInfo, Long > {
Optional<UserInfo> findByEmail(String email);
}
  • JpaRepository를 상속받아줍니다. 그리고 email를 통해 회원을 조회하기 위해 findByEmail()를 만들어줍니다.
  • 이제 Entity와 Repository까지 만들었으니, 아까 Config 파일에서 미리 주입해놓았던 UserService를 만들어보도록 하겠습니다.

User Service 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {

private final UserRepository userRepository;

/**
* Spring Security 필수 메소드 구현
*
* @param email 이메일
* @return UserDetails
* @throws UsernameNotFoundException 유저가 없을 때 예외 발생
*/
@Override // 기본적인 반환 타입은 UserDetails, UserDetails를 상속받은 UserInfo로 반환 타입 지정 (자동으로 다운 캐스팅됨)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { // 시큐리티에서 지정한 서비스이기 때문에 이 메소드를 필수로 구현
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException((email)));
}
/**
* 회원정보 저장
*
* @param infoDto 회원정보가 들어있는 DTO
* @return 저장되는 회원의 PK
*/
public Long save(UserInfoDto infoDto) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
infoDto.setPassword(encoder.encode(infoDto.getPassword())); //입력받은 패스워드를 **BCrypt**로 암호화

return userRepository.save(infoDto.toEntity()).getCode();
}
}
  • 아까 설정해놓은 서비스를 만들어주고, UserDetailService를 상속받습니다. 그 후에 필수 메소드인 loadUserByUsername()
    를 구현하는데, 기본 반환 타입인 UserDetails를 UserInfo
    로 바꿔줍니다. UserInfo는 UserDetails을 상속받았기 때문에 자동으로 다운 캐스팅이 됩니다.
  • 그리고 방금 만든 UesrRepository의 findByEmail()를 사용해서 null이면 UsernameNotFoundException 예외를 발생시키고, null이 아니면 UserInfo를 반환하게 처리해줍니다. 이렇게 하면 로그인 관련 기능 구현은 모두 완료되었습니다.
  • 회원가입시 회원정보를 저장하는 메소드를 만들기 위해 UserService
    를 수정합니다. 입력받은 패스워드를 BCrypt로 암호화한 후에 회원을 저장해주는 메소드를 만들어줍니다.

User DTO 작성

  • 폼으로 받을 회원정보를 매핑 시켜줄 객체만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Setter
public class UserInfoDto {
private String email;
private String password;

private String auth;

public UserInfo toEntity(){
UserInfo userInfo = UserInfo.builder()
.email(email)
.auth(auth)
.password(password)
.build();
return userInfo;

}
}

Controller 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequiredArgsConstructor
@Controller
public class UserController {

private final UserService userService;

@PostMapping("/user")
public String signup(UserInfoDto infoDto) { // 회원 추가
userService.save(infoDto);
return "redirect:/login";
}

@GetMapping(value = "/logout")
public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
}
}
  • 회원가입시 UserInfoDto를 통해 값을 받고 save()호출
  • 로그아웃시, 기본으로 제공해주는 SecurityContextLogoutHandler()
    logout() 을 사용해서 로그아웃 처리를 해주었습니다.이제 로그아웃까지 구현했으니, 나머지를 구현해보도록 하겠습니다

요청에 해당하는 필요한 뷰 연결

  • MvcConfig.java
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MvcConfig implements WebMvcConfigurer {

// 요청 - 뷰 연결
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("main");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/admin").setViewName("admin");
registry.addViewController("/signup").setViewName("signup");
}
}

html, 뷰 파일

admin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>admin</title>
</head>
<body>
<h2>관리자 전용 페이지</h2>
ID : <span sec:authentication="name"></span> <br>
소유 권한 : <span sec:authentication="authorities"></span> <br>

<form id="logout" action="/logout" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="submit" value="로그아웃"/>
</form>
</body>
</html>

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>Login</h1> <hr>

<form action="/login" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
email : <input type="text" name="username"> <br>
password : <input type="password" name="password"> <br>
<button type="submit">Login</button>
</form> <br>

<a href="/signup">Go to join! →</a>
</body>
</html>
  • 로그인할 때에는 csrf 를 보내줘야하기 때문에 hideen 타입으로 함께 보냅니다.

main.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<title>main</title>
</head>
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../../static/css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<h2>회원 전용 페이지</h2>
ID : <span sec:authentication="name"></span> <br>
소유 권한 : <span sec:authentication="authorities"></span> <br>

<form id="logout" action="/logout" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="submit" value="로그아웃"/>
</form>

</body>
</html>

signup.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>sign up</title>
</head>
<body>
<h1>Sign Up</h1> <hr>

<form th:action="@{/user}" method="POST">
email : <input type="text" name="email"> <br>
password : <input type="password" name="password"> <br>
<input type="radio" name="auth" value="ROLE_ADMIN,ROLE_USER"> admin
<input type="radio" name="auth" value="ROLE_USER" checked="checked"> user <br>
<button type="submit">Join</button>
</form> <br>

<a href="/login">Go to login →</a>
</body>
</html>
  • sec:authentication
    를 이용하면 권한, 이름 같이 현재 로그인된 정보를 확인할 수 있습니다. 로그아웃 역시 POSTcsrf 를 붙여줍니다. (하지만 아까 GET /logout에 관한 컨트롤러 메소드를 만들어주었기 때문에 를 사용해도 무방합니다)
CATALOG
  1. 1. Spring 게시판 프로젝트 2편 (회원가입, 로그인)
  2. 2. 로그인 구현
    1. 2.1. Spring Security란?
      1. 2.1.1. 제공하는기능
    2. 2.2. 출처
    3. 2.3. build.gradle
    4. 2.4. Config 파일 작성
    5. 2.5. User Entity작성
    6. 2.6. User Repository작성
    7. 2.7. User Service 작성
    8. 2.8. User DTO 작성
    9. 2.9. Controller 작성
    10. 2.10. 요청에 해당하는 필요한 뷰 연결
    11. 2.11. html, 뷰 파일
      1. 2.11.1. admin.html
      2. 2.11.2. login.html
      3. 2.11.3. main.html
      4. 2.11.4. signup.html