19 Commits

Author SHA1 Message Date
Daeil Choi
4f13595479 Add custom filter 추가 2023-02-07 06:47:33 +09:00
Daeil Choi
26ef372734 Remove SampleTest 삭제 2023-02-06 15:27:23 +09:00
Daeil Choi
a3c9c60d9e Add 테스트에 expect view 추가 2023-02-06 15:19:54 +09:00
Daeil Choi
4c0c60a69f Modify NoteControllerTest WithUserDetails로 테스트하도록 수정 2023-02-06 15:16:49 +09:00
Daeil Choi
432e0aff08 Add 어드민 컨트롤러 테스트 코드 추가 2023-02-06 15:10:00 +09:00
Daeil Choi
b209a017d4 Add NoteControllerTest 추가 2023-02-06 14:48:29 +09:00
Daeil Choi
d798b23d63 Add 정적리소스는 SpringSecurity 대상에서 제외하도록 ignoring추가 2023-02-06 14:27:32 +09:00
Daeil Choi
8c26d488fc Modify AdminController 패키지 수정,
Modify 기본 데이터 초기화 Config refactoring,
Remove Example 페이지 삭제,
Modify logging level 수정,
Add application.yml 주석추가,
Add SpringSecurity 로고 icon 추가
2023-02-06 14:05:50 +09:00
Daeil Choi
f6d702da7f Modify Post를 Note로 수정 2023-02-06 13:55:02 +09:00
Daeil Choi
b7d58486ac Add User Test추가 2023-02-06 12:26:46 +09:00
Daeil Choi
3f5f5cbc4b Modify 주석 추가 2023-02-06 12:04:50 +09:00
Daeil Choi
110324776d Add remember-me 설정 추가 2023-02-06 11:53:40 +09:00
Daeil Choi
584d7f1b94 Modify 게시글 관련 오류 수정 2023-02-06 11:47:56 +09:00
Daeil Choi
20ccbd7e13 Add PostService Test 추가 2023-02-06 11:13:13 +09:00
Daeil Choi
ad10828483 Remove PostStatus삭제 2023-02-06 10:38:07 +09:00
Daeil Choi
de564ef4fe Mdofiy 관리자페이지 게시글내역 히스토리 볼수 있도록 수정, securityConfig 순환참조 오류 수정 2023-02-06 09:47:25 +09:00
Daeil Choi
b5c06cf76f Add user에 isAdmin추가 2023-02-03 16:31:19 +09:00
Daeil Choi
d4f032ad32 Add UserNotFound Exception 추가 2023-02-03 16:30:16 +09:00
Daeil Choi
b531d2ac0e Add Noticecontroller Security test 추가 2023-02-03 16:27:30 +09:00
47 changed files with 1004 additions and 293 deletions

View File

@@ -19,17 +19,27 @@ repositories {
}
dependencies {
// spring data jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// security
implementation 'org.springframework.boot:spring-boot-starter-security'
// 웹 페이지를 쉽게 생성하기 위한 thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// spring web mvc
implementation 'org.springframework.boot:spring-boot-starter-web'
// Thymeleaf에서 SpringSecurity를 Integration
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
// lombok
compileOnly 'org.projectlombok:lombok'
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
// h2
runtimeOnly 'com.h2database:h2'
// starter test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// security test
testImplementation 'org.springframework.security:spring-security-test'
// junit test
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

View File

@@ -3,6 +3,10 @@ package com.example.springsecuritystudy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* SpringSecurity 학습용 Application
*/
@SpringBootApplication
public class SpringSecurityStudyApplication {

View File

@@ -0,0 +1,35 @@
package com.example.springsecuritystudy.admin;
import java.util.List;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.springsecuritystudy.note.Note;
import com.example.springsecuritystudy.note.NoteService;
import com.example.springsecuritystudy.user.User;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
@RequestMapping("/admin")
public class AdminController {
private final NoteService noteService;
/**
* 어드민인 경우 게시글 조회
* @return admin/index.html
*/
@GetMapping
public String getPostForAdmin(Authentication authentication, Model model) {
User user = (User) authentication.getPrincipal();
List<Note> notes = noteService.findByUser(user);
model.addAttribute("notes", notes);
return "admin/index";
}
}

View File

@@ -0,0 +1,15 @@
package com.example.springsecuritystudy.common;
/**
* 이미 등록된 유저를 재등록하려고 할때 발생하는 Exception
*/
public class AlreadyRegisteredUserException extends RuntimeException {
public AlreadyRegisteredUserException(String message) {
super(message);
}
public AlreadyRegisteredUserException() {
super("이미 등록된 유저입니다.");
}
}

View File

@@ -0,0 +1,15 @@
package com.example.springsecuritystudy.common;
/**
* 유저를 찾을 수 없을 때 발생하는 Exception
*/
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
public UserNotFoundException() {
super("유저를 찾을 수 없습니다.");
}
}

View File

@@ -0,0 +1,47 @@
package com.example.springsecuritystudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import com.example.springsecuritystudy.note.NoteService;
import com.example.springsecuritystudy.notice.NoticeService;
import com.example.springsecuritystudy.user.User;
import com.example.springsecuritystudy.user.UserService;
import lombok.RequiredArgsConstructor;
/**
* 초기 상태 등록 Config
*/
@Configuration
@RequiredArgsConstructor
@Profile(value = "!test")
public class InitializeDefaultConfig {
private final UserService userService;
private final NoteService noteService;
private final NoticeService noticeService;
/**
* 유저등록, note 4개 등록
*/
@Bean
public void initializeDefaultUser() {
User user = userService.signup("user", "user");
noteService.saveNote(user, "테스트", "테스트입니다.");
noteService.saveNote(user, "테스트2", "테스트2입니다.");
noteService.saveNote(user, "테스트3", "테스트3입니다.");
noteService.saveNote(user, "여름 여행계획", "여름 여행계획 작성중...");
}
/**
* 어드민등록, 공지사항 2개 등록
*/
@Bean
public void initializeDefaultAdmin() {
userService.signupAdmin("admin", "admin");
noticeService.saveNotice("환영합니다", "환영합니다 여러분");
noticeService.saveNotice("게시글 작성 방법 공지", "1. 회원가입\n2. 로그인\n3. 게시글 작성\n4. 저장\n* 본인 외에는 게시글을 볼 수 없습니다.");
}
}

View File

@@ -3,7 +3,10 @@ package com.example.springsecuritystudy.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
* JPA auditor enable
*/
@Configuration
@EnableJpaAuditing
public class AuditorConfig {
public class JpaAuditorConfig {
}

View File

@@ -3,18 +3,12 @@ package com.example.springsecuritystudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* WebMVC Config
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/home").setViewName("index");
registry.addViewController("/admin").setViewName("admin/index");
registry.addViewController("/login").setViewName("login");
}
public class MvcConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){

View File

@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* PasswordEncoder Config
*/
@Configuration
public class PasswordEncoderConfig {

View File

@@ -1,32 +1,55 @@
package com.example.springsecuritystudy.config;
import com.example.springsecuritystudy.filter.StopwatchFilter;
import com.example.springsecuritystudy.filter.TesterAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.example.springsecuritystudy.user.UserRepository;
import lombok.RequiredArgsConstructor;
@EnableWebSecurity
/**
* Security 설정 Config
*/
@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final UserRepository userRepository;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// stopwatch filter
http.addFilterBefore(
new StopwatchFilter(),
WebAsyncManagerIntegrationFilter.class
);
// tester authentication filter
http.addFilterBefore(
new TesterAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))),
UsernamePasswordAuthenticationFilter.class
);
http
.httpBasic().disable()
.csrf();
http
.rememberMe();
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/", "/home", "/signup", "/example",
"/css/**", "/js/**", "/h2-console/**").permitAll()
.antMatchers("/post").hasRole("USER")
.antMatchers("/", "/home", "/signup").permitAll()
.antMatchers("/note").hasRole("USER")
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/notice").hasRole("ADMIN")
@@ -50,12 +73,11 @@ public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().antMatchers("/css/**", "/js/**", "/h2-console/**");
// 정적 리소스 spring security 대상에서 제외
return (web) -> web.ignoring()
.antMatchers("/h2-console/**")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
;
}
@Bean
public UserDetailsService userDetailsService() {
return username -> userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("유저를 찾지 못 했습니다."));
}
}

View File

@@ -0,0 +1,26 @@
package com.example.springsecuritystudy.config;
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 org.springframework.transaction.annotation.Transactional;
import com.example.springsecuritystudy.common.UserNotFoundException;
import com.example.springsecuritystudy.user.UserRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserDetailServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(UserNotFoundException::new);
}
}

View File

@@ -1,17 +0,0 @@
package com.example.springsecuritystudy.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SampleController {
@GetMapping("/example")
public String example(Model model) {
model.addAttribute("name", "정우성");
model.addAttribute("age", 51);
return "example";
}
}

View File

@@ -0,0 +1,27 @@
package com.example.springsecuritystudy.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class StopwatchFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
StopWatch stopWatch = new StopWatch(request.getServletPath());
stopWatch.start();
filterChain.doFilter(request, response);
stopWatch.stop();
// Log StopWatch '/login' : running time = 150545041 ns
log.info(stopWatch.shortSummary());
}
}

View File

@@ -0,0 +1,42 @@
package com.example.springsecuritystudy.filter;
import com.example.springsecuritystudy.user.User;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 테스트 유저인 경우에는 어드민과 유저 권한 모두를 줍니다.
*/
public class TesterAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public TesterAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
AuthenticationException {
Authentication authentication = super.attemptAuthentication(request, response);
User user = (User) authentication.getPrincipal();
if (user.getUsername().startsWith("test")) {
// 테스트 유저인 경우 어드민과 유저 권한 모두 부여
return new UsernamePasswordAuthenticationToken(
user,
null,
Stream.of("ROLE_ADMIN", "ROLE_USER")
.map(authority -> (GrantedAuthority) () -> authority)
.collect(Collectors.toList())
);
}
return authentication;
}
}

View File

@@ -1,8 +1,6 @@
package com.example.springsecuritystudy.post;
package com.example.springsecuritystudy.note;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@@ -15,6 +13,7 @@ import com.example.springsecuritystudy.model.BaseTimeEntity;
import com.example.springsecuritystudy.user.User;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -22,7 +21,7 @@ import lombok.NoArgsConstructor;
@Table
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseTimeEntity {
public class Note extends BaseTimeEntity {
@Id
@GeneratedValue
@@ -30,17 +29,20 @@ public class Post extends BaseTimeEntity {
private String title;
@Lob
private String content;
@Enumerated(EnumType.STRING)
private PostStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "USER_ID")
private User user;
public Post(String title, String content, User user) {
@Builder
public Note(String title, String content, User user) {
this.title = title;
this.content = content;
this.status = PostStatus.Y;
this.user = user;
}
public void updatePost(String title, String content) {
this.title = title;
this.content = content;
}
}

View File

@@ -1,4 +1,4 @@
package com.example.springsecuritystudy.post;
package com.example.springsecuritystudy.note;
import java.util.List;
@@ -18,30 +18,30 @@ import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
@RequestMapping("/post")
public class PostController {
@RequestMapping("/note")
public class NoteController {
private final PostService postService;
private final NoteService noteService;
@GetMapping
public String findByPost(Authentication authentication, Model model) {
public String getPost(Authentication authentication, Model model) {
User user = (User) authentication.getPrincipal();
List<Post> posts = postService.findByUser(user);
model.addAttribute("posts", posts);
return "post/index";
List<Note> notes = noteService.findByUser(user);
model.addAttribute("notes", notes);
return "note/index";
}
@PostMapping
public String savePost(@ModelAttribute PostDto postDto, Authentication authentication) {
public String savePost(@ModelAttribute NoteDto noteDto, Authentication authentication) {
User user = (User) authentication.getPrincipal();
postService.savePost(user, postDto.getTitle(), postDto.getContent());
return "redirect:post";
noteService.saveNote(user, noteDto.getTitle(), noteDto.getContent());
return "redirect:note";
}
@DeleteMapping
public String deletePost(@RequestParam Long id, Authentication authentication) {
User user = (User) authentication.getPrincipal();
postService.deletePost(user, id);
return "redirect:post";
noteService.deleteNote(user, id);
return "redirect:note";
}
}

View File

@@ -1,11 +1,11 @@
package com.example.springsecuritystudy.post;
package com.example.springsecuritystudy.note;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class PostDto {
public class NoteDto {
private String title;
private String content;

View File

@@ -0,0 +1,15 @@
package com.example.springsecuritystudy.note;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.springsecuritystudy.user.User;
public interface NoteRepository extends JpaRepository<Note, Long> {
List<Note> findByUserOrderByIdDesc(User user);
Note findByIdAndUser(Long id, User user);
}

View File

@@ -0,0 +1,47 @@
package com.example.springsecuritystudy.note;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.springsecuritystudy.common.UserNotFoundException;
import com.example.springsecuritystudy.user.User;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@Transactional
public class NoteService {
private final NoteRepository noteRepository;
@Transactional(readOnly = true)
public List<Note> findByUser(User user) {
userNullCheck(user);
if (Boolean.TRUE.equals(user.isAdmin())) {
return noteRepository.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
return noteRepository.findByUserOrderByIdDesc(user);
}
private static void userNullCheck(User user) {
if (user == null) {
throw new UserNotFoundException();
}
}
public Note saveNote(User user, String title, String content) {
userNullCheck(user);
return noteRepository.save(new Note(title, content, user));
}
public void deleteNote(User user, Long id) {
userNullCheck(user);
Note note = noteRepository.findByIdAndUser(id, user);
noteRepository.delete(note);
}
}

View File

@@ -1,16 +1,14 @@
package com.example.springsecuritystudy.notice;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Lob;
import com.example.springsecuritystudy.model.BaseTimeEntity;
import com.example.springsecuritystudy.post.PostStatus;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -25,12 +23,10 @@ public class Notice extends BaseTimeEntity {
private String title;
@Lob
private String content;
@Enumerated(EnumType.STRING)
private PostStatus status;
@Builder
public Notice(String title, String content) {
this.title = title;
this.content = content;
this.status = PostStatus.Y;
}
}

View File

@@ -11,10 +11,13 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.springsecuritystudy.post.PostDto;
import com.example.springsecuritystudy.note.NoteDto;
import lombok.RequiredArgsConstructor;
/**
* 공지사항 서비스 Controller
*/
@Controller
@RequiredArgsConstructor
@RequestMapping("/notice")
@@ -30,8 +33,8 @@ public class NoticeController {
}
@PostMapping
public String savePost(@ModelAttribute PostDto postDto) {
noticeService.saveNotice(postDto.getTitle(), postDto.getContent());
public String savePost(@ModelAttribute NoteDto noteDto) {
noticeService.saveNotice(noteDto.getTitle(), noteDto.getContent());
return "redirect:notice";
}

View File

@@ -2,6 +2,7 @@ package com.example.springsecuritystudy.notice;
import java.util.List;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -16,7 +17,7 @@ public class NoticeService {
@Transactional(readOnly = true)
public List<Notice> findAll() {
return noticeRepository.findAll();
return noticeRepository.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
public Notice saveNotice(String title, String content) {

View File

@@ -1,14 +0,0 @@
package com.example.springsecuritystudy.post;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.springsecuritystudy.user.User;
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByUserAndStatus(User user, PostStatus status);
Post findByIdAndUser(Long id, User user);
}

View File

@@ -1,44 +0,0 @@
package com.example.springsecuritystudy.post;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.springsecuritystudy.user.User;
import com.example.springsecuritystudy.user.UserRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@Transactional
public class PostService {
private final UserRepository userRepository;
private final PostRepository postRepository;
@Transactional(readOnly = true)
public List<Post> findByUser(User user) {
userNullCheck(user);
return postRepository.findByUserAndStatus(user, PostStatus.Y);
}
private static void userNullCheck(User user) {
if (user == null) {
throw new RuntimeException("유저가 없습니다.");
}
}
public Post savePost(User user, String title, String content) {
userNullCheck(user);
return postRepository.save(new Post(title, content, user));
}
public void deletePost(User user, Long id) {
userNullCheck(user);
Post post = postRepository.findByIdAndUser(id, user);
postRepository.delete(post);
}
}

View File

@@ -1,6 +0,0 @@
package com.example.springsecuritystudy.post;
public enum PostStatus {
Y,
N
}

View File

@@ -12,6 +12,7 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -28,6 +29,7 @@ public class User implements UserDetails {
private String password;
private String authority;
@Builder
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
@@ -39,6 +41,10 @@ public class User implements UserDetails {
return Collections.singleton((GrantedAuthority) () -> authority);
}
public Boolean isAdmin() {
return authority.equals("ROLE_ADMIN");
}
@Override
public boolean isAccountNonExpired() {
return true;

View File

@@ -4,23 +4,26 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
@RequestMapping("/signup")
public class UserController {
private final UserService userService;
@GetMapping
@GetMapping("/login")
public String loginView() {
return "login";
}
@GetMapping("/signup")
public String signupView() {
return "signup";
}
@PostMapping
@PostMapping("/signup")
public String signup(@ModelAttribute UserDto userDto) {
userService.signup(userDto.getUsername(), userDto.getPassword());
return "redirect:login";

View File

@@ -1,9 +1,11 @@
package com.example.springsecuritystudy.user;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.springsecuritystudy.common.AlreadyRegisteredUserException;
import com.example.springsecuritystudy.common.UserNotFoundException;
import lombok.RequiredArgsConstructor;
@Service
@@ -14,22 +16,34 @@ public class UserService {
private final PasswordEncoder passwordEncoder;
public User signup(String username, String password) {
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("이미 등록된 유저입니다.");
}
return userRepository.save(new User(username, passwordEncoder.encode(password), "ROLE_USER"));
alreadyRegisteredUser(username);
User user = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.authority("ROLE_USER")
.build();
return userRepository.save(user);
}
public User signupAdmin(String username, String password) {
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("이미 등록된 Admin유저입니다.");
}
return userRepository.save(new User(username, passwordEncoder.encode(password), "ROLE_ADMIN"));
alreadyRegisteredUser(username);
User user = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.authority("ROLE_ADMIN")
.build();
return userRepository.save(user);
}
public User findByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 유저입니다."));
.orElseThrow(UserNotFoundException::new);
}
private void alreadyRegisteredUser(String username) {
if (userRepository.findByUsername(username).isPresent()) {
throw new AlreadyRegisteredUserException();
}
}
}

View File

@@ -4,9 +4,10 @@ server:
session:
timeout: 10m # 기본 30분, 최소는 1분
# logging 레벨 설정
logging:
level:
root: info
org.springframework.web: debug
sql: error
root: INFO
sql: ERROR
# org.springframework.web: debug
# org.springframework.security: debug

View File

@@ -30,3 +30,6 @@
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.row p {
white-space: pre-wrap;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -8,7 +8,19 @@
<header th:insert="fragments.html::nav"></header>
<div class="container">
<h1>관리자 페이지</h1>
<p>당신은 관리자입니다.</p>
<h3>게시글 내역</h3>
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div th:each="note : ${notes}">
<p style="margin: 10px 0;">
<strong th:text="${note.title}"></strong>
Posted by
<strong th:if="${note.user}" th:text="${note.user.username}"></strong> on
<strong th:text="${#temporals.format(note.createdAt, 'yyyy-MM-dd')}"></strong>
</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Example</title>
</head>
<body>
<div class="container">
<h1>Welcome Spring Security Example</h1>
<p>당신의 이름은 <span th:text="${name}">차현우</span> 입니다.</p>
<p>당신의 나이는 <span th:text="${age}">0</span>살 입니다.</p>
<p>변수는 <span th:with="temp=${name}" th:text="${temp}"></span>입니다.</p>
<th:block th:if="${age < 30}"><p>당신은 30대가 아닙니다.</p></th:block>
<th:block th:unless="${age < 30}"><p>당신은 30대 이상입니다.</p></th:block>
<th:block th:switch="${name}">
<p th:case="정우성"> 당신은 정씨 입니다.</p>
<p th:case="감우성"> 당신은 감씨 입니다.</p>
</th:block>
<a href="/">홈으로</a>
</div>
</body>
</html>

View File

@@ -8,6 +8,7 @@
<head th:fragment="header">
<meta charset="UTF-8">
<title>스프링 시큐리티 학습용</title>
<link rel="shortcut icon" type="image/x-icon" href="/images/spring-security.png">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
@@ -16,6 +17,7 @@
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.0.6/css/all.css"
>
<link rel="stylesheet" type="text/css" href="css/signin.css">
<style>
.nav-username {
border-radius: 5px;
@@ -65,9 +67,9 @@
<a
class="nav-link active"
sec:authorize="hasAnyRole('ROLE_USER')"
th:href="@{/post}"
th:href="@{/note}"
>
게시글
개인노트
</a>
</li>
<li class="nav-item">

View File

@@ -1,9 +1,6 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<link rel="stylesheet" type="text/css" href="css/signin.css">
<head th:insert="fragments.html::header"></head>
</head>
<head th:insert="fragments.html::header"></head>
<body>
<header th:insert="fragments.html::nav"></header>
<div class="container">
@@ -17,6 +14,10 @@
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<div>
<label for="remember-me">로그인 유지하기</label>
<input type="checkbox" id="remember-me" name="remember-me" class="form-check-input mt-0" autocomplete="off">
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">로그인</button>
</form>
</div>

View File

@@ -13,14 +13,14 @@
<header th:insert="fragments.html::nav"></header>
<!-- 개인 user만 접근할 수 있는 페이지 -->
<div class="container">
<h1>게시글</h1>
<h1>개인노트</h1>
<!-- Button trigger modal -->
<button
type="button"
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#newPostModal"
data-bs-target="#newNoteModal"
data-bs-whatever="@mdo">
새 글 쓰기
</button>
@@ -28,7 +28,7 @@
<!-- Modal -->
<div
class="modal fade"
id="newPostModal"
id="newNoteModal"
tabindex="-1"
aria-labelledby="newPostModalLabel"
aria-hidden="true"
@@ -36,7 +36,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="newPostModalLabel">새 글 쓰기</h5>
<h5 class="modal-title" id="newNoteModalLabel">새 글 쓰기</h5>
<button
type="button"
class="btn-close"
@@ -45,7 +45,7 @@
</button>
</div>
<form
th:action="@{/post}"
th:action="@{/note}"
method="post"
>
<div class="modal-body">
@@ -55,7 +55,7 @@
</div>
<div class="mb-3">
<label for="content" class="col-form-label">내용</label>
<textarea class="form-control" id="content" name="content"></textarea>
<textarea class="form-control" rows="20" id="content" name="content"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -69,15 +69,15 @@
<div class="row">
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
<div class="border border-dark" th:each="post : ${posts}">
<h2 th:text="${post.title}"></h2>
<div class="border border-dark" th:each="note : ${notes}">
<h2 th:text="${note.title}"></h2>
<div>
<p th:text="${post.content}"></p>
<form th:action="@{/post}" th:method="delete">
<input type="hidden" name="id" th:value="${post.id}">
<span style="margin: 10px 0px;">Posted by
<strong th:if="${post.user}" th:text="${post.user.username}"></strong> on
<strong th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd')}"></strong>
<p th:text="${note.content}"></p>
<form th:action="@{/note}" th:method="delete">
<input type="hidden" name="id" th:value="${note.id}">
<span style="margin: 10px 0;">Posted On
<strong th:if="${note.user}" th:text="${note.user.username}"></strong> on
<strong th:text="${#temporals.format(note.createdAt, 'yyyy-MM-dd')}"></strong>
</span>
<button type="submit" class="btn btn-secondary">삭제</button>
</form>

View File

@@ -4,7 +4,12 @@
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
<head th:insert="fragments.html::header"></head>
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"></script>
<head th:insert="fragments.html::header"></head>
</head>
<body>
<header th:insert="fragments.html::nav"></header>
<div class="container">
@@ -51,7 +56,7 @@
</div>
<div class="mb-3">
<label for="content" class="col-form-label">내용</label>
<textarea class="form-control" id="content" name="content"></textarea>
<textarea class="form-control" rows="20" id="content" name="content"></textarea>
</div>
</div>
<div class="modal-footer">
@@ -71,7 +76,7 @@
<p th:text="${notice.content}"></p>
<form th:action="@{/notice}" th:method="delete">
<input type="hidden" name="id" th:value="${notice.id}">
<span style="margin: 10px 0px;">Posted At
<span style="margin: 10px 0;">Posted On
<strong th:text="${#temporals.format(notice.createdAt, 'yyyy-MM-dd')}"></strong>
</span>
<button

View File

@@ -3,10 +3,7 @@
lang="ko"
xmlns:th="http://www.thymeleaf.org"
>
<head>
<link rel="stylesheet" type="text/css" href="css/signin.css">
<head th:insert="fragments.html::header"></head>
</head>
<head th:insert="fragments.html::header"></head>
<body>
<header th:insert="fragments.html::nav"></header>
<div class="container">

View File

@@ -1,81 +0,0 @@
package com.example.springsecuritystudy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
class SampleControllerTest {
@Autowired
private WebApplicationContext applicationContext;
private MockMvc mvc;
@BeforeEach
public void setUp() {
mvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.alwaysDo(print())
.build();
}
@Test
void example() throws Exception {
mvc.perform(
get("/example")
).andExpect(status().isOk());
}
@Test
void login_user() throws Exception {
mvc.perform(
formLogin("/login")
.user("user")
.password("user")
).andExpect(status().is3xxRedirection());
}
@Test
void login_admin() throws Exception {
mvc.perform(
formLogin("/login")
.user("admin")
.password("admin")
).andExpect(status().is3xxRedirection());
}
@Test
@WithMockUser
void access_user() throws Exception {
mvc.perform(
get("/user")
).andExpect(status().isOk());
}
@Test
@WithMockAdmin
void access_admin() throws Exception {
mvc.perform(
get("/admin")
).andExpect(status().isOk());
}
@Test
@WithMockUser
void access_denied() throws Exception {
mvc.perform(
get("/admin")
).andExpect(status().isForbidden());
}
}

View File

@@ -0,0 +1,61 @@
package com.example.springsecuritystudy.admin;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.example.springsecuritystudy.helper.TestConfig;
import com.example.springsecuritystudy.user.User;
import com.example.springsecuritystudy.user.UserRepository;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class AdminControllerTest extends TestConfig {
@Autowired
private UserRepository userRepository;
private MockMvc mvc;
private User user;
private User admin;
@BeforeEach
void setUp(@Autowired WebApplicationContext applicationContext) {
this.mvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.alwaysDo(print())
.build();
user = userRepository.save(new User("user", "user", "ROLE_USER"));
admin = userRepository.save(new User("admin", "admin", "ROLE_ADMIN"));
}
@Test
void getNoteForAdmin_인증없음() throws Exception {
mvc.perform(
get("/admin").with(csrf())
).andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
@Test
void getNoteForAdmin_어드민인증있음() throws Exception {
mvc.perform(
get("/admin").with(csrf()).with(user(admin))
).andExpect(status().is2xxSuccessful());
}
@Test
void getNoteForAdmin_유저인증있음() throws Exception {
mvc.perform(
get("/admin").with(csrf()).with(user(user))
).andExpect(status().isForbidden());
}
}

View File

@@ -0,0 +1,11 @@
package com.example.springsecuritystudy.helper;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@ActiveProfiles(value = "test")
@Transactional
public class TestConfig {
}

View File

@@ -1,4 +1,4 @@
package com.example.springsecuritystudy;
package com.example.springsecuritystudy.helper;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

View File

@@ -0,0 +1,154 @@
package com.example.springsecuritystudy.note;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.TestExecutionEvent;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.example.springsecuritystudy.helper.TestConfig;
import com.example.springsecuritystudy.user.User;
import com.example.springsecuritystudy.user.UserRepository;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class NoteControllerTest extends TestConfig {
@Autowired
private UserRepository userRepository;
@Autowired
private NoteRepository noteRepository;
private MockMvc mvc;
private User user;
private User admin;
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.alwaysDo(print())
.build();
User user2 = User.builder()
.username("user123")
.password("user")
.authority("ROLE_USER")
.build();
User admin2 = User.builder()
.username("admin123")
.password("admin")
.authority("ROLE_ADMIN")
.build();
this.user = userRepository.save(user2);
this.admin = userRepository.save(admin2);
}
@Test
void getNote_인증없음() throws Exception {
mvc.perform(
get("/note")
).andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
@Test
//WithUserDetails 로 테스트 하는 방법
@WithUserDetails(
value = "user123", // userDetailsService를 통해 가져올 수 있는 유저
userDetailsServiceBeanName = "userDetailServiceImpl", // UserDetailsService 구현체의 Bean
setupBefore = TestExecutionEvent.TEST_EXECUTION // 테스트 실행 직전에 유저를 가져온다.
)
void getNote_인증있음() throws Exception {
mvc.perform(
get("/note")
).andExpect(status().isOk())
.andExpect(view().name("note/index"))
.andDo(print());
}
@Test
void postNote_인증없음() throws Exception {
mvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
@Test
@WithUserDetails(
value = "admin123",
userDetailsServiceBeanName = "userDetailServiceImpl",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void postNote_어드민인증있음() throws Exception {
mvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden());
}
@Test
@WithUserDetails(
value = "user123",
userDetailsServiceBeanName = "userDetailServiceImpl",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void postNote_유저인증있음() throws Exception {
mvc.perform(
post("/note").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrl("note"))
.andExpect(status().is3xxRedirection());
}
@Test
void deleteNote_인증없음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mvc.perform(
delete("/note?id=" + note.getId()).with(csrf())
).andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
@Test
@WithUserDetails(
value = "user123",
userDetailsServiceBeanName = "userDetailServiceImpl",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void deleteNote_인증있음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mvc.perform(
delete("/note?id=" + note.getId()).with(csrf())
).andExpect(redirectedUrl("note"))
.andExpect(status().is3xxRedirection());
}
@Test
@WithUserDetails(
value = "admin123",
userDetailsServiceBeanName = "userDetailServiceImpl",
setupBefore = TestExecutionEvent.TEST_EXECUTION
)
void deleteNote_어드민계정있음() throws Exception {
Note note = noteRepository.save(new Note("제목", "내용", user));
mvc.perform(
delete("/note?id=" + note.getId()).with(csrf())
).andExpect(status().isForbidden());
}
}

View File

@@ -0,0 +1,96 @@
package com.example.springsecuritystudy.note;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.springsecuritystudy.helper.TestConfig;
import com.example.springsecuritystudy.user.User;
import com.example.springsecuritystudy.user.UserService;
import static org.assertj.core.api.BDDAssertions.*;
class NoteServiceTest extends TestConfig {
@Autowired
private NoteService noteService;
@Autowired
private UserService userService;
@Test
void findByUser_유저가_게시물조회() {
//given
User user = userService.signup("user", "user");
noteService.saveNote(user, "title1", "content1");
noteService.saveNote(user, "title2", "content2");
//when
List<Note> notes = noteService.findByUser(user);
//then
then(notes.size()).isEqualTo(2);
Note note1 = notes.get(0);
Note note2 = notes.get(1);
// post1 = title2
then(note1.getUser().getUsername()).isEqualTo("user");
then(note1.getTitle()).isEqualTo("title2");
then(note1.getContent()).isEqualTo("content2");
// post2 = title1
then(note2.getUser().getUsername()).isEqualTo("user");
then(note2.getTitle()).isEqualTo("title1");
then(note2.getContent()).isEqualTo("content1");
}
@Test
void findByUser_어드민이_조회() {
// given
User admin = userService.signupAdmin("admin", "admin");
User user1 = userService.signup("user1", "user1");
User user2 = userService.signup("user2", "user2");
noteService.saveNote(user1, "title1", "content1");
noteService.saveNote(user1, "title2", "content2");
noteService.saveNote(user2, "title3", "content3");
// when
List<Note> notes = noteService.findByUser(admin);
// then
then(notes.size()).isEqualTo(3);
Note note1 = notes.get(0);
Note note2 = notes.get(1);
Note note3 = notes.get(2);
// post1 = title3
then(note1.getUser().getUsername()).isEqualTo("user2");
then(note1.getTitle()).isEqualTo("title3");
then(note1.getContent()).isEqualTo("content3");
// post1 = title2
then(note2.getUser().getUsername()).isEqualTo("user1");
then(note2.getTitle()).isEqualTo("title2");
then(note2.getContent()).isEqualTo("content2");
// post1 = title1
then(note3.getUser().getUsername()).isEqualTo("user1");
then(note3.getTitle()).isEqualTo("title1");
then(note3.getContent()).isEqualTo("content1");
}
@Test
void savePost() {
// given
User user = userService.signup("user", "user");
// when
noteService.saveNote(user, "title", "content");
// then
then(noteService.findByUser(user).size()).isEqualTo(1);
}
@Test
void deletePost() {
// given
User user = userService.signup("user", "user");
Note note = noteService.saveNote(user, "title", "content");
// when
noteService.deleteNote(user, note.getId());
// then
then(noteService.findByUser(user).size()).isZero();
}
}

View File

@@ -0,0 +1,109 @@
package com.example.springsecuritystudy.notice;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.example.springsecuritystudy.helper.TestConfig;
import com.example.springsecuritystudy.helper.WithMockAdmin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class NoticeControllerTest extends TestConfig {
@Autowired
private NoticeRepository noticeRepository;
private MockMvc mvc;
@BeforeEach
public void setUp(@Autowired WebApplicationContext applicationContext) {
this.mvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.alwaysDo(print())
.build();
}
@Test
void getNotice_인증없음() throws Exception {
mvc.perform(get("/notice"))
.andExpect(redirectedUrlPattern("**/login"))
.andExpect(status().is3xxRedirection());
}
@Test
@WithMockUser
void getNotice_인증있음() throws Exception {
mvc.perform(get("/notice"))
.andExpect(status().isOk())
.andExpect(view().name("notice/index"));
}
@Test
void postNotice_인증없음() throws Exception {
mvc.perform(post("/notice")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "USER", username = "user", password = "user")
void postNotice_유저인증있음() throws Exception {
mvc.perform(post("/notice")
.with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(status().isForbidden());
}
@Test
@WithMockAdmin
void postNotice_어드민인증있음() throws Exception {
mvc.perform(post("/notice")
.with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("title", "제목")
.param("content", "내용")
).andExpect(redirectedUrl("notice"))
.andExpect(status().is3xxRedirection());
}
@Test
void deleteNotice_인증없음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mvc.perform(delete("/notice?id=" + notice.getId()))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "USER", username = "user", password = "user")
void deleteNotice_유저인증있음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mvc.perform(delete("/notice?id=" + notice.getId())
.with(csrf()))
.andExpect(status().isForbidden());
}
@Test
@WithMockAdmin
void deleteNotice_어드민인증있음() throws Exception {
Notice notice = noticeRepository.save(new Notice("제목", "내용"));
mvc.perform(delete("/notice?id=" + notice.getId())
.with(csrf()))
.andExpect(redirectedUrl("notice"))
.andExpect(status().is3xxRedirection());
}
}

View File

@@ -0,0 +1,41 @@
package com.example.springsecuritystudy.user;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.example.springsecuritystudy.helper.TestConfig;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class UserControllerTest extends TestConfig {
private MockMvc mvc;
@BeforeEach
void setUp(@Autowired WebApplicationContext context) {
this.mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.alwaysDo(print())
.build();
}
@Test
void signup() throws Exception {
mvc.perform(
post("/signup").with(csrf())
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("username", "user123")
.param("password", "password")
).andExpect(redirectedUrl("login"))
.andExpect(status().is3xxRedirection());
}
}

View File

@@ -0,0 +1,72 @@
package com.example.springsecuritystudy.user;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.springsecuritystudy.helper.TestConfig;
import static org.assertj.core.api.BDDAssertions.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest extends TestConfig {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void signup() {
//given
String username = "user123";
String password = "password";
//when
User user = userService.signup(username, password);
//then
then(user.getId()).isNotNull();
then(user.getUsername()).isEqualTo(username);
then(user.getPassword()).startsWith("{bcrypt}");
then(user.getAuthorities()).hasSize(1);
then(user.getAuthorities().stream().findFirst().get().getAuthority()).isEqualTo("ROLE_USER");
then(user.isAdmin()).isFalse();
then(user.isAccountNonExpired()).isTrue();
then(user.isAccountNonLocked()).isTrue();
then(user.isEnabled()).isTrue();
then(user.isCredentialsNonExpired()).isTrue();
}
@Test
void signupAdmin() {
//given
String username = "admin123";
String password = "password";
//when
User user = userService.signupAdmin(username, password);
//then
then(user.getId()).isNotNull();
then(user.getUsername()).isEqualTo(username);
then(user.getPassword()).startsWith("{bcrypt}");
then(user.getAuthorities()).hasSize(1);
then(user.getAuthorities().stream().findFirst().get().getAuthority()).isEqualTo("ROLE_ADMIN");
then(user.isAdmin()).isTrue();
then(user.isAccountNonExpired()).isTrue();
then(user.isAccountNonLocked()).isTrue();
then(user.isEnabled()).isTrue();
then(user.isCredentialsNonExpired()).isTrue();
}
@Test
void findByUsername() {
//given
User user = User.builder()
.username("user123")
.password("password")
.authority("ROLE_USER")
.build();
userRepository.save(user);
//when
User savedUser = userService.findByUsername("user123");
//then
then(savedUser.getId()).isNotNull();
}
}