24 Commits

Author SHA1 Message Date
Daeil Choi
7cab42f9b6 Modify h2console ignoring 방식 수정 2023-02-14 12:30:42 +09:00
Daeil Choi
35be1538f6 Modify login success alwaysUse false 2023-02-13 17:25:27 +09:00
Daeil Choi
e8d3336225 Add jjwt 라이브러리 테스트 2023-02-09 16:11:06 +09:00
Daeil Choi
eb7f9974a2 Modify 비밀키 환경변수로 숨김 처리 2023-02-08 12:20:14 +09:00
Daeil Choi
09f1e3d07f Add JWT 토큰 인증 방식으로 변경, JWT 커스텀 필터 추가 2023-02-07 17:18:41 +09:00
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
55 changed files with 1388 additions and 290 deletions

View File

@@ -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'
}

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,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("유저를 찾지 못 했습니다."));
}
}

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

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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";
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

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

@@ -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

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,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);
}
}

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();
}
}