LostCatBox

SpringProject-Board-CH02

Word count: 2.2kReading time: 13 min
2022/12/24 10 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. 로그인 구현