layout 의존성 추가.

master
강석 최 2021-11-24 18:27:36 +09:00
parent af707c262a
commit a22bf73dba
21 changed files with 378 additions and 112 deletions

View File

@ -19,18 +19,19 @@ repositories {
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
developmentOnly 'org.springframework.boot:spring-boot-devtools:2.5.6'
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.6'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:2.5.6'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.5.6'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '2.5.3'
implementation 'org.springframework.boot:spring-boot-starter-security:2.5.6'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
// implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.7.0'
implementation group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.7.4'
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
// implementation group: 'org.webjars', name: 'bootstrap', version: '5.1.3'

View File

@ -6,8 +6,9 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class BaseController {
/** * 메인 페이지 이동 * @return */
@GetMapping("/")
public String home(){
return "login";
public String main() {
return "index";
}
}

View File

@ -1,27 +0,0 @@
package com.dbnt.kcgfilemanager.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/static/", "classpath:/public/", "classpath:/", "classpath:/resources/",
"classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/"};
@Override
public void addViewControllers(ViewControllerRegistry registry){
registry.addViewController("/").setViewName("forward:/index");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
}
}

View File

@ -0,0 +1,13 @@
package com.dbnt.kcgfilemanager.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum Role {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
private String value;
}

View File

@ -0,0 +1,57 @@
package com.dbnt.kcgfilemanager.config;
import com.dbnt.kcgfilemanager.userInfo.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserInfoService userInfoService;
@Bean
public PasswordEncoder passwordEncoder(){
return new Pbkdf2PasswordEncoder();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/img/**", "/js/**", "/lib/**", "/vendor/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 페이지 권한 설정
.antMatchers("/info").hasRole("MEMBER") // MEMBER, ADMIN만 접근 허용
.antMatchers("/admin").hasRole("ADMIN") // ADMIN만 접근 허용
.antMatchers("/**").permitAll() // 그외 모든 경로에 대해서는 권한 없이 접근 허용
// .anyRequest().authenticated() // 나머지 요청들은 권한의 종류에 상관 없이 권한이 있어야 접근 가능
.and() // 로그인 설정
.formLogin() .loginPage("/user/login") // Custom login form 사용
.failureUrl("/login-error") // 로그인 실패 시 이동
.defaultSuccessUrl("/") // 로그인 성공 시 redirect 이동
.and() // 로그아웃 설정
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // 로그아웃 시 URL 재정의
.logoutSuccessUrl("/") // 로그아웃 성공 시 redirect 이동
.invalidateHttpSession(true) // HTTP Session 초기화
.deleteCookies("JSESSIONID") // 특정 쿠키 제거
.and() // 403 예외처리 핸들링
.exceptionHandling().accessDeniedPage("/denied");
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoder());
}
}

View File

@ -0,0 +1,48 @@
package com.dbnt.kcgfilemanager.config;
import lombok.RequiredArgsConstructor;
import nz.net.ultraq.thymeleaf.LayoutDialect;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
@Configuration
@RequiredArgsConstructor
public class ThymeleafViewResolverConfig {
private final ApplicationContext applicationContext;
@Bean
public SpringResourceTemplateResolver templateResolver(){
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("classpath:templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(false);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.addDialect(new SpringSecurityDialect());
templateEngine.addDialect(new LayoutDialect());
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setOrder(0);
return viewResolver;
}
}

View File

@ -1,47 +0,0 @@
package com.dbnt.kcgfilemanager.config;
import com.dbnt.kcgfilemanager.userInfo.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserInfoService userInfoService;
@Bean
public PasswordEncoder passwordEncoder(){
return new Pbkdf2PasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userInfoService);
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/authenticate")
.permitAll().anyRequest().authenticated()
.and().exceptionHandling().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

View File

@ -1,8 +1,6 @@
package com.dbnt.kcgfilemanager.userInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@ -13,31 +11,60 @@ import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Data
@AllArgsConstructor
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "USER_INFO")
public class UserInfo{
public class UserInfo implements UserDetails{
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "user_seq", nullable = false)
private Integer userSeq;
@Column(name = "USER_ID")
private String userId;
@Column(name = "PASSWORD")
private String password;
@Column(name = "NAME")
private String name;
@Column(name = "POSITION")
private int position;
@Column(name = "DEPARTMENT")
private int department;
@Column(name = "USER_ROLE")
private String userRole;
@Column(name = "CREATE_DATE")
private Date createDate;
public Integer getUserSeq() {
return userSeq;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
for (String role : userRole.split(",")) {
roles.add(new SimpleGrantedAuthority(role));
}
return roles;
}
public void setUserSeq(Integer userSeq) {
this.userSeq = userSeq;
@Override
public String getUsername() {
return userId;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@ -6,26 +6,59 @@ import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RequiredArgsConstructor
@Controller
@RequiredArgsConstructor
public class UserInfoController {
private final UserInfoService userInfoService;
@PostMapping("/user")
public String signup(UserInfo userInfo){
userInfoService.signup(userInfo);
return "redirect:/login";
/** * 로그인 페이지 이동 * @return */
@GetMapping("/user/login")
public String goLogin() {
return "login/login";
}
@GetMapping("/logout")
public String logoutPage(HttpServletRequest request, HttpServletResponse response){
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
/** * 로그인 에러 * @param model * @return */
@GetMapping("/login-error")
public String loginError(Model model) {
model.addAttribute("loginError", true);
return "/login/login";
}
/** * 회원가입 페이지 이동 * @return */
@GetMapping("/signup")
public String goSignup() {
return "login/signup";
}
/** * 회원가입 처리 * @param memberDto * @return */
@PostMapping("/signup")
public String signup(UserInfo userInfo) {
userInfoService.signup(userInfo);
return "redirect:/user/login";
}
/** * 접근 거부 페이지 이동 * @return */
@GetMapping("/denied")
public String doDenied() {
return "login/denied";
}
/** * 내 정보 페이지 이동 * @return */
@GetMapping("/info")
public String goMyInfo() {
return "login/myinfo";
}
/** * Admin 페이지 이동 * @return */
@GetMapping("/admin")
public String goAdmin() {
return "login/admin";
}
}

View File

@ -19,16 +19,15 @@ public class UserInfoService implements UserDetailsService {
private final UserInfoRepository userInfoRepository;
@Transactional
public int signup(UserInfo userInfo){
public String signup(UserInfo userInfo){
Pbkdf2PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder();
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
return userInfoRepository.save(userInfo).getUserSeq();
return userInfoRepository.save(userInfo).getUserId();
}
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
UserInfo userInfo = userInfoRepository.findByUserId(userId).orElseThrow(()->new UsernameNotFoundException(userId));
return new User(userInfo.getUserId(), userInfo.getPassword(), new ArrayList<>());
return userInfoRepository.findByUserId(userId).orElseThrow(() -> new UsernameNotFoundException(userId));
}
}

View File

@ -1,3 +1,4 @@
spring.jpa.show-sql=true
spring.jpa.generate-ddl=false

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<footer th:fragment="footerFragment">
<div style="border: 1px solid gold">
Footer영역입니다.
</div>
</footer>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<header th:fragment="headerFragment">
<div style="border: 1px solid green">
header.html 입니다.
</div>
</header>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="leftMenuFragment">
<div style="border: 1px solid black">
menu 영역입니다.
</div>
</div>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="ko"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<div layout:fragment="content">
<h1>This is Main Page.</h1>
<hr/>
<div sec:authorize="isAuthenticated()">
<span sec:authentication="name"></span>님 환영합니다.
</div>
<!-- 익명의 사용자 -->
<a sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
<a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
<!-- 인증된 사용자 -->
<a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
<!-- 특정 권한의 사용자 -->
<a sec:authorize="hasRole('ROLE_MEMBER')" th:href="@{/info}">내정보</a>
<a sec:authorize="hasRole('ROLE_ADMIN')" th:href="@{/admin}">어드민</a>
</div>
</html>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8" />
<title>해양경찰청 파일관리 시스템</title>
<!-- 공통으로 쓰이는 css파일을넣는다.-->
<!--bootstrap-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- 컨텐츠페이지의 CSS 영역이 들어감 -->
<th:block layout:fragment="css"></th:block>
<!-- 공통으로 쓰이는 js파일을넣는다.-->
<!--bootstrap-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js" integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
<!-- 컨텐츠페이지의 스크립트 영역이 들어감 -->
<th:block layout:fragment="script"></th:block>
</head>
<header th:replace="fragments/header :: headerFragment"></header>
<div sec:authorize="isAnonymous()">
<div layout:fragment="content"></div>
</div>
<div sec:authorize="isAuthenticated()" class="row">
<div class="col-3">
<div th:replace="fragments/leftMenu :: leftMenuFragment"></div>
</div>
<div class="col-9">
<div layout:fragment="content"></div>
</div>
</div>
<footer th:replace="fragments/footer :: footerFragment"></footer>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Admin</title>
</head>
<body>
<h1>This is Admin Page.</h1>
<hr />
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>denied</title>
</head>
<body>
<h1>This is denied Page.</h1>
<hr />
</body>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>Login</title>
</head>
<body>
<h1>This is Login Page.</h1>
<hr />
<!-- Security config의 loginPage("url")와 action url과 동일하게 작성-->
<form action="/user/login" method="post">
<!-- Spring Security가 적용되면 POST 방식으로 보내는 모든 데이터는 csrf 토큰 값이 필요 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<p th:if="${loginError}" class="error">Wrong user or password</p>
<!-- 로그인 시 아이디의 name 애트리뷰트 값은 username -->
<!-- 파라미터명을 변경하고 싶을 경우 config class formlogin()에서 .usernameParameter("") 명시 -->
<input type="text" name="username" placeholder="이메일 입력해주세요" />
<input type="password" name="password" placeholder="비밀번호" />
<button type="submit">로그인</button>
</form>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>MyInfo</title>
</head>
<body>
<h1>This is MyInfo Page.</h1>
<hr />
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title>Signup</title>
</head>
<body>
<h1>This is Signup Page.</h1>
<hr />
<form th:action="@{/signup}" method="post">
<input type="text" name="email" placeholder="이메일 입력해주세요" />
<input type="password" name="password" placeholder="비밀번호" />
<input type="radio" name="auth" value="ROLE_ADMIN,ROLE_MEMBER" /> admin
<input type="radio" name="auth" value="ROLE_MEMBER" checked="checked" />
member <br />
<button type="submit">가입하기</button>
</form>
</body>
</html>