일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- jscalendar
- paging
- jQuery값전달
- jQuery값전송
- 제네릭
- JPQL
- 제너릭
- namedQuery
- javaservlet
- 페이징
- calendar
- fetchjoin
- 엔티티직접사용
- Hibernate
- LIST
- springflow
- JQuery
- 자바서블릿
- fullcalendar
- values()
- javascriptcalendar
- 스프링데이터흐름
- 대량쿼리
- jQueryUI
- Generic
- 벌크연산
- joinfetch
- JPA
- 페치조인
- 프로젝트생성
- Today
- Total
가자공부하러!
Spring Security 활용 회원 관리 (1) - 개요, 환경설정, DB 모델링 본문
1. Spring Security 개요
1. 기본 동작 방식
2. 환경설정
1. 개발환경 :
> spring boot 2.1.7(Maven, JSP, Tiles)
> jdk 8
> sts3
2. 설정 순서
> 기본 환경 설정
- pom.xml 작성
- config 파일 작성 : src/main/java/com/exam/app/config/
- MyBatis mapper 작성 : src/main/resources/mapper/
- application.yml 작성 : src/main/resources/
- 컨트롤러 작성 : src/main/java/com/exam/app/common/controller/
> DB 테이블 생성
- SEC_MEMBER, SEC_AUTHORITY
> WebSecurityConfigurerAdapter를 상속받는 파일 작성
> AuthenticationProvider 구현
3. 책 내용 참고
> 소스코드 : https://github.com/HyeongJunMin/springboot/tree/master/SecurityExamWithBook
> 기본 환경 설정
- pom.xml 작성
- config 파일 작성 : src/main/java/com/exam/app/config/
- MyBatis mapper 작성 : src/main/resources/mapper/
- application.yml 작성 : src/main/resources/
- 컨트롤러 작성 : src/main/java/com/exam/app/common/controller/
- URI 설계
- /exam/all : 모든 사용자 접근 가능
- /exam/member : 멤버만 접근 가능
- /exam/admin : 관리자 권한이 있는 사용자만 접근 가능
> DB 테이블 생성
- SEC_MEMBER, SEC_AUTHORITY
> AuthenticationSuccessHandler를 구현하는 파일 작성
- src/main/java/com/exam/app/config/CustomLoginSuccessHandler.java
- 로그인 처리를 수행하기 위함
> AbstractSecurityWebApplicationInitializer를 상속받는 파일 작성
- src/main/java/com/exam/app/config/SecurityInitializer.java
- DelegatingFilterProxy를 스프링에 등록하는 설정
> WebSecurityConfigurerAdapter를 상속받는 파일 작성
- src/main/java/com/exam/app/config/SpringSecurityConfiguration.java
- security-context.xml 역할을 수행
- @EnableWebSecurity : 스프링MVC와 스프링 시큐리티를 결합하기 위함
- 오버라이드 메소드
- void configure(HttpSecurity http)
- URI 별 필요한 권한 설정
- 로그인 관련 기능 설정
- 커스텀 로그인 폼, 로그인 성공 핸들러
- void configure(AuthenticationManagerBuilder auth)
- 테스트를 위한 기본 계정의 ID/PW와 권한 설정
- DB계정정보를 가져올 쿼리 설정
- queryUser : 계정이 있는지에 대한 여부를 검사
- queryDetails : 계정이 가진 권한이 무엇인지에 대해 검사
- 계정정보를 DB에서 가져오는 UserDetailsService를 활용해서 계정 인증/인가 수행
> UserDetailsService를 구현하는 파일 작성
- src/main/java/com/exam/app/security/CustomUserDetailsService.java
- DB에서 인증, 인가 정보를 가져올 수 있는 mapper를 가짐
- UserDetails loadUserByUsername(String userName)
- 매개변수로 아이디를 받아서 mapper에서 찾은 후 UserDetails를 리턴
> UserDetails를 구현하는 User를 상속받는 파일 작성
- src/main/java/com/exam/app/security/domain/CustomUser.java
-
> AbstractAnnotationConfigDispatcherServletInitializer를 상속받는 파일 작성
- src/main/java/com/exam/app/config/WebConfig.java
> AuthenticationSuccessHandler를 상속받는 파일 작성
- src/main/java/com/exam/app/security/CustomLoginSuccessHandler.java
- 로그인을 정상적으로 성공한 경우 수행할 내용을 설정
4. 완료내용 :
> 커스텀 로그인 페이지 활용
> URI 별 권한 설정
> DB에 저장된 계정 정보를 통해 권한 부여(id, pw, enabled, authority)
> 패키지 구조
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─exam
│ │ │ │ SecurityExamWithBookApplication.java
│ │ │ │ ServletInitializer.java
│ │ │ ├─app
│ │ │ │ ├─common
│ │ │ │ │ ├─controller
│ │ │ │ │ │ LoginController.java
│ │ │ │ │ │ TestController.java
│ │ │ │ │ ├─dao
│ │ │ │ │ │ CommonDAO.java
│ │ │ │ │ └─model
│ │ │ │ │ AuthDTO.java
│ │ │ │ │ TestDTO.java
│ │ │ │ ├─config
│ │ │ │ │ ├─db
│ │ │ │ │ │ MariaDBConfiguration.java
│ │ │ │ │ └─security
│ │ │ │ │ RootConfig.java
│ │ │ │ │ SecurityInitializer.java
│ │ │ │ │ SpringSecurityConfiguration.java
│ │ │ │ │ WebConfig.java
│ │ │ │ └─security
│ │ │ │ │ CustomLoginSuccessHandler.java
│ │ │ │ │ CustomLogoutSuccessHandler.java
│ │ │ │ │ CustomUserDetailsService.java
│ │ │ │ └─domain
│ │ │ │ CustomUser.java
│ │ │ └─test
│ │ ├─resources
│ │ │ │ application.yml
│ │ │ ├─mapper
│ │ │ │ └─common
│ │ │ │ dbmapper.xml
│ │ │ │ TestDTOMapper.xml
│ │ │ ├─static
│ │ │ └─templates
│ │ └─webapp
│ │ └─WEB-INF
│ │ └─views
│ │ │ clogin.jsp
│ │ │ clogout.jsp
│ │ │ welcome.jsp
│ │ ├─admin
│ │ │ main.jsp
│ │ └─member
│ │ main.jsp
****실패한 내용****
> 테이블과 매핑되는 클래스와 매퍼 작성
- UserDetails를 구현하는 클래스 작성
- src/main/java/com/sse/app/member/model/Account.java
- 쿼리문을 저장할 매퍼 작성
- src/main/java/com/sse/app/member/model/AccountMapper.java
> DAO 역할을 수행할 AccountRepository 작성
- src/main/java/com/sse/app/member/dao/AccountRepository.java
> UserDetailsService를 구현하는 AccountService 작성
- src/main/java/com/sse/app/member/service/AccountService.java
****실패한 내용****
> AuthenticationProvider 구현
- src/main/java/com/sse/app/config/MemberAuthenticationProvider.java
> WebSecurityConfigurerAdapter를 상속받는 Configuration 파일 작성
- src/main/java/com/sse/app/config/WebSecurityConfig.java
> 사용자 정보를 담는 인터페이스 UserDetails 구현
- src/main/java/com/sse/app/member/model/MemberUserDetails.java
- VO역할 수행
- 작성 내용
- 테이블이 갖는 컬럼에 맞게끔 멤버변수 작성 : id, pw, name, email, auth
- Collection<? extends GrantedAuthority> getAuthorities() : 계정이 갖고 있는 권한을 목록으로 리턴하기 위한 메소드 설정
- boolean isEnabled() : 계정의 활성/비활성 여부가 담긴 ENABLED멤버변수를 리턴
> DB에서 유저 정보를 가져오기 위한 MemberDAO, xml 작성
- src/main/java/com/sse/app/member/dao/MemberDAO.java
- src/main/resources/mapper/member/Member.xml
> 인증 절차 인터페이스 UserDetailsService 구현
- 추상화 해놓은 UserDetailsService를 상속받아 loadUserByName(String id)메소드를 구현
- loadUserByName(String id) : 유저의 id를 통해 유저에 대한 인증 정보를 가져오는 기능 수행
- 유저 id를 통해 해당 유저를 찾는 로직과 유저의 권한들을 가져오는 로직이 포함되어 있음
- 작성 내용
- loadUserByName(String id)
- 사용자 정보를 MemberUserDetails 타입으로 가져옴
- id에 해당하는 사용자 정보가 없으면 UsernameNotFoundException을 throw
- 사용자 정보가 있는 경우 user 리턴
3. DB 모델링
1 2 3 4 5 6 7 8 9 10 11 12 13 | --기본적인 인증에 필요한 속성만 담은 테스트용 테이블 --is로 시작하는 컬럼은 spring security가 제공하는 검사 타입 CREATE TABLE SEC_MEMBER( USERNAME VARCHAR2(1000) PRIMARY KEY, PASSWORD VARCHAR2(1000), isEnabled NUMBER(10) ); --각 유저별로 할당되는 권한을 보관하는 테이블 CREATE TABLE SEC_AUTHORITY( USERNAME VARCHAR2(1000) PRIMARY KEY, AUTHORITY_NAME VARCHAR2(1000) ); | cs |
4. 문제 해결
1. Spring Security 기본 제공 폼 로그인 방법
Username에 user입력
password에 아래처럼 생성된 패스워드 입력
2. UserDetailsService 관련
> 위치 : src/main/java/com/exam/app/config/SpringSecurityConfiguration.java
- 문제 : configure메소드에서 auth.jdbcAuthentication()...으로 db정보 꺼내오는 방법을 UserDetailsService 구현을 통해 꺼내오려고 시도할 때 문제 발생
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { log.info("configure............................"); //테스트를 위한 기본 멤버 설정 // auth // .inMemoryAuthentication() // .withUser("admin").password("{noop}admin").roles("ADMIN"); // auth // .inMemoryAuthentication() // .withUser("member").password("{noop}member").roles("MEMBER"); // // String queryUser = "SELECT ID, PW, ENABLED FROM MEMBER_TEST WHERE ID=?"; // String queryDetails = "SELECT ID, AUTH FROM AUTH_TEST WHERE ID=?"; // // auth // .jdbcAuthentication() // .dataSource(dataSource) // .passwordEncoder(pwe) // .usersByUsernameQuery(queryUser) // .authoritiesByUsernameQuery(queryDetails); auth.userDetailsService(customUserService()).passwordEncoder(pwe); } | cs |
- 오류내용
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | org.springframework.security.authentication.InternalAuthenticationServiceException: Cannot pass null or empty values to constructor at org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(DaoAuthenticationProvider.java:123) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:144) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:200) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(UsernamePasswordAuthenticationFilter.java:94) ~[spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:124) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:74) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) [spring-security-web-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) [spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:860) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587) [tomcat-embed-core-9.0.24.jar:9.0.24] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.24.jar:9.0.24] at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_211] at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_211] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.24.jar:9.0.24] at java.lang.Thread.run(Unknown Source) [na:1.8.0_211] Caused by: java.lang.IllegalArgumentException: Cannot pass null or empty values to constructor at org.springframework.security.core.userdetails.User.<init>(User.java:113) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at org.springframework.security.core.userdetails.User.<init>(User.java:86) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] at com.exam.app.security.domain.CustomUser.<init>(CustomUser.java:27) ~[classes/:na] at com.exam.app.security.CustomUserDetailsService.loadUserByUsername(CustomUserDetailsService.java:32) ~[classes/:na] at org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(DaoAuthenticationProvider.java:108) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE] ... 57 common frames omitted | cs |
- 해결 :
- 원인 : mapper xml에 컬럼이름지정이 잘못되어있어서 생성자 매개변수에 null이 들어갔음
- 해결방법 : 매퍼에 컬럼이름 수정, Spring Security가 기본적으로 다루는 변수명에 맞게 alias 지정(userid, userpw)
3. URI 별 권한설정 미인식
> 문제 :
- /member/admin 리퀘스트는 ROLE_ADMIN 인가정보가 있어야만 접근이 가능한데, 없어도 접근이 되는 상태
- 소스코드( src/main/java/com/rhymes/app/config/security/SpringSecurityConfigutaion.java)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @Override protected void configure(HttpSecurity http) throws Exception { // TODO Auto-generated method stub super.configure(http); log.info("HttpSecurity"); //URI패턴 별 권한 설정 http .authorizeRequests() .antMatchers("/welcome").permitAll() .antMatchers("/member/admin", "/member/admin/**").access("hasRole('ROLE_ADMIN')") .antMatchers("/member/member", "/member/member/**").access("hasRole('ROLE_MEMBER')"); //로그인페이지 설정 http .formLogin() .loginPage("/member/login").permitAll() .loginProcessingUrl("/login") .successHandler( loginSuccessHandler() ); //로그아웃 관련 설정 http .logout() .logoutUrl("/logout") .invalidateHttpSession(true) .deleteCookies("remember-me", "JSESSION_ID") .logoutSuccessHandler(logoutSuccessHandler()); } | cs |
> 해결 방법
- 혹시나 싶어서 super.config(http);부분을 지워보니 정상적으로 403에러페이지 확인
'공부 > Spring Boot' 카테고리의 다른 글
Spring Security 활용 회원 관리 (3) - Spring Security와 Embedded Redis (0) | 2019.09.17 |
---|---|
Spring Security 활용 회원 관리 (2) - 커스텀 로그인 뷰 설정 (0) | 2019.09.17 |
뷰(form)에 입력한 많은 데이터를 List 타입으로 컨트롤러로 보내는 방법 (4) | 2019.09.09 |
12. Spring Boot Gradle 프로젝트 생성 방법(Thymeleaf) (0) | 2019.09.04 |
11. Spring Boot 파일 업로드/파일 다운로드 (0) | 2019.09.02 |