๋ฐ๋ก ๋ก๊ทธ์ธ
์ด์ ํ์ด์ง
์ด์ ํ์ด์ง ๋ด์ฉ์ ์ด์ด์ ์งํํฉ๋๋ค.
Code
Preview
1.
ํ์ ๊ฐ์
ํ๋ฉด
2.
๋ฉ์ธ ํ๋ฉด
์์ ํ๋ก์ธ์ค
1.
ํ๋ก์ ํธ ์์ฑ
2.
ํ๋ก์ ํธ ์ค์
3.
์๋น์ค
4.
์ปจํธ๋กค๋ฌ
Preview
1.
ํ์ ๊ฐ์
ํ๋ฉด
2.
๋ฉ์ธ ํ๋ฉด
ํ์ ๊ฐ์ ํ๋ฉด
ํ์ ๊ฐ์
์ฒ๋ฆฌ ๋ฐ๋ก ๋ก๊ทธ์ธ ๋ฉ์ธ ํ๋ฉด
ํ์ ๊ฐ์
์์ฒญ ์, ํ์ ๊ฐ์
์ด ์ฑ๊ณตํ๊ฒ ๋๋ฉด ๋ฐ๋ก ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ํ ๋ฉ์ธ ํ๋ฉด์ผ๋ก ์ด๋ํฉ๋๋ค.
๋ฉ์ธ ํ๋ฉด
์์ ํ๋ก์ธ์ค
1.
ํ๋ก์ ํธ ์์ฑ
2.
ํ๋ก์ ํธ ์ค์
3.
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
4.
์๋น์ค
5.
์ปจํธ๋กค๋ฌ
ํ๋ก์ ํธ ์์ฑ
build.gradle
spring boot 3.x.x
spring security 6.x.x
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.aloha'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Java
๋ณต์ฌ
ํ๋ก์ ํธ ์ค์
application.properties
spring.application.name=form-jointologin
# ๋ฐ์ดํฐ ์์ค - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
# Mybatis ์ค์
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.aloha.security6.domain
mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Markdown
๋ณต์ฌ
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
SecurityConfig.java
package com.aloha.security6.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.BadSqlGrammarException;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource;
// ์คํ๋ง ์ํ๋ฆฌํฐ ์ค์ ๋ฉ์๋
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// โ
์ธ๊ฐ ์ค์
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/**").permitAll()
.anyRequest().permitAll()
);
// ๐ ํผ ๋ก๊ทธ์ธ ์ค์
http.formLogin( login -> login.permitAll() );
// ๐ ์๋ ๋ก๊ทธ์ธ ์ค์
http.rememberMe(me -> me.key("aloha")
.tokenRepository(tokenRepository())
.tokenValiditySeconds(60 * 60 * 24 * 7)); // 7์ผ ์ ํจ์๊ฐ (์ด๋จ์)
return http.build();
}
/**
* ๐ ๋น๋ฐ๋ฒํธ ์ํธํ ๋น ๋ฑ๋ก
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* ๐ AuthenticationManager ์ธ์ฆ ๊ด๋ฆฌ์ ๋น ๋ฑ๋ก
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration ) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* ๐ JDBC ์ธ์ฆ ๋ฐฉ์ ๋น ๋ฑ๋ก
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager userDetailsManager
= new JdbcUserDetailsManager(dataSource);
// ์ฌ์ฉ์ ์ธ์ฆ ์ฟผ๋ฆฌ
String sql1 = " SELECT username, password, enabled "
+ " FROM user "
+ " WHERE username = ? "
;
// ์ฌ์ฉ์ ๊ถํ ์ฟผ๋ฆฌ
String sql2 = " SELECT username, auth "
+ " FROM user_auth "
+ " WHERE username = ? "
;
userDetailsManager.setUsersByUsernameQuery(sql1);
userDetailsManager.setAuthoritiesByUsernameQuery(sql2);
return userDetailsManager;
}
/**
* ๐ ์๋ ๋ก๊ทธ์ธ ์ ์ฅ์ ๋น ๋ฑ๋ก
* โ
๋ฐ์ดํฐ ์์ค
* โญ persistent_logins ํ
์ด๋ธ ์์ฑ
create table persistent_logins (
username varchar(64) not null
, series varchar(64) primary key
, token varchar(64) not null
, last_used timestamp not null
);
* ๐ ์๋ ๋ก๊ทธ์ธ ํ๋ก์ธ์ค
* โ
๋ก๊ทธ์ธ ์
* โก ๐ฉโ๐ผ(ID, ์๋ฆฌ์ฆ, ํ ํฐ) ์ ์ฅ
* โ
๋ก๊ทธ์์ ์,
* โก ๐ฉโ๐ผ(ID, ์๋ฆฌ์ฆ, ํ ํฐ) ์ญ์
* @return
*/
@Bean
public PersistentTokenRepository tokenRepository() {
// JdbcTokenRepositoryImpl : ํ ํฐ ์ ์ฅ ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ๋ฑ๋กํ๋ ๊ฐ์ฒด
JdbcTokenRepositoryImpl repositoryImpl = new JdbcTokenRepositoryImpl();
// โ
ํ ํฐ ์ ์ฅ์๋ฅผ ์ฌ์ฉํ๋ ๋ฐ์ดํฐ ์์ค ์ง์
// - ์ํ๋ฆฌํฐ๊ฐ ์๋ ๋ก๊ทธ์ธ ํ๋ก์ธ์ค๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ DB๋ฅผ ์ง์ ํฉ๋๋ค.
repositoryImpl.setDataSource(dataSource);
// ์๋ฒ ์คํ ์, ์๋ ๋ก๊ทธ์ธ ํ
์ด๋ธ ์๋ ์์ฑ
// repositoryImpl.setCreateTableOnStartup(true);
// persistent_logins ํ
์ด๋ธ ์์ฑ
try {
repositoryImpl.getJdbcTemplate().execute(JdbcTokenRepositoryImpl.CREATE_TABLE_SQL);
}
catch (BadSqlGrammarException e) {
log.error("persistent_logins ํ
์ด๋ธ์ด ์ด๋ฏธ ์กด์ฌํฉ๋๋ค.");
}
catch (Exception e) {
log.error("์๋ ๋ก๊ทธ์ธ ํ
์ด๋ธ ์์ฑ ์ค , ์์ธ ๋ฐ์");
}
return repositoryImpl;
}
}
Java
๋ณต์ฌ
์๋น์ค
โข
UserService.java
โข
UserServiceImpl.java
UserService.java
login() ๋ฉ์๋๋ฅผ ์ถ๊ฐ๋ก ์ ์ํฉ๋๋ค.
public interface UserService {
// ๋ก๊ทธ์ธ
public boolean login(Users user) throws Exception;
// ์กฐํ
public Users select(String username) throws Exception;
// ํ์ ๊ฐ์
public int join(Users user) throws Exception;
// ํ์ ์์
public int update(Users user) throws Exception;
// ํ์ ๊ถํ ๋ฑ๋ก
public int insertAuth(UserAuth userAuth) throws Exception;
}
Java
๋ณต์ฌ
UserServiceImpl.java
ํ์ ๊ฐ์
์, ์
๋ ฅํ ๋ก๊ทธ์ธ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ํตํด ๋ก๊ทธ์ธ ์ธ์ฆ ์ฒ๋ฆฌ๊ฐ ๋๋๋ก ๊ตฌํํฉ๋๋ค.
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public boolean login(Users user, HttpServletRequest request) throws Exception {
// ๐ ํ ํฐ ์์ฑ
String username = user.getUsername(); // ์์ด๋
String password = user.getPassword(); // ์ํธํ๋์ง ์์ ๋น๋ฐ๋ฒํธ
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, password);
// ํ ํฐ์ ์ด์ฉํ์ฌ ์ธ์ฆ
Authentication authentication = authenticationManager.authenticate(token);
// ์ธ์ฆ ์ฌ๋ถ ํ์ธ
boolean result = authentication.isAuthenticated();
// ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด SecurityContext์ ์ค์
if (result) {
SecurityContextHolder.getContext().setAuthentication(authentication);
// ์ธ์
์ ์ธ์ฆ ์ ๋ณด ์ค์ (์ธ์
์ด ์์ผ๋ฉด ์๋ก ์์ฑ)
HttpSession session = request.getSession(true); // ์ธ์
์ด ์์ผ๋ฉด ์๋ก ์์ฑ
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
}
return result;
}
@Override
public Users select(String username) throws Exception {
Users user = userMapper.select(username);
return user;
}
@Override
public int join(Users user) throws Exception {
String username = user.getUsername();
String password = user.getPassword();
String encodedPassword = passwordEncoder.encode(password); // ๐ ๋น๋ฐ๋ฒํธ ์ํธํ
user.setPassword(encodedPassword);
// ํ์ ๋ฑ๋ก
int result = userMapper.join(user);
if( result > 0 ) {
// ํ์ ๊ธฐ๋ณธ ๊ถํ ๋ฑ๋ก
UserAuth userAuth = new UserAuth();
userAuth.setUsername(username);
userAuth.setAuth("ROLE_USER");
result = userMapper.insertAuth(userAuth);
}
return result;
}
@Override
public int update(Users user) throws Exception {
int result = userMapper.update(user);
return result;
}
@Override
public int insertAuth(UserAuth userAuth) throws Exception {
int result = userMapper.insertAuth(userAuth);
return result;
}
}
Java
๋ณต์ฌ
์ปจํธ๋กค๋ฌ
- ~/controller/HomeController.java
~/controller/HomeController.java
ํ์ ๊ฐ์
์ฒ๋ฆฌ ์ฑ๊ณต ์, ๋ก๊ทธ์ธ ์์ฒญ ํ ๋ฉ์ธ ํ๋ฉด์ผ๋ก ์ด๋ํ๋๋ก ์ฝ๋๋ฅผ ์์ ํฉ๋๋ค.
์ด์ ์ฝ๋
int result = userService.join(user);
if( result > 0 ) {
return "redirect:/login";
}
Java
๋ณต์ฌ
์์ ์ฝ๋
// ์ํธํ ์ ๋น๋ฐ๋ฒํธ
String plainPassword = user.getPassword();
// ํ์ ๊ฐ์
์์ฒญ
int result = userService.join(user);
// ํ์ ๊ฐ์
์ฑ๊ณต ์, ๋ฐ๋ก ๋ก๊ทธ์ธ
if( result > 0 ) {
// ์ํธํ ์ ๋น๋ฐ๋ฒํธ ๋ค์ ์ธํ
// ํ์๊ฐ์
์, ๋น๋ฐ๋ฒํธ ์ํธํํ๊ธฐ ๋๋ฌธ์,
user.setPassword(plainPassword);
userService.login(user);
return "redirect:/";
}
Java
๋ณต์ฌ
HomeController.java
@Slf4j
@Controller
public class HomeController {
@Autowired
private UserService userService;
//**
* ๋ฉ์ธ ํ๋ฉด
* ๐ [GET] - /
* ๐ index.html
* @return
* @throws Exception
*/
@GetMapping("")
// public String home(Principal principal, Model model) throws Exception {
// public String home(Authentication authentication, Model model) throws Exception {
public String home(@AuthenticationPrincipal User authUser, Model model) throws Exception {
log.info(":::::::::: ๋ฉ์ธ ํ๋ฉด ::::::::::");
// if( authentication != null ) {
// User authAuth = (User) authentication.getPrincipal();
// String username = authAuth.getUsername();
// log.info("username : " + username);
// Users user = userService.select(username); // ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
// log.info("user : " + user);
// model.addAttribute("user", user);
// }
// if (principal != null) {
// String username = principal.getName(); // ์ธ์ฆ๋ ์ฌ์ฉ์ ์์ด๋
// log.info("username : " + username);
// Users user = userService.select(username); // ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
// log.info("user : " + user);
// model.addAttribute("user", user); // ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ชจ๋ธ์ ๋ฑ๋ก
// } else {
// log.info("principal is null");
// }
if( authUser != null ) {
String username = authUser.getUsername(); // ์ธ์ฆ๋ ์ฌ์ฉ์ ์์ด๋
log.info("username : " + username);
Users user = userService.select(username); // ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
log.info("user : " + user);
model.addAttribute("user", user); // ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ชจ๋ธ์ ๋ฑ๋ก
log.info("authAuth : " + authUser);
}
return "index";
}
/**
* ํ์ ๊ฐ์
ํ๋ฉด
* ๐ [GET] - /join
* ๐ join.html
* @return
*/
@GetMapping("/join")
public String join() {
log.info(":::::::::: ํ์ ๊ฐ์
ํ๋ฉด ::::::::::");
return "join";
}
/**
* ํ์ ๊ฐ์
์ฒ๋ฆฌ
* ๐ [POST] - /join
* โก โญ ๐๐๋ฐ๋ก ๋ก๊ทธ์ธ โก /
* โ /join?error
* @param user
* @return
* @throws Exception
*/
@PostMapping("/join")
public String joinPro(Users user) throws Exception {
log.info(":::::::::: ํ์ ๊ฐ์
์ฒ๋ฆฌ ::::::::::");
log.info("user : " + user);
// ์ํธํ ์ ๋น๋ฐ๋ฒํธ
String plainPassword = user.getPassword();
// ํ์ ๊ฐ์
์์ฒญ
int result = userService.join(user);
// ํ์ ๊ฐ์
์ฑ๊ณต ์, ๋ฐ๋ก ๋ก๊ทธ์ธ
if( result > 0 ) {
// ์ํธํ ์ ๋น๋ฐ๋ฒํธ ๋ค์ ์ธํ
// ํ์๊ฐ์
์, ๋น๋ฐ๋ฒํธ ์ํธํํ๊ธฐ ๋๋ฌธ์,
user.setPassword(plainPassword);
userService.login(user);
return "redirect:/";
}
return "redirect/join?error";
}
/**
* ์์ด๋ ์ค๋ณต ๊ฒ์ฌ
* @param username
* @return
* @throws Exception
*/
@ResponseBody
@GetMapping("/check/{username}")
public ResponseEntity<Boolean> userCheck(@PathVariable("username") String username) throws Exception {
log.info("์์ด๋ ์ค๋ณต ํ์ธ : " + username);
Users user = userService.select(username);
// ์์ด๋ ์ค๋ณต
if( user != null ) {
log.info("์ค๋ณต๋ ์์ด๋ ์
๋๋ค - " + username);
return new ResponseEntity<>(false, HttpStatus.OK);
}
// ์ฌ์ฉ ๊ฐ๋ฅํ ์์ด๋์
๋๋ค.
log.info("์ฌ์ฉ ๊ฐ๋ฅํ ์์ด๋ ์
๋๋ค." + username);
return new ResponseEntity<>(true, HttpStatus.OK);
}
}
Java
๋ณต์ฌ
์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
Principal vs Authentication vs User
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฃผ์ ์ธํฐํ์ด์ค ๋ฐ ํด๋์ค
๋ค์์ Spring Security์ ์ฃผ์ ์ธํฐํ์ด์ค์ ํด๋์ค์ ๊ตฌ์กฐ๋์
๋๋ค
java.security.Principal (Interface)
โ
Authentication (Interface)
โ
UsernamePasswordAuthenticationToken (๊ตฌํ์ฒด)
โ
getPrincipal()
โณ org.springframework.security.core.userdetails.User
(implements UserDetails)
Java
๋ณต์ฌ
์ด ๊ตฌ์กฐ๋๋ Spring Security์์ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๊ฐ ์ด๋ป๊ฒ ๊ณ์ธตํ๋์ด ์๋์ง๋ฅผ ๋ณด์ฌ์ค๋๋ค.
Principal์ ์ต์์ ์ธํฐํ์ด์ค์ด๋ฉฐ, Authentication์ ์ด๋ฅผ ํ์ฅํ์ฌ ๋ ๋ง์ ์ธ์ฆ ๊ด๋ จ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. UsernamePasswordAuthenticationToken์ Authentication์ ๊ตฌํ์ฒด ์ค ํ๋์ด๋ฉฐ, getPrincipal() ๋ฉ์๋๋ฅผ ํตํด UserDetails๋ฅผ ๊ตฌํํ User ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
ํด๋์ค/์ธํฐํ์ด์ค | ์ญํ | ์ฃผ์ ๋ฉ์๋ | ์ฃผ์ ๊ตฌํ์ฒด |
Principal | ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ธฐ๋ณธ ์ ๋ณด ์ ๊ณต (ํ์ค ์๋ฐ ์ธํฐํ์ด์ค) | getName() | Spring Security์ User, ๋๋ ์ฌ์ฉ์ ์ ์ ๊ฐ์ฒด |
Authentication | Spring Security ์ธ์ฆ ์ ๋ณด (์ฌ์ฉ์, ๊ถํ, ์ธ์ฆ ์ํ ๋ฑ ํฌํจ) | getPrincipal(), getAuthorities(), isAuthenticated() | UsernamePasswordAuthenticationToken, JwtAuthenticationToken |
User | ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ธ๋ถ ์ ๋ณด ์ ๊ณต (Spring Security์ ๊ตฌํ์ฒด, UserDetails๋ฅผ ๊ตฌํ) | getUsername(), getAuthorities(), isAccountNonLocked() ๋ฑ | Spring Security ๊ธฐ๋ณธ User ํด๋์ค |
โข
Principal: ์ต์ํ์ ์ฌ์ฉ์ ์๋ณ ์ ๋ณด๋ฅผ ์ ๊ณต.
โข
Authentication: ์ธ์ฆ ๋ฐ ๊ถํ ๊ด๋ จ ์ ๋ณด๋ฅผ ๋ชจ๋ ํฌํจ.
โข
User: Spring Security์์ ์ฌ์ฉ์ ์ธ๋ถ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ์ฒด.