
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
logging:
level:
com.zaxxer: info
org.springframework.jdbc: info
org.springframework.security: trace
org:
apache:
ibatis: debug
package org.demo.springdemo.board.config;
@Configuration
@Log4j2
@EnableMethodSecurity(prePostEnabled = true)
public class CustomSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("filterChain..............");
http.formLogin(config -> {
});
http.csrf(config -> {config.disable();});
return http.build();
}
}
๐ EnableMethodSecurity์ controller์์ ํด๋น ๊ถํ์ด ์์๋ ์ฌ์ฉํ ์์๋๋ก ์ฌ์ฉํ๋ @PreAuthorize, @PostAuthorize ์ด๋ ธํ ์ด์ ๋ค์ ํ์ฑํํ๋ ์ด๋ ธํ ์ด์ ์ด๋ค.
๐ password๋ฅผ Encodeingํด์ผํ๋ฏ๋ก @bean์ ํตํด ์ ์ํ๊ณ , http://localhost:8080/login์ ์ ๊ทผํ ์์๋๋ก http.formLogin(config) ์ค์ ํ๋ค.
๐ ๋ณด์์ ์ํด ๋ณ๊ฒฝ๋๋ csrfํ ํฐ์ disable๋ก ๋นํ์ฑํ์ํจ๋ค.
package org.demo.springdemo.board.security;
@Component
@Log4j2
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("userName: "+ username);
UserDetails userDetails = User.builder()
.username(username)
.password("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i")
.authorities("ROLE_USER")
.build();
return userDetails;
}
}
๐ ํด๋น ํด๋์ค๋ UserDetailsService๋ฅผ ์ธํฐํ์ด์คํ๊ณ ์์ผ๋ฏ๋ก ๊ด๋ จ ์ถ์๋ฉ์๋๋ฅผ ์์ํ๋๋ฐ, UserDetails ์ ๋ฐ์ดํฐํ์ ์ผ๋ก๋ ๊ฐ์ฒด๋ฅผ builder๋ก ์์ฑํ์ฌ username, password, authorities๋ฅผ ๊ตฌ์กฐํํ๋ค.
๐ username์ ์ ํด์ง ๊ท์น์ด ์๊ณ , password๋ ์ธ์ฝ๋ฉ์ ํตํ ํด์ํจ์๋ก 11, authorities๋ ๊ถํ์ด๋ค.
๐ userService ์ธํฐํ์ด์ค์ ๊ตฌ์ฑ

package org.demo.springdemo.board.controller;
@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
private final UploadUtil uploadUtil;
private final BoardService boardService;
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/register")
public void register(){
}
@PreAuthorize("hasRole('ROLE_USER')")
@PostMapping("/register")
public String register(BoardRegisterDTO boardRegisterDTO, RedirectAttributes rttr){
log.info("register: "+ boardRegisterDTO);
List<String> uploadedFileNames = uploadUtil.upload(boardRegisterDTO.getImages());
boardRegisterDTO.setFileNames(uploadedFileNames);
boardService.register(boardRegisterDTO);
rttr.addFlashAttribute("bno", boardRegisterDTO.getBno());
return "redirect:/board/list";
}
@PreAuthorize("permitAll()")
@GetMapping("/list")
public void list(PageRequest pageRequest, Model model) {
model.addAttribute("result", boardService.list(pageRequest));
}
@PreAuthorize("isAuthenticated()") // ๋ก๊ทธ์ธ ๋ ์ฌ์ฉ์๋ง ์ฌ์ฉ๊ฐ๋ฅ
@GetMapping("/read/{bno}")
public String read(@CookieValue("view")String viewValue, @PathVariable("bno") Long bno, Model model) {
log.info("Reading board: "+bno);
log.info("viewValue: "+viewValue);
boolean existed = false;
if(viewValue != null){
existed = Arrays.stream(viewValue.split("%")).anyMatch(str -> str.equals(bno+""));
}
if(!existed) {
log.info("View Count... update........................");
}
Optional<BoardReadDTO> result = boardService.get(bno);
BoardReadDTO boardReadDTO = result.orElseThrow();
model.addAttribute("board", boardReadDTO);
return "/board/read";
}
}
๐ PreAuthorize๋ฅผ ํตํด registerํ์ด์ง๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ROLE_USER์ ๊ถํ์ด ์์๋ ์ฌ์ฉ๊ฐ๋ฅํ๊ณ , listํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๋๊ฑด ๋ชจ๋ ์ฌ์ฉ์๊ฐ ๊ฐ๋ฅํ๋๋ก, ๊ฒ์๋ฌผ์ ์ฝ๋ ํ์ด์ง๋ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์๋ง ์ฌ์ฉ๊ฐ๋ฅํ๋๋ก ๊ถํ์ ๋ถ์ฌํ๋ค.
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/basicLayout.html}">
<div layout:fragment="content">
<h1>Board Register</h1>
<form id="form1" method="post" enctype="multipart/form-data"> <!--ํผ ๋ฐ์ดํฐ๊ฐ ์ฌ๋ฌ ๋ถ๋ถ์ผ๋ก ๋๋์ด ์ ์ก / ํ์ผ์ ์ก์ ํ์-->
<input type="text" name="title" >
<input type="text" name="content" >
<input type="text" name="writer" th:value="${#authentication.principal.username}" readonly>
<input type="text" name="tag" >
<input type="file" name="images" multiple>
<button class="submitBtn">CLICK</button>
</form>
</div>
<script th:inline="javascript" layout:fragment="script">
const auth = [[ ${#authentication.principal} ]] //์ฌ์ฉ์์ ์ ๋ณด
console.log(auth)
const form1 = document.querySelector("#form1")
document.querySelector(".submitBtn").addEventListener("click",e=>{
e.preventDefault();
form1.submit();
form1.reset();
},false)
</script>
๐ ํด๋น registerํ์ด์ง์์๋ ๋ก๊ทธ์ธํ์ด์ง์์ ๋ก๊ทธ์ธํ username์ ๊ฐ์ผ๋ก ๊ฐ์ ธ์์ username์ ๊ณ ์ value๊ฐ์ผ๋ก ๋ง๋ ๋ค.
๐ scriptํ๊ทธ์์ auth๋ผ๋ ๋ณ์์ ${#authentication.principal} ๋ฅผ ํตํด ๋ก๊ทธ์ธํ user์ ๋ณด๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ค๊ณ , value๊ฐ์๋ ${#authentication.principal.username} ํด๋น ๊ฐ์ ์ฌ์ฉํ๋ค.
package org.demo.springdemo.board.config;
@Configuration
@Log4j2
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class CustomSecurityConfig {
private final DataSource dataSource; // JDBC์์ DB์ฐ๊ฒฐ์ ์ํ ์ธํฐํ์ด์ค, application.yml์ datasource์ ๊ฐ
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("filterChain..............");
http.formLogin(config -> {
});
http.csrf(config -> {config.disable();});
http.rememberMe(config -> {
config.tokenValiditySeconds(60*60*24*30);
config.tokenRepository(persistentTokenRepository());
});
http.logout(config -> { // logout // http://localhost:8080/logout
});
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
repo.setDataSource(dataSource);
return repo;
}
}
๐ Datasource๋ฅผ ์ฃผ์
๋ฐ์์ ์ฌ์ฉํ๋๋ฐ, ์ด๋ jdbc์ ์ฐ๊ฒฐํ๋๋ฐ ํ์ํ๋ค.
๐ rememberMe๋ฅผ ํตํด์ ์ธ์
๊ณผ ๊ฐ์ ๊ฐ๋
์ผ๋ก ๋ก๊ทธ์ธ๊ธฐ๋ก์ ๊ธฐ์ตํ๋๋ก ํ๋ค.
๐ PersistentTokenRepository ํตํด ๋ก๊ทธ์ธํ ๊ธฐ๋ก์ db์ ๊ธฐ๋กํ๋๋กํ๋ค.
๐ logout ๋ก๊ทธ์์์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.

๐ ๋ง๋ค์ด๋์ ๊ณ์ ์์, ๋ก๊ทธ์ธ on์ ํด๋๊ณ

๐ ๋๋ค์๊ณผ ํ๋กํ์ฌ์ง์ ๋์๊ฐ ๊ฐ๋ฅํ๋ฐ, ์ด๋ฉ์ผ์ ๋ถ๊ฐ๋ฅํด์ ธ์๋ค. ๊ฐ์ธ์ ๋ณด๋์ ์ฌ์ฌ์ ์ฒญ์์ ์ฑ์์ด์ฝ๋ฑ๋กํ์ฌ ํ์ผ์ฌ์ง์ ํํ-> ๋น์ฆ์ฑ -> ๊ฐ์ธ๊ฐ๋ฐ์ ๋น์ฆ์ฑ์ ํ - ์ด๋ฉ์ผํ์๋์ ์ ํ

๐ ํ๋ซํผ์์ ํ๋จ ๋๋ฉ์ธ์ ์ถ๊ฐ ๋ฐ ์์ ํด์ฃผ๊ธฐ
๐ ํ๋จ์ redirect url ์ค์ ๋ค์ด๊ฐ์ ํ์ฑํ


๐ url ๋ณต์ฌํด๋๊ธฐ

๐ ๋ณด์ํค ๋ณต์ฌ

๐ (Rest Key) ๋ณต์ฌ
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
http.oauth2Login(config -> { // kakaoLogin
});
spring:
application:
name: springDemo
datasource:
driver-class-name: org.mariadb.jdbc.Driver
username: malldbuser
password: malldbuser
url: jdbc:mariadb://localhost:3306/malldb
hikari:
minimum-idle: 1
maximum-pool-size: 5
connection-timeout: 10000
thymeleaf:
cache: false
servlet:
multipart:
enabled: true
location: C:\\upload
max-file-size: 2MB
max-request-size: 20MB
security:
oauth2:
client:
registration:
kakao:
client-id: #Rest key
redirect-uri: http://localhost:8080/login/oauth2/code/kakao
client-authentication-method: client_secret_post
authorization-grant-type: authorization_code
scope: profile_nickname, account_email, profile_image
client-name: Kakao
client-secret: #๋ณด์ key
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
logging:
level:
com.zaxxer: info
org.springframework.jdbc: info
org.springframework.security: trace
org:
apache:
ibatis: debug
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: org.demo.springdemo.**.dto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
board:
upload: C:\\upload\\attach
package org.demo.springdemo.board.security;
@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService implements OAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("---------------");
log.info("---------------");
log.info(userRequest);
log.info("---------------");
log.info("---------------");
return null;
}
}

๐ kakao ๋ค์ด๊ฐ์ ๋ก๊ทธ์ธํ๋ฉด

๐ ํด๋น์ค๋ฅ๋ฐ์
package org.demo.springdemo.board.security;
@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("---------------");
log.info("---------------");
log.info(userRequest);
String serviceName = userRequest.getClientRegistration().getClientName();
OAuth2User oAuth2User = super.loadUser(userRequest);
java.util.Map<String, Object> paramMap = oAuth2User.getAttributes();
paramMap.forEach((k,v) -> {
log.info("key: "+k+" value: "+v);
});
log.info(serviceName); // kakao
LinkedHashMap accountMap = (LinkedHashMap) paramMap.get("account");
String email = (String) accountMap.get("email");
log.info(email);
return oAuth2User;
}
}
๐ servicename์ userRequest์์ ํด๋ผ์ด์ธํธ ๋ฑ๋ก์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ณ , getClientName์ผ๋ก ์ธ์ฆ์๋น์ค ์ด๋ฆ์ธ kakao๋ฅผ ๊ฐ์ ธ์จ๋ค.
๐ loaduser๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ OAuth2์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ก๋ํ๊ณ , OAuth2๊ฐ์ฒด์์ ์ฌ์ฉ์ ์์ฑ์ map์ ํํ์ธ ํค์ ๊ฐ์ ํํ๋ก ๊ฐ์ ธ์จ๋ค.
๐ ๊ทธ ์์ฑ์ค์ account์ ํด๋นํ๋ ๊ฐ์ LinkedHashMap์ผ๋ก ์บ์คํ ํ์ฌ ํ ๋นํ๊ณ , email๋ํ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ํ ๋นํ๋ค.
package org.demo.springdemo.board.dto;
@Data
public class MemberDTO implements UserDetails, OAuth2User {
private String mid;
private String mpw;
private String mname;
private List<String> roles;
private Map<String, Object> props; // ์ฌ์ฉ์์ ๋ณด์ ์์ฑ์ ์ ์ฅ
@Override
public Map<String, Object> getAttributes() { // ์ฌ์ฉ์์ ์์ฑ์ ์ ๋ณด๋ฅผ ๋ฐํํ๋๋ฐ ์ฌ์ฉ๋๋ ๋ฉ์๋
return props;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_"+role)).collect(Collectors.toList());
}
@Override
public String getPassword() {
return mpw;
}
@Override
public String getUsername() {
return mid;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String getName() {
return this.mid;
}
}
๐ DTO์์๋ props๋ฅผ ๋ง๋ค์ด map์ธ ํค์๊ฐ์ ํํ๋ก ๋ณ์๋ฅผ ์ ์ํ๊ณ , getAttribute๋ฅผ ํตํด์ ์ฌ์ฉ์์ ์์ฑ์ ์ ๋ณด๋ฅผ ๋ฐํํ๋๋ฐ ์ฌ์ฉ๋๋ ๋ฉ์๋๋ฅผ ์ ์ํ๋ค.
package org.demo.springdemo.board.security;
@Component
@Log4j2
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("userName: "+ username);
MemberDTO memberDTO = new MemberDTO();
memberDTO.setMid(username);
memberDTO.setMpw("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i");
memberDTO.setRoles(List.of("USER"));
return memberDTO;
}
}
๐ ํด๋น CustomLogin์ ์ผ๋ฐ์ ์ธ Login์ ๊ตฌํํ์ฌ ๋ง๋ค์๋ค.
package org.demo.springdemo.board.security;
@Component
@Log4j2
@RequiredArgsConstructor
public class SocialUserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("---------------");
log.info("---------------");
log.info(userRequest);
String serviceName = userRequest.getClientRegistration().getClientName();
OAuth2User oAuth2User = super.loadUser(userRequest);
java.util.Map<String, Object> paramMap = oAuth2User.getAttributes();
paramMap.forEach((k,v) -> {
log.info("key: "+k+" value: "+v);
});
log.info(serviceName); // kakao
LinkedHashMap accountMap = (LinkedHashMap) paramMap.get("account");
String email = (String) accountMap.get("email");
log.info(email);
MemberDTO memberDTO = new MemberDTO();
memberDTO.setMid(email);
memberDTO.setMpw("$2a$12$vt3AQj71d1s4j4YAe5JkjeHHVrmZli0g.hjEqnfziBJcV7MUHcM8i");
memberDTO.setRoles(List.of("USER"));
memberDTO.setProps(paramMap);
return oAuth2User;
}
}
๐ ํด๋น์ฝ๋๋ kakao์ ๊ฐ์ socailLogin๋ฐฉ์์ ์ํด ์์ฑ๋์๋ค.