제일 처음으로 Spring-Boot프로젝트에 Spring-Security, MariaDB 드라이버,Thymeleaf 확장 플러그인을 추가해줌
다음으로 application.properties
파일에 데이터 베이스와 JPA관련 설정과 시큐리티와 관련된 부분의 로그 레벨을 설정해줌
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/bootex
spring.datasource.username=bootuser
spring.datasource.password=bootuser
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
spring.thymeleaf.cache=false
logging.level.org.springframework.security.web=trace
logging.level.com.crow=debug
이후 제대로 설정된건지 알아보기 위해서 프로젝트를 실행해서 로그에 출력된 자동으로 생성된 임시 패스워드와 user라는 이름으로 로그인을 해봄
로그인을 성공해도 현재 프로젝트 내부에 컨트롤러가 없기 때문에 Whitelabel Error Page가 나옴
(비밀번호를 틀릴시 자격 증명에 실패했다고 나옴)
http://localhost:8080/login
프로젝트 내 src/main/java/내패키지이름
경로에 config 패키지를 추가해주고 SecurityConfig 클래스를 작성해줌
여기서 5.7.x버전은 이전 버전들과 다르게
WebSecurityConfigurerAdapter
를 extends 받아서 override를 통해서 구현했지만
5.7이후 버전부턴 @EnableWebSecurity
을 클래스에 선언해주고 @Bean
을 메서드에 사용해서 구현해야함
package com.crow.oauthex.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@Log4j2
@EnableWebSecurity
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/sample/all").permitAll()
.antMatchers("/sample/member").hasRole("USER");
http.formLogin();
http.csrf().disable();
http.logout();
return http.build();
}
}
패스워드를 인코딩
가장 많이 사용되는BCryptPasswordEncoder
클래스를 사용함
(bcrypt
해쉬 사용 원래대로 복호화 불가능 매번 동일 길이의 다른 값이 설정됨)
스프링부트 2.0부턴 반드시 PasswordEncoder
를 지정해야 함
특정한 URL 접근을 제한
SecurityFilterChain
구현
HttpSecurity
객체는 .
을 이용해서 연속적 처리하는 빌더 방식을 이용함
http.authorizeRequests()
로 인증이 필요한 자원을 설정할 수 있고,
.antMatchers()
와 같은 앤트 스타일 패턴으로 원하는 자원을 선택해주면됨
.permitAll()
은 모든 사용자들에게 접근이 가능하게 해줌
해당 코드는 /sample/all
은 모든사용자가 접근 가능하고 /sample/member
는 USER
라는 롤을 가진 사용자만 접근이 가능하게 만듬
http.formLogin()
은 인가/인증에 실패하면 로그인 페이지를 보여줌
시큐리티의 CSRF(크로스 사이트 요청 위조)공격 방식을 막기위해 발행한 토큰은 disable해주고
logout도 처리해줌
(CSRF 토큰은 보안상이점을 위해서 사용하는게 좋지만
이건 테스트 이기 때문에 사용하지 않음)
package com.crow.oauthex.controller;
import com.crow.oauthex.security.dto.OAuthMemberDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@Log4j2
@RequestMapping("/sample/")
public class SampleController {
@GetMapping("/all")
public void exAll() {
log.info("exAll...............");
}
@GetMapping("/member")
public void exMember(@AuthenticationPrincipal OAuthMemberDTO oAuthMemberDTO) {
log.info("exMember...............");
}
@GetMapping("/admin")
public void exAdmin() {
log.info("exAdmin..................");
}
}
프로젝트에 스프링 시큐리티를 적용하기 위해서는 이에 맞는 데이터베이스 관련 처리가 필요함
com.crow.oauthex.entity
패키지를 추가하고
OAuthMember.java OAuthMemberRole.java BaseEntity.java
구현
각각 회원 정보/회원 권한/수정일,생성일 관리용도임
package com.crow.oauthex.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Getter
abstract class BaseEntity {
@CreatedDate
@Column(name = "regdate" , updatable = false)
private LocalDateTime regDate;
@LastModifiedDate
@Column(name ="moddate" )
private LocalDateTime modDate;
}
@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
이 부분에서 사용한 어노테이션들은 JPA 설계 공통 칼럼을 적용하여 다른 Entity에 상속해줌
(주로 생성일 수정일등 Date)
또한 BaseEntity.class
를 이용하기 위해선 프로젝트 생성시 만들어진 OAuthExApplication.class
에
@EnableJpaAuditing
어노테이션을 추가해야함
package com.crow.oauthex;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class OAuthExApplication {
public static void main(String[] args) {
SpringApplication.run(OAuthExApplication.class, args);
}
}
package com.crow.oauthex.entity;
import lombok.*;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import java.util.HashSet;
import java.util.Set;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class OAuthMember extends BaseEntity {
@Id
private String email;
private String password;
private String name;
private boolean fromSocial;
@ElementCollection(fetch = FetchType.LAZY)
@Builder.Default
private Set<OAuthMemberRole> roleSet = new HashSet<>();
public void addMemberRole(OAuthMemberRole OAuthMemberRole) {
roleSet.add(OAuthMemberRole);
}
}
BaseEntity
를 상속 받아서 생성일 수정일적용
PK키
는 소셜 로그인을 감안해서 이메일로 적용함
(요즘 많이 쓰는 트렌드임)
OAuthMemberRole
타입 값을 처리하기 위한Set<OAuthMemberRole>
타입을 추가하고
지연 로딩방식(fetch = FetchType.LAZY)사용
package com.crow.oauthex.entity;
public enum OAuthMemberRole {
USER, MANAGER, ADMIN
}
유저 권한은 USER, MANAGER, ADMIN으로 설계함
이후 프로젝트가 실행되면
2개의 테이블이 생성됨
package com.crow.oauthex.repository;
import com.crow.oauthex.entity.OAuthMember;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface OAuthMemberRepository extends JpaRepository<OAuthMember, String> {
@EntityGraph(attributePaths = {"roleSet"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("select m from OAuthMember m where m.fromSocial = :social and m.email =:email")
Optional<OAuthMember> findByEmail(String email, boolean social);
}
findByEmail()
은 사용자의 이메일과 소셜로 추가된 회원 여부를 선택해서 동작하도록 설계
@EntityGraph
을 이용해서 left outer join
으로 OAuthMemberRole
이 처리될 수 있도록함
package com.crow.oauthex.security;
import com.crow.oauthex.entity.OAuthMember;
import com.crow.oauthex.entity.OAuthMemberRole;
import com.crow.oauthex.repository.OAuthMemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashSet;
import java.util.Optional;
import java.util.stream.IntStream;
@SpringBootTest
public class OAuthMemberTests {
@Autowired
private OAuthMemberRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void insertDummies() {
//1 - 80까지는 USER만 지정
//81- 90까지는 USER,MANAGER
//91- 100까지는 USER,MANAGER,ADMIN
IntStream.rangeClosed(1,100).forEach(i -> {
OAuthMember oAuthMember = OAuthMember.builder()
.email("user"+i+"@test.com")
.name("사용자"+i)
.fromSocial(false)
.roleSet(new HashSet<OAuthMemberRole>())
.password( passwordEncoder.encode("1111") )
.build();
//default role
oAuthMember.addMemberRole(OAuthMemberRole.USER);
if(i > 80){
oAuthMember.addMemberRole(OAuthMemberRole.MANAGER);
}
if(i > 90){
oAuthMember.addMemberRole(OAuthMemberRole.ADMIN);
}
repository.save(oAuthMember);
});
}
@Test
public void testRead(){
Optional<OAuthMember> result = repository.findByEmail("user95@test.com",false);
OAuthMember OAuthMember = result.get();
System.out.println(OAuthMember);
OAuthMember.addMemberRole(OAuthMemberRole.USER);
}
}
insertDummies()
는 더미 데이터를 추가해서 제대로 롤이 구현됬는지 테스트하는 코드임
testRead()
는 OAuthMemberRepository.interface
에
구현된 findByEmail()
를 테스트하는 코드임
(일반 로그인 사용자와 소셜 로그인 사용자 구분)
JPA
로 회원정보를 처리하는데 문제가 없다는 것을 확인했다면
다음으론 OAuthMemberRepository
를 이용해서 회원 정보를 처리하는 부분을 구현해야함
일반 로그인 처리와 다르게 스프링 시큐리티는UserDetailsService
를 상속받아
loadUserByUsername()
를 오버라이딩 해서 사용함
여기서 크게 두가지 방식으로 나뉨
기존 DTO 클래스에 UserDetails
인터페이스를 구현하거나, DTO와 같은 개념으로 별도의 클래스를 구성하는 방식
이중 별도의 클래스를 구성하는 방식을 사용하겠음
프로젝트 내 security 패키지를 구성하고 내부에 dto, service 패키지를 구성
package com.crow.oauthex.security.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Log4j2
@Getter
@Setter
@ToString
public class OAuthMemberDTO extends User {
private String email;
private String name;
private boolean fromSocial;
public OAuthMemberDTO(String username,
String password,
boolean fromSocial,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.email = username;
this.fromSocial = fromSocial;
}
}
DTO를 구성하는 첫번째 단계는 User 클래스를 상속하고
부모 클래스인 User 클래스의 생성자를 호출할 수 있는 코드를 만드는것임
(부모 클래스인 User 클래스에 사용자 정의 생성자가 있으므로 반드시 호출 해야함)
package com.crow.oauthex.security.service;
import com.crow.oauthex.entity.OAuthMember;
import com.crow.oauthex.repository.OAuthMemberRepository;
import com.crow.oauthex.security.dto.OAuthMemberDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.stream.Collectors;
@Log4j2
@Service
@RequiredArgsConstructor
public class AuthUserDetailsService implements UserDetailsService {
private final OAuthMemberRepository OAuthMemberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("-------------------");
log.info("ClubUserDetailsService loadUserByUsername " + username);
Optional<OAuthMember> result = OAuthMemberRepository.findByEmail(username, false);
if (result.isEmpty()) {
throw new UsernameNotFoundException("Check Email or social");
}
OAuthMember OAuthMember = result.get();
log.info("-------------------");
log.info(OAuthMember);
log.info("-------------------");
OAuthMemberDTO clubAuthMember = new OAuthMemberDTO(
OAuthMember.getEmail(),
OAuthMember.getPassword(),
OAuthMember.isFromSocial(),
OAuthMember.getRoleSet().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
.collect(Collectors.toSet())
);
clubAuthMember.setName(OAuthMember.getName());
clubAuthMember.setFromSocial(OAuthMember.isFromSocial());
return clubAuthMember;
}
}
loadUserByUsername()
를 오버라이드 받아서
AuthUserDetailsService
와 OAuthMemberRepository
를 연동
username
이 실제로는 Email
을 의미하므로 이를 이용해서
findByEmail()
호출 소셜 여부는 false임
사용자가 존재 하지 않는다면 UsernameNotFoundException
처리
OAuthMember
를 UserDetails
타입으로 처리 하기 위해서
OAuthMemberDTO
타입으로 변환함
OAuthMemberRole
은 시큐리티에서 사용하는 SimpleGrantedAuthority
로 변환 이때 "ROLE_"
이라는 접두어 추가해줌
이후 /sample/member
에 접근해서 로그인 한다면 For Member
라는 내용을 확인가능
(로그인은 아까 테스트에서 추가한 계정들로 해주면됨)
참조
코드로 배우는 스프링부트 웹프로젝트
스타트 스프링부트
페이지 다듬어서 h1태그별로 포스트하고 시리즈로 정리