scribnote5 on Aug 23, 20202020-08-23T00:00:00+09:00
Updated Apr 27, 20212021-04-27T00:00:00+09:00 33 min read
Spring Security
- Spring Sercurity는 스프링 기반의 어플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링의 하위 프레임워크이다. 보안 관련하여 다양한 옵션을 제공한다.
- 해당 이미지는 Spring Security 인증 아키텍처이며, Spring Security를 이해하기 위한 자세한 설명은 하단 출처를 참고하였다.
출처: https://sjh836.tistory.com/165
https://webfirewood.tistory.com/m/115?category=694472
로그인 구현 및 설계
- Spring Security를 사용하여 로그인에 필요한 전반적인 기능(파일 업로드를 제공하는 사용자 CRUD 페이지, 권한, 로그아웃, 로그인 실패시 메시지 출력 등)들을 구현하였다.
- Spring Security 관련 기능들은 하단 출처를 주로 참고하여 개발하였다.
- 본 프로젝트에서는 Spring Security Session을 사용하여서 로그인을 구현하였다. 로그인 기능 관련 최신 트렌드는 Session 방식이 아니라 JWT(JSON WEB TOKEN) 방식이다. JWT는 Session 보다 서버의 부하를 줄일 수 있는 방식으로 추후 프로젝트 개발할 때 JWT 방식을 사용할 예정이다.
출처: https://victorydntmd.tistory.com/328
https://ict-nroo.tistory.com/118
권한 종류
Spring Sercurity와 H2-console 같이 사용하기
- Spring Security를 적용한 프로젝트에서 H2-DB를 같이 사용하기 위해서는 부가적인 보안 설정이 필요하다.
- 자세한 설정은 하단 출처를 참고하였다.
출처: https://github.com/HomoEfficio/dev-tips/blob/master/Spring%20Security%EC%99%80%20h2-console%20%ED%95%A8%EA%BB%98%20%EC%93%B0%EA%B8%B0.md
사용자 테이블 설계
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
| <user table>
CREATE TABLE user
(
idx bigint auto_increment primary key,
created_by varchar(255) null,
created_date datetime null,
last_modified_by varchar(255) null,
last_modified_date datetime null,
active_status varchar(255) null,
admission_date date null,
authority_type varchar(255) null,
birth_date date null,
contact varchar(255) null,
email varchar(255) null,
private_email varchar(255) null,
english_name varchar(255) null,
gender varchar(255) null,
graduated_date date null,
introduction longtext null,
korean_name varchar(255) null,
username varchar(255) null,
user_status varchar(255) null,
user_type varchar(255) null,
messanger_id varchar(255) null,
password varchar(255) null,
web_page varchar(255) null,
workplace varchar(255) null
);
|
- 사용자의 이미지 파일 업로드시 파일 정보를 저장하는 테이블을 생성한다.
1
2
3
4
5
6
7
8
9
10
11
| <user_attached_file table>
CREATE TABLE user_attached_file (
idx bigint auto_increment primary key,
created_by varchar(255) null,
created_date datetime(6) null,
file_name varchar(255) null,
saved_file_name varchar(255) null,
user_idx bigint null,
file_size varchar(255) null
);
|
의존성 관리
- module-app-web는 view인 thymeleaf에서 spring Security 관련 태그를 사용가능 하도록 thymeleaf-extras-springSecurity5 의존성 라이브러리를 추가하였다.
- 모든 모듈에서 spring-boot-starter-Security 의존성 라이브러리를 사용하도록 변경하였다.
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
| <build.gradle>
// 프로젝트 개발에 필요한 공통 의존성 라이브러리를 선언한다.
dependencies {
// spring boot
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-Security"
runtimeOnly "org.springframework.boot:spring-boot-devtools"
...
project(":module-app-web") {
dependencies {
compile project(":module-system-common")
compile project(":module-domain-core")
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "org.thymeleaf.extras:thymeleaf-extras-springSecurity5"
}
}
project("module-app-api") {
dependencies {
compile project(":module-system-common")
compile project(":module-domain-core")
// spring-boot-starter-Security 의존성 라이브러리를 전체 프로젝트로 변경
}
...
|
Spring Security 설정 파일
- Spring Security 기본 설정을 개발자가 새로 정의한 파일이다.
- 각 설정에 대한 설명은 주석을, 인증 처리를 위한 CORS 관련 설명은 하단 출처를 참고하였다.
SecurityConfig.java에서 설정한 항목
- static 디렉터리 파일들을 Spring Security 보안 설정에서 제외
- 페이지 접근 권한 설정
- 비밀번호 암호화(BCryptPasswordEncoder) 라이브러리 사용
- 로그인 및 로그아웃 설정
- 로그인 성공 및 실패시 이를 처리하는 사용자가 정의 예외처리 handelr 등록
출처: https://oddpoet.net/blog/2017/04/27/cors-with-spring-Security/
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
| <module-app-web/src/main/java/kr/ac/univ/config/SecurityConfig.java>
package kr.ac.univ.config;
import kr.ac.univ.handler.CustomAuthenticationFailureHandler;
import kr.ac.univ.handler.CustomAuthenticationSuccessHandler;
import kr.ac.univ.user.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.Security.authentication.AuthenticationManager;
import org.springframework.Security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.Security.config.annotation.web.builders.HttpSecurity;
import org.springframework.Security.config.annotation.web.builders.WebSecurity;
import org.springframework.Security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.Security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.Security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.Security.crypto.password.PasswordEncoder;
import org.springframework.Security.web.authentication.AuthenticationFailureHandler;
import org.springframework.Security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.Security.web.header.writers.frameoptions.WhiteListedAllowFromStrategy;
import org.springframework.Security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.Security.web.util.matcher.AntPathRequestMatcher;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
// static 디렉터리의 하위 파일 목록은 인증 무시 ( = 항상통과 )
web.ignoring().antMatchers("/css/**", "/js/**", "/imgages/**", "/summernote/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 페이지 권한 설정
.antMatchers("/user/list").hasAuthority("root")
.antMatchers("/h2-console/**").permitAll() // h2-console 접근 허용
.antMatchers("/**").permitAll()
.and()
.csrf().ignoringAntMatchers("/console/**") // h2-console csrf 제외
.and()
.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(new WhiteListedAllowFromStrategy(Arrays.asList("localhost")))) // he-console X-Frame-Options 제외
.frameOptions().sameOrigin()
.and()
// 로그인 설정
.formLogin()
.loginPage("/user/login") // login 페이지 URL
// 사용자 정의 handler
.successHandler(CustomAuthenticationSuccessHandler())
.failureHandler(CustomAuthenticationFailureHandler())
.defaultSuccessUrl("/user/index") // login 성공 URL
.permitAll()
.and()
// 로그아웃 설정
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/user/logout/success")
.invalidateHttpSession(true)
.and()
// 권한이 없는 경우, 403 예외처리 핸들링
.exceptionHandling().accessDeniedPage("/user/permission-denied");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 비밀번호 암호화에 사용될 PasswordEncoder를 BCryptPasswordEncoder로 사용
* BCryptPasswordEncoder는 해시 뿐만 아니라 Salt를 넣는 작업을 수행하므로, 입력 값이 같음에도 매번 다른 encoded된 값을 반환함
*
* @return
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/**
* 로그인 성공 후 수행하는 handler
*
* @return
*/
@Bean
public AuthenticationSuccessHandler CustomAuthenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
/**
* 로그인 실패 후 수행하는 handler
*
* @return
*/
@Bean
public AuthenticationFailureHandler CustomAuthenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
/**
* 개발자가 원하는 시점에 로그인을 할 수 있도록 구현 가능
*
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
|
Handler
- 로그인 실패 또는 성공시 수행하는 handler를 사용자가 새로 정의한 파일이다.
- 로그인 실패시 실패한 에러 메시와 id를 controller에 전달하며, controller는 다시 로그인 페이지에 전달한다.
- 에러 종류에 따른 에러 메시지는 각 주석을 참고하면 된다.
출처: https://u2ful.tistory.com/35
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
| <module-app-web/src/main/java/kr/ac/univ/handler/CustomAuthenticationFailureHandler.java>
package kr.ac.univ.handler;
import org.springframework.Security.authentication.*;
import org.springframework.Security.core.AuthenticationException;
import org.springframework.Security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 로그인 실패시 기능 구현
String username = request.getParameter("username");
String errormsg = null;
if (exception instanceof BadCredentialsException) {
errormsg = "The email or passwords do not match.";
} else if (exception instanceof InternalAuthenticationServiceException) {
errormsg = "The authentication is not processed due to system problem that occured internally.";
} else if (exception instanceof AuthenticationCredentialsNotFoundException) {
errormsg = "The authentication is not found.";
}
// 추후 잠금 및 비활성화 기능 구현
else if (exception instanceof LockedException) {
errormsg = "This user is locked.";
} else if (exception instanceof DisabledException) {
errormsg = "This user is disabled.";
}
request.setAttribute("username", username);
request.setAttribute("errormsg", errormsg);
request.getRequestDispatcher("/user/login/fail").forward(request, response);
}
}
|
- 로그인 성공시 수행하는 handler로, 추후 admon 페이지가 개발되면 root와 manager 권한을 가진 사용자를 admin 페이지로 redirect 시킬 예정이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <module-app-web/src/main/java/kr/ac/univ/handler/CustomAuthenticationSuccessHandler.java>
package kr.ac.univ.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Configuration;
import org.springframework.Security.core.Authentication;
import org.springframework.Security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
@Configuration
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
// 로그인 성공시 기능 구현
super.onAuthenticationSuccess(request, response, authentication);
}
}
|
UserPrincipal
- Spring Security는 UserDetails 인터페이스를 구현한 UserPricipal 클래스에 데이터를 저장한 다음 사용자 인증을 진행한다.
- 사용자가 로그인하는 동안 UserPricipal 클래스를 통하여 데이터를 불러올 수 있다.
- UserPricipal 클래스의 멤버 변수와 생성자를 변경하면, 사용자가 로그인하는 동안 UserPricipal 클래스가 저장하는 데이터를 변경할 수 있다.
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
| <module-domain-core/src/main/java/kr/ac/univ/user/dto/UserPrincipal.java>
package kr.ac.univ.user.dto;
import kr.ac.univ.common.domain.enums.ActiveStatus;
import kr.ac.univ.user.domain.User;
import kr.ac.univ.user.domain.enums.AuthorityType;
import lombok.*;
import org.springframework.Security.core.GrantedAuthority;
import org.springframework.Security.core.authority.SimpleGrantedAuthority;
import org.springframework.Security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@ToString
public class UserPrincipal implements UserDetails {
private long idx;
@Getter(value = AccessLevel.NONE)
private String username;
@Getter(value = AccessLevel.NONE)
private String password;
private String koreanName;
private String englishName;
private ActiveStatus activeStatus;
private AuthorityType authorityType;
public UserPrincipal(User user) {
setIdx(user.getIdx());
setUsername(user.getUsername());
setPassword(user.getPassword());
setKoreanName(user.getKoreanName());
setEnglishName(user.getEnglishName());
setActiveStatus(user.getActiveStatus());
setAuthorityType(user.getAuthorityType());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
switch (this.getAuthorityType()) {
case ROOT:
authorities.add(new SimpleGrantedAuthority(AuthorityType.ROOT.getAuthorityType()));
break;
case MANAGER:
authorities.add(new SimpleGrantedAuthority(AuthorityType.MANAGER.getAuthorityType()));
break;
case GENERAL:
authorities.add(new SimpleGrantedAuthority(AuthorityType.GENERAL.getAuthorityType()));
break;
default:
authorities.add(new SimpleGrantedAuthority(AuthorityType.NON_USER.getAuthorityType()));
break;
}
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.getActiveStatus() == ActiveStatus.ACTIVE;
}
}
|
Repository
- User에서 사용하는 쿼리다.
- countByMemberId: 매개변수의 username과 같은 사용자를 모두 검색한다.(username 중복 조회)
- findAllByUsernameContaining, findAllByKoreanNameContaining, findAllByEmailContaining: username, koreanname, email을 검색할 때 사용하는 메소드다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| <module-domain-core/src/main/java/kr/ac/univ/user/repository/UserRepository.java>
package kr.ac.univ.user.repository;
import kr.ac.univ.noticeBoard.domain.NoticeBoard;
import kr.ac.univ.user.domain.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Long countByUsername(String username);
User findByUsername(String username);
Page<User> findAllByUsernameContaining(Pageable pageable, String username);
Page<User> findAllByKoreanNameContaining(Pageable pageable, String koreanName);
Page<User> findAllByEmailContaining(Pageable pageable, String email);
}
|
Service
- UserDetails 인터페이스를 구현하면 Spring Security에서 해당 인터페이스를 구현한 클래스에서 인증 작업을 수행한다.
- 사용자가 새로 정의한 메소드는 다음과 같다.
- joinUser: 회원 가입 할 때 사용하는 메소드로, 비밀번호는 암호화하여 DB에 저장한다.
- loadUserByUsername: 사용자가 새로 정의한 로그인 메소스다. Spring Security가 사용자 인증시 데이터를 저장하는 UserPrincipal 객체를 생성 후 반환한다. 예외 처리는 추후 개발 예정이다.
출처: https://to-dy.tistory.com/86
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
| <module-domain-core/src/main/java/kr/ac/univ/user/service/UserService.java>
package kr.ac.univ.user.service;
import kr.ac.univ.common.dto.SearchDto;
import kr.ac.univ.user.domain.User;
import kr.ac.univ.user.dto.UserDto;
import kr.ac.univ.user.dto.UserPrincipal;
import kr.ac.univ.user.dto.mapper.UserMapper;
import kr.ac.univ.user.repository.UserRepository;
import kr.ac.univ.util.EmptyUtil;
import org.springframework.data.domain.*;
import org.springframework.Security.core.userdetails.UserDetails;
import org.springframework.Security.core.userdetails.UserDetailsService;
import org.springframework.Security.core.userdetails.UsernameNotFoundException;
import org.springframework.Security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Page<UserDto> findUserList(Pageable pageable, SearchDto searchDto) {
Page<User> userList = null;
Page<UserDto> userDtoList = null;
pageable = PageRequest.of(pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber() - 1, pageable.getPageSize(), Sort.Direction.DESC, "idx");
switch (searchDto.getSearchType()) {
case "USER_ID":
userList = userRepository.findAllByUsernameContaining(pageable, searchDto.getKeyword());
break;
case "KOREAN_NAME":
userList = userRepository.findAllByKoreanNameContaining(pageable, searchDto.getKeyword());
break;
case "Email":
userList = userRepository.findAllByEmailContaining(pageable, searchDto.getKeyword());
break;
default:
userList = userRepository.findAll(pageable);
break;
}
userDtoList = new PageImpl<UserDto>(UserMapper.INSTANCE.toDto(userList.getContent()), pageable, userList.getTotalElements());
return userDtoList;
}
/**
* 회원 가입
*
* @param userDto
* @return
*/
@Transactional
public Long joinUser(UserDto userDto) {
// 비밀번호 암호화
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
userDto.setPassword(passwordEncoder.encode(userDto.getPassword()));
return userRepository.save(UserMapper.INSTANCE.toEntity(userDto)).getIdx();
}
/**
* 사용자 정의 로그인
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
UserPrincipal userPrincipal = null;
if(EmptyUtil.isEmpty(user)) {
// 추후 변경: 에러 처리
throw new UsernameNotFoundException("Username is not found");
} else {
userPrincipal = new UserPrincipal(user);
}
return userPrincipal;
}
public Long insertUser(User user) {
return userRepository.save(user).getIdx();
}
public User findUserByIdx(Long idx) {
return userRepository.findById(idx).orElse(new User());
}
@Transactional
public Long updateUser(Long idx, UserDto userDto) {
User persistUser = userRepository.getOne(idx);
User user = UserMapper.INSTANCE.toEntity(userDto);
persistUser.update(user);
return userRepository.save(persistUser).getIdx();
}
public void deleteUserByIdx(Long idx) {
userRepository.deleteById(idx);
}
public boolean isDupulicationUserByUsername(String username) {
return (userRepository.countByUsername(username) > 0) ? true : false;
}
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
}
|
Controller
- User(로그인, CRUD 페이지) 관련한 URI 매핑을 담당한다.
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
| <module-app-web/src/main/java/kr/ac/univ/controller/UserController.java>
package kr.ac.univ.controller;
import kr.ac.univ.common.dto.SearchDto;
import kr.ac.univ.user.dto.UserDto;
import kr.ac.univ.user.dto.UserPrincipal;
import kr.ac.univ.user.dto.mapper.UserMapper;
import kr.ac.univ.user.service.UserAttachedFileService;
import kr.ac.univ.user.service.UserService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.Security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/user")
public class UserController {
private final UserService userService;
private final UserAttachedFileService userAttachedFileService;
public UserController(UserService userService, UserAttachedFileService userAttachedFileService) {
this.userService = userService;
this.userAttachedFileService = userAttachedFileService;
}
// Login Index
@GetMapping("/index")
public String index(@AuthenticationPrincipal UserPrincipal userPrincipal, Model model) {
model.addAttribute("userDto", userPrincipal);
return "/user/index";
}
//Login Page
@GetMapping("/login")
public String login() {
return "/user/login";
}
// Login Fail
@PostMapping("/login/fail")
public String loginFail(HttpServletRequest request, String errormsg) {
return "/user/login";
}
// Logout
@GetMapping("/logout/success")
public String logout() {
return "/user/logoutSuccess";
}
// Permission Denied
@GetMapping("/permission-denied")
public String permissionDenied() {
return "/user/permission-denied";
}
// List
@GetMapping("/list")
public String noticeBoardList(@PageableDefault Pageable pageable, SearchDto searchDto, Model model) {
model.addAttribute("userDtoList", userService.findUserList(pageable, searchDto));
return "/user/list";
}
// Form Update
@GetMapping("/form{idx}")
public String loginForm(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
UserDto userDto = null;
userDto = UserMapper.INSTANCE.toDto(userService.findUserByIdx(idx));
userDto = UserMapper.INSTANCE.toDto(userDto, userAttachedFileService.findAttachedFileByUserIdx(idx));
model.addAttribute("userDto", userDto);
return "/user/form";
}
// Read
@GetMapping({"", "/"})
public String noticeBoardRead(@RequestParam(value = "idx", defaultValue = "0") Long idx, Model model) {
UserDto userDto = null;
userDto = UserMapper.INSTANCE.toDto(userService.findUserByIdx(idx));
userDto = UserMapper.INSTANCE.toDto(userDto, userAttachedFileService.findAttachedFileByUserIdx(idx));
model.addAttribute("userDto", userDto);
return "/user/read";
}
}
|
RestController
- 회원 가입하는 사용자의 ID의 중복을 확인한다.
- User를 Create, Update, Delete를 담당하는 메소드는 생략하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| <module-app-api/src/main/java/kr/ac/univ/controller/UserRestController.java>
...
@PostMapping
public ResponseEntity<?> postUser(@RequestBody UserDto userDto) {
// 추후 변경
if (userService.isDupulicationUserByUsername(userDto.getUsername())) {
}
Long idx = userService.joinUser(userDto);
return new ResponseEntity<>(idx, HttpStatus.CREATED);
}
...
|
View
- CSRF(Cross-site request forgery)는 사용자가 자신의 의지와는 무관하게 공격자의 의도한 행위를 특정 웹사이트에 요청하게 하는 공격이다.
- CSRF 공격을 방지하기 위한 대표적인 방법은 form 페이지에 CSRF 토큰을 사용하는 것이다.
- View에서 CSRF 토큰을 보내면 Spring Security는 해당 토큰 값을 확인하고, 신뢰할수 있는 form 데이터인지 확인한다.
출처: https://velog.io/@josworks27/CSRF-%EA%B0%9C%EB%85%90
https://velog.io/@max9106/Spring-Security-csrf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <module-app-web/src/main/resources/templates/layout/script.html>
<script th:src="@{/js/jquery.min.js}"></script>
<script th:src="@{/js/jquery.serialize-object.min.js}"></script>
<script th:src="@{/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/js/fileUtil.js}"></script>
<script th:src="@{/summernote/summernote.min.js}"></script>
<script th:inline="javascript">
$(function() {
var csrfToken = /*[[${_csrf.token}]]*/ null;
var csrfHeader = /*[[${_csrf.headerName}]]*/ null;
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader(csrfHeader, csrfToken);
});
});
</script>
|
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
| <module-app-web/src/main/resources/templates/user/login.html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- css -->
<th:block th:replace="layout/css.html"></th:block>
<title>Login</title>
</head>
<body>
<!-- header -->
<div class="container">
<h1>Login</h1>
<hr>
<form action="/user/login" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="text" name="username" th:value="*{username}" placeholder="Enter the Id.">
<input type="password" name="password" placeholder="Enter the passwords.">
<span th:text="*{errormsg}"> </span>
<button type="submit">Login</button>
</form>
</div>
</body>
</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
31
32
33
34
35
36
37
38
39
| <module-app-web/src/main/resources/templates/user/index.html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- css -->
<th:block th:replace="layout/css.html"></th:block>
<title>Index</title>
</head>
<body>
<!-- header -->
<div class="container">
<form name="form" id="form" th:object="${userDto}" action="#">
<h1>Index</h1>
<hr>
<a sec:authorize="isAnonymous()" th:href="@{/user/login}">Login</a>
<a sec:authorize="isAnonymous()" th:href="@{/user/form}">Join</a>
<a sec:authorize="isAuthenticated()" th:href="@{/user/logout}">Logout</a>
<a sec:authorize="isAuthenticated()" th:href="@{'/user?idx='+*{idx}}">My Information</a>
<br>
<br>
User Authority:
<a sec:authorize="hasAuthority('root')">root</a>
<a sec:authorize="hasAuthority('manager')">manager</a>
<a sec:authorize="hasAuthority('general')">gerneral</a>
<br>
<br>
<!-- User Authority Check:-->
<!-- <span sec:authentication="principal.authorities"></span>-->
</form>
</div>
</body>
</html>
|
1
2
3
4
5
6
| <module-app-web/src/main/resources/templates/user/logoutSuccess.html>
<script>
alert("You have logged out.");
location.href = "/user/index";
</script>
|
- 사용자의 권한으로는 접근 불가능한 페이지에 접근하는 경우 이동하는 페이지다.
1
2
3
4
5
6
| <module-app-web/src/main/resources/templates/user/permission-denied.html>
<script>
alert("You do not have permission on this path.");
window.history.back();
</script>
|
- User form 페이지에서 ID 중복을 위한 기능을 ajax로 구현하였다.
- 사용자는 회원 가입 전 ID가 중복되는지 확인 후 회원 가입을 진행해야 한다.(해당 기능은 구현되었으며 추후 세부적으로 개발 예정)
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
| <module-app-web/src/main/resources/templates/user/form.html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<!-- css -->
<th:block th:replace="layout/css.html"></th:block>
<title>Member Form</title>
</head>
<body>
<!-- header -->
<div th:replace="layout/header::header"></div>
...
/* username 중복 검사 */
$("#validationUserId").click(function () {
$.ajax({
url: "http://localhost:8081/api/users/validation/username/" + document.getElementsByName("username")[0].value,
type: "get",
dataType: "text",
contentType: "application/json",
async: false,
success: function (msg) {
if (msg == "false") {
document.getElementById("usernameCheckResult").innerHTML = "This user id is not duplicated.";
document.getElementById("usernameCheckResult").style.color = "blue";
usernameVaildation = true;
} else {
document.getElementById("usernameCheckResult").innerHTML = "This user id is already in use.";
document.getElementById("usernameCheckResult").style.color = "red";
usernameVaildation = false;
}
},
error: function () {
alert("User id duplicate fail!");
}
});
});
...
|
Util
- 현재 로그인한 사용자 정보를 가져와 해당 페이지에 접근하는지 판별하는 메소드다.
- 이후 각 페이지 개발이 완료되면 해당 메소드를 사용하여 권한별로 그리고 사용자 아이디 별로 페이지를 접근 가능 여부 기능을 구현할 예정이다.
출처: https://itstory.tk/entry/Spring-Security-%ED%98%84%EC%9E%AC-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EB%B3%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0
https://lemontia.tistory.com/602
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
| <module-system-common/src/main/java/kr/ac/univ/util/AccessCheck.java>
package kr.ac.univ.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
public class AccessCheck {
/**
* [일반적인 상황에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* 비인증 사용자인 경우 접근 불가
* root: 모든 권한에 대한 접근 허용
* manager: 생성자가 root인 경우 접근 허용, 로그인한 사용자의 username과 생성자가 같은 경우 접근 허용
*
* @param createdBy
* @return
*/
public static Boolean isAccessInGeneral(String createdBy, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if (!"anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
for (GrantedAuthority grantedAuthority : userDetails.getAuthorities()) {
switch (grantedAuthority.getAuthority()) {
// 로그인한 사용자의 권한: root
// -> 접근 가능
case "root":
result = true;
break;
case "manager":
// createdBy: root
// -> 접근 불가
if ("root".equals(createdBy)) {
result = false;
}
// username authority: MANAGER
// 로그인한 사용자의 username과 username: 다름
// -> 접근 불가
else if ("manager".equals(authorityType) && !authenticationUsername.equals(createdBy)) {
result = false;
}
// 나머지 조건
// -> 접근 가능
else {
result = true;
}
break;
default:
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
if (authenticationUsername.equals(createdBy)) {
result = true;
}
// 로그인한 사용자의 username과 createdBy: 다름
// -> 접근 불가
else {
result = false;
}
break;
}
}
}
return result;
}
/**
* [module-app-admin user에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* 비인증 사용자인 경우 접근 불가
* root: 모든 권한에 대한 접근 허용
* manager: 생성자가 root인 경우 접근 허용, 로그인한 사용자의 username과 생성자가 같은 경우 접근 허용
*
* @param createdBy
* @return
*/
public static Boolean isAccessInModuleAdminUser(String createdBy, String username, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if (!"anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
for (GrantedAuthority grantedAuthority : userDetails.getAuthorities()) {
switch (grantedAuthority.getAuthority()) {
// 로그인한 사용자의 권한: root
// -> 접근 가능
case "root":
result = true;
break;
case "manager":
// 로그인한 사용자의 권한: manager
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
if (authenticationUsername.equals(createdBy)) {
result = true;
}
// 로그인한 사용자의 권한: manager
// username의 권한 general || non_user
// -> 접근 가능
else if (("general".equals(authorityType) || "non_user".equals(authorityType))) {
result = true;
}
// 로그인한 사용자의 권한: manager
// 로그인한 사용자의 username과 username: 같음
// -> 접근 가능
else if(authenticationUsername.equals(username)) {
result = true;
}
// 이외
// -> 접근 불가
else {
result = false;
}
break;
default:
result = false;
break;
}
}
}
return result;
}
/**
* [module-app-web user에서 사용자 권한에 따른 접근 가능 여부]
* <p>
* 비인증 사용자인 경우 접근 불가
* 생성자가 root인 경우 접근 허용
* 생성자 권한이 MANAGER인 경우 접근 허용
* 생성자와 사용자 아이디가 같은 경우 접근 허용
*
* @param createdBy
* @return
*/
public static Boolean isAccessInModuleWebUser(String createdBy, String username, String authorityType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean result = false;
// 비인증 사용자, 인증이 안된 경우, authentication 객체가 null인 경우
// -> 접근 불가
if ("anonymousUser".equals(authentication.getPrincipal()) || !authentication.isAuthenticated() || EmptyUtil.isEmpty(authentication)) {
result = false;
} else {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String authenticationUsername = userDetails.getUsername();
// createdBy: root
// username authority: MANAGER
// 로그인한 사용자의 username과 username: 같음
// -> 접근 가능
if ("root".equals(createdBy) || "MANAGER".equals(authorityType) && username.equals(authenticationUsername)) {
result = true;
}
// 로그인한 사용자의 username과 createdBy: 같음
// -> 접근 가능
else if (authenticationUsername.equals(createdBy)) {
result = true;
} else {
result = false;
}
}
return result;
}
}
|
- 해당 변수가 문자열인 경우 공백을 제외한 문자열의 길이가 0인지, 객체인 경우 NULL 인지 확인하기 위해 사용하는 Util 메소드다.
출처: https://gun0912.tistory.com/1
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
| <module-system-common/src/main/java/kr/ac/univ/util/EmptyUtil.java>
package kr.ac.univ.util;
import java.util.List;
import java.util.Map;
public class EmptyUtil {
public static boolean isEmpty(Object obj) {
if (obj == null)
return true;
if ((obj instanceof String) && (((String) obj).trim().length() == 0)) {
return true;
}
if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
}
if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
}
if (obj instanceof List) {
return ((List<?>) obj).isEmpty();
}
if (obj instanceof Object[]) {
return (((Object[]) obj).length == 0);
}
return false;
}
}
|
main
- 프로젝트 수행시 사용자 정보를 미리 DB에 저장하도록 하였다.
- 사용자 아이디와 비밀번호는 ‘root/123123123’, ‘manager/123123123’, ‘general/123123123’, ‘non_user/123123123’ 다.
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
| <module-app-web/src/main/java/kr/ac/univ/ModuleWebApplication.java>
package kr.ac.univ;
import kr.ac.univ.common.domain.enums.ActiveStatus;
import kr.ac.univ.noticeBoard.repository.NoticeBoardRepository;
import kr.ac.univ.user.domain.enums.AuthorityType;
import kr.ac.univ.user.domain.enums.UserType;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.Security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RestController;
import kr.ac.univ.user.domain.User;
import kr.ac.univ.user.repository.UserRepository;
@RestController
@SpringBootApplication
public class ModuleWebApplication {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public static void main(String[] args) {
SpringApplication.run(ModuleWebApplication.class, args);
}
@Bean
public CommandLineRunner runner(NoticeBoardRepository noticeBoardRepository, UserRepository userRepository) {
return (args) -> {
/* 게시글 모두 삭제 */
/* noticeBoardRepository.deleteAll(); */
/* 게시글 등록 */
/*
IntStream.rangeClosed(1, 200).forEach(index ->
noticeBoardRepository.save(NoticeBoard.builder()
.title("게시글" + index)
.content("컨텐츠" + index)
.activeStatus(ActiveStatus.ACTIVE)
.build()));
*/
/* 사용자 모두 삭제 */
/* userRepository.deleteAll(); */
/* 사용자 생성 */
/* userRepository.save(User.builder()
.username("root")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.ROOT)
.activeStatus(ActiveStatus.ACTIVE)
.build());
userRepository.save(User.builder()
.username("manager")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.MANAGER)
.activeStatus(ActiveStatus.ACTIVE)
.build());
userRepository.save(User.builder()
.username("manager2")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.MANAGER)
.activeStatus(ActiveStatus.ACTIVE)
.build());
userRepository.save(User.builder()
.username("general")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.GENERAL)
.activeStatus(ActiveStatus.ACTIVE)
.build());
userRepository.save(User.builder()
.username("general2")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.GENERAL)
.activeStatus(ActiveStatus.ACTIVE)
.build());
userRepository.save(User.builder()
.username("non_user")
.password(passwordEncoder.encode("123123123"))
.userType(UserType.PART_TIME_MS)
.authorityType(AuthorityType.NON_USER)
.activeStatus(ActiveStatus.ACTIVE)
.build());
*/
};
}
}
|
프로젝트 수행 결과
Comments powered by Disqus.