Skip to content

Commit 8f52d1d

Browse files
committed
✨ Add: project sources
1 parent feab31b commit 8f52d1d

File tree

10 files changed

+358
-0
lines changed

10 files changed

+358
-0
lines changed

build.gradle

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
plugins {
2+
id 'org.springframework.boot' version '2.5.6'
3+
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
4+
id 'java'
5+
}
6+
7+
group = 'io.github.imsejin'
8+
version = '0.1.0'
9+
sourceCompatibility = JavaVersion.VERSION_11
10+
11+
configurations {
12+
compileOnly {
13+
extendsFrom annotationProcessor
14+
}
15+
}
16+
17+
repositories {
18+
mavenCentral()
19+
}
20+
21+
dependencies {
22+
implementation 'org.springframework.boot:spring-boot-starter-security'
23+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
24+
implementation 'org.springframework.boot:spring-boot-starter-web'
25+
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
26+
implementation 'org.springframework.boot:spring-boot-devtools'
27+
28+
compileOnly 'org.projectlombok:lombok'
29+
annotationProcessor 'org.projectlombok:lombok'
30+
31+
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
32+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
33+
testImplementation 'org.springframework.security:spring-security-test'
34+
}
35+
36+
test {
37+
useJUnitPlatform()
38+
}

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'study-spring-security'
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.imsejin.study.springsecurity;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
6+
import org.springframework.scheduling.annotation.EnableAsync;
7+
8+
@EnableAsync
9+
@SpringBootApplication
10+
@ConfigurationPropertiesScan
11+
public class SpringSecurityApplication {
12+
13+
public static void main(String[] args) {
14+
SpringApplication.run(SpringSecurityApplication.class, args);
15+
}
16+
17+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package io.github.imsejin.study.springsecurity.config.security;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
5+
import org.springframework.security.authentication.AuthenticationManager;
6+
import org.springframework.security.authentication.AuthenticationProvider;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
10+
import org.springframework.security.config.http.SessionCreationPolicy;
11+
import org.springframework.security.core.Authentication;
12+
import org.springframework.security.core.AuthenticationException;
13+
import org.springframework.security.core.context.SecurityContext;
14+
import org.springframework.security.core.context.SecurityContextHolder;
15+
import org.springframework.security.core.userdetails.UserDetailsService;
16+
import org.springframework.security.web.authentication.*;
17+
import org.springframework.security.web.authentication.logout.LogoutFilter;
18+
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
19+
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
20+
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
21+
import org.springframework.security.web.session.ConcurrentSessionFilter;
22+
import org.springframework.security.web.session.SessionManagementFilter;
23+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
24+
25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.HttpServletResponse;
27+
import java.io.IOException;
28+
import java.util.concurrent.TimeUnit;
29+
30+
/**
31+
* WebSecurityConfig
32+
*
33+
* @see AnonymousAuthenticationFilter
34+
* @see AnonymousAuthenticationToken
35+
*/
36+
@EnableWebSecurity
37+
@RequiredArgsConstructor
38+
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
39+
40+
private final UserDetailsService userDetailsService;
41+
42+
@Override
43+
protected void configure(HttpSecurity http) throws Exception {
44+
// Authentication(인증)
45+
// All requests must be authenticated.
46+
http.authorizeRequests().anyRequest().authenticated();
47+
48+
// Authorization(인가)
49+
login(http);
50+
51+
logout(http);
52+
rememberMe(http);
53+
manageSession(http);
54+
}
55+
56+
/**
57+
* Login Form Flow
58+
*
59+
* <ol>
60+
* <li>Receive request.</li>
61+
* <li>{@link UsernamePasswordAuthenticationFilter} checks if request URL matches.</li>
62+
* <li>If {@link AntPathRequestMatcher} returns {@code true}, pass the next step,
63+
* else execute {@code chain.doFilter}.</li>
64+
* <li>Pass a {@link Authentication} with username and password.</li>
65+
* <li>{@link AuthenticationManager} delegates validation to {@link AuthenticationProvider}.</li>
66+
* <li>{@link AuthenticationProvider} validates the given user information.</li>
67+
* <li>If authentication succeed, return a {@link Authentication} with user and authorities,
68+
* else throw {@link AuthenticationException}.</li>
69+
* <li>{@link Authentication} is stored in {@link SecurityContext}.</li>
70+
* <li>Invoke {@link AuthenticationSuccessHandler}</li>
71+
* </ol>
72+
*
73+
* @see UsernamePasswordAuthenticationFilter
74+
*/
75+
private static void login(HttpSecurity http) throws Exception {
76+
http.formLogin()
77+
// .loginPage("/auth") // Request URL to go to custom login page(default: /login)
78+
.defaultSuccessUrl("/home") // Page to arrive after login succeed.
79+
.failureUrl("/login") // Page to arrive after login failed.
80+
.usernameParameter("id") // Attribute 'name' in input tag.
81+
.passwordParameter("pw") // Attribute 'name' in input tag.
82+
.loginProcessingUrl("/login-process") // Attribute 'action' in form tag.
83+
.successHandler(new CustomAuthenticationSuccessHandler())
84+
.failureHandler(new CustomAuthenticationFailureHandler())
85+
// Request URL for login page must be passed without authentication.
86+
// If not, you go to infinite redirect loop.
87+
.permitAll();
88+
}
89+
90+
/**
91+
* Logout Flow
92+
*
93+
* <ol>
94+
* <li>Receive request.</li>
95+
* <li>{@link LogoutFilter} checks if request URL matches.</li>
96+
* <li>If {@link AntPathRequestMatcher} returns {@code true}, pass the next step,
97+
* else execute {@code chain.doFilter}.</li>
98+
* <li>Get a {@link Authentication} from {@link SecurityContext}.</li>
99+
* <li>Pass it to {@link SecurityContextLogoutHandler}.</li>
100+
* <li>Invalidate session, delete cookies and invoke {@link SecurityContextHolder#clearContext()}.</li>
101+
* <li>Delegate to {@link SimpleUrlLogoutSuccessHandler}.</li>
102+
* <li>Redirect to login page.</li>
103+
* </ol>
104+
*
105+
* @see LogoutFilter
106+
*/
107+
private static void logout(HttpSecurity http) throws Exception {
108+
http.logout()
109+
// .logoutUrl("/") // Request URL to go to custom logout page(default: POST /logout)
110+
.logoutSuccessUrl("/auth")
111+
.addLogoutHandler((request, response, authentication) -> {
112+
System.out.printf("logout-1: '%s'%n", authentication.getName());
113+
request.getSession().invalidate();
114+
})
115+
.logoutSuccessHandler((request, response, authentication) -> {
116+
System.out.printf("Good bye, '%s'!%n", authentication.getName());
117+
response.sendRedirect("/login");
118+
})
119+
.deleteCookies("remember-me");
120+
}
121+
122+
/**
123+
* Remember-me Flow
124+
* (Sustain authenticated state and auto-login)
125+
*
126+
* <ol>
127+
* <li>When {@link Authentication} in {@link SecurityContext} is null and HTTP request
128+
* has cookie 'remember-me', execute {@link RememberMeAuthenticationFilter}.</li>
129+
* <li>Delegate the job to {@link RememberMeServices}.</li>
130+
* <li>Extract the token from cookies.</li>
131+
* <li>If the token exists, validate the decoded token,
132+
* else execute {@code chain.doFilter}(pass the next filter).</li>
133+
* <li>When token is invalid or user account doesn't exist, throw exception.</li>
134+
* <li>If pass all the validations, create new a {@link Authentication}.</li>
135+
* <li>Pass it to {@link AuthenticationManager}.</li>
136+
* </ol>
137+
*
138+
* @see RememberMeAuthenticationFilter
139+
*/
140+
private void rememberMe(HttpSecurity http) throws Exception {
141+
http.rememberMe()
142+
.rememberMeParameter("rememberMe") // default: remember-me
143+
.tokenValiditySeconds(3600) // default: 14 days
144+
.alwaysRemember(false)
145+
.userDetailsService(userDetailsService);
146+
}
147+
148+
/**
149+
* Session Management
150+
*
151+
* @see SessionManagementFilter
152+
* @see ConcurrentSessionFilter
153+
*/
154+
private static void manageSession(HttpSecurity http) throws Exception {
155+
// Concurrent Session Management
156+
http.sessionManagement()
157+
.invalidSessionUrl("/invalid")
158+
.maximumSessions(1) // If -1, allow infinite concurrent session login.
159+
// If true, prevent a user from logging in and sustain the previous session.
160+
// If false, allow a user to login and invalidate the previous session.
161+
.maxSessionsPreventsLogin(false)
162+
.expiredUrl("/expired");
163+
164+
// Protect against session fixation attack.
165+
http.sessionManagement()
166+
.sessionFixation()
167+
.changeSessionId();
168+
169+
// Session creation policy.
170+
http.sessionManagement()
171+
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
172+
}
173+
174+
/**
175+
* Custom authentication success handler
176+
*
177+
* @see <a href="https://stackoverflow.com/questions/28103852/spring-boot-session-timeout">
178+
* Spring Boot Session Timeout</a>
179+
*/
180+
private static class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
181+
private static final int SESSION_TIMEOUT = (int) TimeUnit.SECONDS.toSeconds(60);
182+
183+
@Override
184+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
185+
Authentication authentication) throws IOException {
186+
request.getSession().setMaxInactiveInterval(SESSION_TIMEOUT);
187+
System.out.printf("Welcome-1, '%s'!%n", authentication.getName());
188+
response.sendRedirect("/home");
189+
}
190+
}
191+
192+
private static class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
193+
@Override
194+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
195+
AuthenticationException exception) throws IOException {
196+
System.out.printf("Failed to login: '%s'%n", exception.getMessage());
197+
response.sendRedirect("/");
198+
}
199+
}
200+
201+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.imsejin.study.springsecurity.view.auth;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
7+
@Controller
8+
@RequestMapping("auth")
9+
class AuthController {
10+
11+
@GetMapping
12+
Object login() {
13+
return "authentication";
14+
}
15+
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.imsejin.study.springsecurity.view.home;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RequestMapping;
6+
7+
@Controller
8+
@RequestMapping("home")
9+
class HomeController {
10+
11+
@GetMapping
12+
Object welcome() {
13+
return "home";
14+
}
15+
16+
}

src/main/resources/application.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
spring:
2+
thymeleaf:
3+
enabled: true
4+
encoding: "UTF-8"
5+
prefix: "classpath:/templates/view/"
6+
suffix: ".html"
7+
security:
8+
user:
9+
name: "user"
10+
password: "1234"
11+
devtools:
12+
livereload:
13+
enabled: true
14+
restart:
15+
enabled: true
16+
poll-interval: "2s"
17+
# mvc:
18+
# view:
19+
# prefix: "classpath:/templates/view/"
20+
# suffix: ".html"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Authentication</title>
6+
</head>
7+
<body>
8+
<div>
9+
<form method="post" action="/login-process">
10+
<span>ID: </span>
11+
<label for="username">
12+
<input id="username" type="text" name="id" placeholder="Input your ID" />
13+
</label>
14+
15+
<span>PW: </span>
16+
<label for="password">
17+
<input id="password" type="password" name="pw" placeholder="Input your password" />
18+
</label>
19+
20+
<button type="submit">login</button>
21+
</form>
22+
</div>
23+
</body>
24+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Welcome</title>
6+
</head>
7+
<body>
8+
<div>Welcome to my webpage!</div>
9+
</body>
10+
</html>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.imsejin.study.springsecurity;
2+
3+
import org.junit.jupiter.api.Disabled;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.boot.test.context.SpringBootTest;
6+
7+
@Disabled
8+
@SpringBootTest
9+
class SpringSecurityApplicationTest {
10+
11+
@Test
12+
void contextLoads() {
13+
}
14+
15+
}

0 commit comments

Comments
 (0)