Compare commits
24 Commits
default_co
...
jwt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cab42f9b6 | ||
|
|
35be1538f6 | ||
|
|
e8d3336225 | ||
|
|
eb7f9974a2 | ||
|
|
09f1e3d07f | ||
|
|
4f13595479 | ||
|
|
26ef372734 | ||
|
|
a3c9c60d9e | ||
|
|
4c0c60a69f | ||
|
|
432e0aff08 | ||
|
|
b209a017d4 | ||
|
|
d798b23d63 | ||
|
|
8c26d488fc | ||
|
|
f6d702da7f | ||
|
|
b7d58486ac | ||
|
|
3f5f5cbc4b | ||
|
|
110324776d | ||
|
|
584d7f1b94 | ||
|
|
20ccbd7e13 | ||
|
|
ad10828483 | ||
|
|
de564ef4fe | ||
|
|
b5c06cf76f | ||
|
|
d4f032ad32 | ||
|
|
b531d2ac0e |
18
build.gradle
18
build.gradle
@@ -19,17 +19,31 @@ 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'
|
||||
// jjwt
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
|
||||
// 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'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.springsecuritystudy.common;
|
||||
|
||||
/**
|
||||
* 이미 등록된 유저를 재등록하려고 할때 발생하는 Exception
|
||||
*/
|
||||
public class AlreadyRegisteredUserException extends RuntimeException {
|
||||
|
||||
public AlreadyRegisteredUserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AlreadyRegisteredUserException() {
|
||||
super("이미 등록된 유저입니다.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.springsecuritystudy.common;
|
||||
|
||||
/**
|
||||
* 유저를 찾을 수 없을 때 발생하는 Exception
|
||||
*/
|
||||
public class UserNotFoundException extends RuntimeException {
|
||||
|
||||
public UserNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UserNotFoundException() {
|
||||
super("유저를 찾을 수 없습니다.");
|
||||
}
|
||||
}
|
||||
@@ -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* 본인 외에는 게시글을 볼 수 없습니다.");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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(){
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -1,32 +1,71 @@
|
||||
package com.example.springsecuritystudy.config;
|
||||
|
||||
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.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import com.example.springsecuritystudy.filter.StopwatchFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtAuthenticationFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtAuthorizationFilter;
|
||||
import com.example.springsecuritystudy.jwt.JwtProperties;
|
||||
import com.example.springsecuritystudy.jwt.JwtUtils;
|
||||
import com.example.springsecuritystudy.user.UserRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@EnableWebSecurity
|
||||
/**
|
||||
* Security 설정 Config
|
||||
*/
|
||||
@EnableWebSecurity(debug = true)
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
@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
|
||||
);
|
||||
// JWT filter
|
||||
http.addFilterBefore(
|
||||
new JwtAuthenticationFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class))
|
||||
, jwtUtils),
|
||||
UsernamePasswordAuthenticationFilter.class
|
||||
).addFilterBefore(
|
||||
new JwtAuthorizationFilter(userRepository, jwtUtils),
|
||||
BasicAuthenticationFilter.class
|
||||
);
|
||||
http
|
||||
.httpBasic().disable()
|
||||
.csrf().disable();
|
||||
http
|
||||
.rememberMe().disable()
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
|
||||
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")
|
||||
@@ -34,14 +73,14 @@ public class SecurityConfig {
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/")
|
||||
.defaultSuccessUrl("/", false)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
// .logoutUrl("/logout") // post 방식으로만 동작
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // get 방식으로도 동작
|
||||
.logoutSuccessUrl("/")
|
||||
.deleteCookies("JSESSIONID")
|
||||
.deleteCookies(JwtProperties.COOKIE_NAME)
|
||||
.invalidateHttpSession(true)
|
||||
);
|
||||
|
||||
@@ -50,12 +89,12 @@ public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return web -> web.ignoring().antMatchers("/css/**", "/js/**", "/h2-console/**");
|
||||
// 정적 리소스 spring security 대상에서 제외
|
||||
return (web) -> web.ignoring()
|
||||
.requestMatchers(
|
||||
PathRequest.toStaticResources().atCommonLocations(),
|
||||
PathRequest.toH2Console()
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
return username -> userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("유저를 찾지 못 했습니다."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
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.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
|
||||
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
|
||||
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
public JwtAuthenticationFilter(
|
||||
AuthenticationManager authenticationManager, JwtUtils jwtUtils) {
|
||||
super(authenticationManager);
|
||||
this.jwtUtils = jwtUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 인증 시도
|
||||
*/
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
|
||||
AuthenticationException {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
request.getParameter("username"),
|
||||
request.getParameter("password"),
|
||||
new ArrayList<>()
|
||||
);
|
||||
return getAuthenticationManager().authenticate(authenticationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증에 성공했을 때 사용
|
||||
* JWT Token을 생성해서 쿠키에 넣는다.
|
||||
*/
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||
Authentication authResult) throws IOException, ServletException {
|
||||
User user = (User)authResult.getPrincipal();
|
||||
String token = jwtUtils.createToken(user);
|
||||
// 쿠키 생성
|
||||
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, token);
|
||||
cookie.setMaxAge(JwtProperties.EXPIRATION_TIME); // 쿠키 만료 시간
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException failed) throws IOException, ServletException {
|
||||
response.sendRedirect("/login");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.example.springsecuritystudy.common.UserNotFoundException;
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
import com.example.springsecuritystudy.user.UserRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT를 이용한 인증
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthorizationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = null;
|
||||
try {
|
||||
// cookie에서 JWT token을 가져온다.
|
||||
token = Arrays.stream(request.getCookies())
|
||||
.filter(cookie -> cookie.getName().equals(JwtProperties.COOKIE_NAME)).findFirst()
|
||||
.map(Cookie::getValue)
|
||||
.orElse(null);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (token != null) {
|
||||
try {
|
||||
Authentication authentication = getUsernamePasswordAuthenticationToken(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (Exception e) {
|
||||
Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, null);
|
||||
cookie.setMaxAge(0);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private Authentication getUsernamePasswordAuthenticationToken(String token) {
|
||||
String username = jwtUtils.getUsername(token);
|
||||
if (username != null) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow(UserNotFoundException::new);
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
user,
|
||||
null,
|
||||
user.getAuthorities()
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import javafx.util.Pair;
|
||||
|
||||
/**
|
||||
* JWT Key를 제공하고 조회한다.
|
||||
* Key Rolling을 지원한다.
|
||||
*/
|
||||
@Component
|
||||
public class JwtKey {
|
||||
|
||||
private final Environment env;
|
||||
|
||||
public JwtKey(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
private Map<String, String> SECRET_KEY_SET;
|
||||
private String[] KID_SET;
|
||||
private Random randomIndex;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
SECRET_KEY_SET = new HashMap<String, String>() {
|
||||
{
|
||||
put("key1", env.getProperty("jwt.secret-key1"));
|
||||
put("key2", env.getProperty("jwt.secret-key2"));
|
||||
put("key3", env.getProperty("jwt.secret-key3"));
|
||||
}
|
||||
};
|
||||
KID_SET = SECRET_KEY_SET.keySet().toArray(new String[0]);
|
||||
randomIndex = new Random();
|
||||
}
|
||||
|
||||
/**
|
||||
* SECRET_KEY_SET 에서 랜덤한 KEY 가져오기
|
||||
*
|
||||
* @return kid와 key Pair
|
||||
*/
|
||||
public Pair<String, Key> getRandomKey() {
|
||||
String kid = KID_SET[randomIndex.nextInt(KID_SET.length)];
|
||||
String secretKey = SECRET_KEY_SET.get(kid);
|
||||
return new Pair<>(kid, Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
/**
|
||||
* kid로 Key찾기
|
||||
*
|
||||
* @param kid kid
|
||||
* @return Key
|
||||
*/
|
||||
public Key getKey(String kid) {
|
||||
String key = SECRET_KEY_SET.getOrDefault(kid, null);
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
/**
|
||||
* JWT 기본 설정값
|
||||
*/
|
||||
public class JwtProperties {
|
||||
public static final int EXPIRATION_TIME = 600000; // 10분
|
||||
public static final String COOKIE_NAME = "JWT";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.example.springsecuritystudy.user.User;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import javafx.util.Pair;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtUtils {
|
||||
private final JwtKey jwtKey;
|
||||
|
||||
/**
|
||||
* 토큰에서 username 찾기
|
||||
*
|
||||
* @param token 토큰
|
||||
* @return username
|
||||
*/
|
||||
public String getUsername(String token) {
|
||||
// jwtToken에서 username을 찾는다.
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKeyResolver(new SigningKeyResolver(jwtKey))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody()
|
||||
.getSubject(); // 토큰에 담긴 정보에서 username을 가져온다.
|
||||
}
|
||||
|
||||
/**
|
||||
* user로 토큰 생성
|
||||
* HEADER : alg, kid
|
||||
* PAYLOAD : sub, iat, exp
|
||||
* SIGNATURE : JwtKey.getRandomKey로 구한 Secret Key로 HS512 해시
|
||||
*
|
||||
* @param user 유저
|
||||
* @return jwt token
|
||||
*/
|
||||
public String createToken(User user) {
|
||||
Claims claims = Jwts.claims().setSubject(user.getUsername());
|
||||
Date now = new Date();
|
||||
Pair<String, Key> key = jwtKey.getRandomKey();
|
||||
return Jwts.builder()
|
||||
.setClaims(claims) // 토큰에 담을 정보 설정
|
||||
.setIssuedAt(now) // 토큰 발행 시간 설정
|
||||
.setExpiration(new Date(now.getTime() + JwtProperties.EXPIRATION_TIME)) // 토큰 만료 시간 설정
|
||||
.setHeaderParam(JwsHeader.KEY_ID, key.getKey()) // 토큰에 kid 설정
|
||||
.signWith(key.getValue()) // signature 생성
|
||||
.compact();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.SigningKeyResolverAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* JwsHeader를 통해 Signature 검증에 필요한 Key를 가져오는 코드를 구현합니다.
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class SigningKeyResolver extends SigningKeyResolverAdapter {
|
||||
private final JwtKey jwtKey;
|
||||
|
||||
@Override
|
||||
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
|
||||
String kid = jwsHeader.getKeyId();
|
||||
if (kid == null)
|
||||
return null;
|
||||
return jwtKey.getKey(kid);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.example.springsecuritystudy.post;
|
||||
|
||||
public enum PostStatus {
|
||||
Y,
|
||||
N
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,3 +13,10 @@ spring:
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
show-sql: true
|
||||
|
||||
jwt:
|
||||
secret-key1: SpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFunSpringSecurityJWTPracticeProjectIsSoGoodAndThisProjectIsSoFun
|
||||
secret-key2: GoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurityGoodSpringSecurityNiceSpringSecurity
|
||||
secret-key3: HelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurityHelloSpringSecurity
|
||||
expiration-time: 86400 #60 * 60 * 24
|
||||
remember-me-expiration-time: 2592000 #60 * 60 * 24 * 30
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,3 +30,6 @@
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.row p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
BIN
src/main/resources/static/images/spring-security.png
Normal file
BIN
src/main/resources/static/images/spring-security.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.example.springsecuritystudy;
|
||||
package com.example.springsecuritystudy.helper;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.springsecuritystudy.jwt;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
|
||||
class JwtUtilsTest {
|
||||
|
||||
String secretKey = "SecretKeyToGenJWTsSecretKeyToGenJWTsSecretKeyToGenJWTs";
|
||||
|
||||
private void printToken(String token) {
|
||||
System.out.println("token: " + token);
|
||||
System.out.println("header: " + decodeToken(token.split("\\.")[0]));
|
||||
System.out.println("payload: " + decodeToken(token.split("\\.")[1]));
|
||||
}
|
||||
|
||||
private String decodeToken(String token) {
|
||||
return new String(Base64Utils.decodeFromString(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_okta_token() {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("sub", "test");
|
||||
claims.put("name", "dalichoi");
|
||||
claims.put("admin", "true");
|
||||
claims.put("exp", Instant.now().plusSeconds(60*60*24).getEpochSecond());
|
||||
claims.put("iat", Instant.now().getEpochSecond());
|
||||
|
||||
String oktaToken = Jwts.builder()
|
||||
// .setHeaderParam("typ", "JWT")
|
||||
// .setHeaderParam("alg", "HS256")
|
||||
// .setHeaderParam("kid", "key1")
|
||||
// .setSubject("test")
|
||||
// .setIssuedAt(java.util.Date.from(Instant.now()))
|
||||
// .setExpiration(java.util.Date.from(Instant.now().plusSeconds(60*60*24)))
|
||||
.addClaims(claims)
|
||||
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
printToken(oktaToken);
|
||||
|
||||
Jws<Claims> claimsJws = Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
|
||||
.build()
|
||||
.parseClaimsJws(oktaToken);
|
||||
System.out.println("claimsJws: " + claimsJws);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user