- Spring Boot DevTools
- Lombok
- Spring Web
- Spring Data JPA
- H2 Database
- OAuth2 Client
- Thymeleaf
위 라이브러리들을 추가하여 스프링 시큐리티 예제를 만들어보자.
implementation 'org.springframework.boot:spring-boot-starter-security'
아 그리고 제일 중요한 Security를 추가하지 않았는데 이 때는 build.gradle에 위 코드를 추가하여 시큐리티를 설치하자.
설치가 끝나고 스프링을 시작하여 http://localhost:8080으로 바로 들어가면 스프링 시큐리티가 제공하는 로그인페이지가 나온다.
이렇게 패키지를 만들어두고 이 구조를 기준으로 설명하겠다.
먼저 domain 패키지에 User 엔티티를 생성해보자.
User 클래스
/domain/User
@Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role; // ROLE_USER, ROLE_ADMIN
private String provider;
private String providerId;
@CreationTimestamp // INSERT 쿼리 시 현재 시간으로 생성
private Timestamp createDate;
@Builder
public User(String username, String password, String email, String role, String provider, String providerId, Timestamp createDate) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.provider = provider;
this.providerId = providerId;
this.createDate = createDate;
}
}
User 클래스를 간단히 설명하자면 빠르게 사용하려고 롬복의 @Data를 추가하여 Setter,Getter,ToString 등을 사용할 수 있다.
role 에는 역할을 지정해둘 컬럼을 만든 것이고, @CreationTimestamp는 주석처럼 INSERT 시 현재 시간으로 생성된다.
Builder패턴 생성자를 사용했지만 그냥 생성자를 사용해도 무방하다.
UserRepository 인터페이스
/repository/UserRepository
public interface UserRepository extends JpaRepository<User, Integer> {
public User findByUsername(String username);
}
스프링 데이터 JPA를 사용하여 손쉽게 CRUD 메서드를 사용할 수 있다.
Interface로 생성 후 JpaRepository<클래스, PK타입> 을 상속받으면 된다.
그 후 데이터 JPA 문법에 따라 findBy 를 적고 Username을 적었는데 이것은 "select * from user where username = 값" 이 된다.
SecurityConfig 클래스
/config/SecurityConfig
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록 된다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); //csrf 비활성화
http.authorizeRequests()
.antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있는 주소
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해준다.
.defaultSuccessUrl("/");
}
}
WebSecurityConfigureAdapter를 상속받아서 시큐리티를 커스터마이징하면 된다.
현재는 BCryptPasswordEncoder로 BCrypt 암호화를 사용하게된다. 추후 암호화를 커스터마이징할 때 변경하게 된다.
configure 메서드안에 antMatchers로 경로를 적고, access로 권한을 설정하게 된다.
formLogin() 메서드만 쓰면 스프링 시큐리티가 제공하는 로그인 페이지만 사용하는 것이고,
그 후 loginPage메서드로 경로를 설정하면 내가 만든 커스텀 페이지로 로그인 페이지를 변경하게 된다.
loginProcessingUrl은 로그인페이지에서 "/login" 경로로 post신호를 보내면 시큐리티쪽으로 username과 password를 가져온다.
defaultSuccessUrl은 성공 시 이동할 Url이다.
여기서 따로 로그아웃은 설정하지 않았으므로 기본값인 "/logout"이 로그아웃을 하는 경로이다.
IndexController 클래스
/controller/IndexController
@Controller
@RequiredArgsConstructor
public class IndexController {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping({"", "/"})
public String index() {
return "index"; // src/main/resources/templates/index.html
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "admin";
}
@GetMapping("/manager")
public @ResponseBody String manager() {
return "manager";
}
@GetMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
@PostMapping("/join")
public String join(User user) {
System.out.println(user);
user.setRole("ROLE_USER");
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user);
return "redirect:/loginForm";
}
}
컨트롤러에서 권한마다 들어갈 수 있는 경로들을 설정했다.
admin은 전부다, manager는 manager까지, user는 admin과 manager를 들어갈 수 없다.
왜냐하면 SecurityConfig에서 경로를 들어갈 수 있는 권한들을 설정했기 때문이다.
UserRepository와 BCryptPasswordEncoder를 생성자 주입하여 사용했다.
이제 페이지를 설정해보자.
페이지는 전부 resources/templates 폴더 안에다가 생성한다.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>인덱스페이지</title>
</head>
<body>
<h1>인덱스페이지입니다.</h1>
</body>
</html>
joinForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr>
<form action="/join" method="POST">
<input type="text" name="username" placeholder="Username"><br>
<input type="password" name="password" placeholder="Password"><br>
<input type="email" name="email" placeholder="email"><br>
<button>회원가입</button>
</form>
</body>
</html>
loginForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>로그인페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr>
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username"><br>
<input type="password" name="password" placeholder="Password"><br>
<button>로그인</button>
</form>
<a href="/oauth2/authorization/google">구글 로그인</a>
<a href="/oauth2/authorization/facebook">페이스북 로그인</a>
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
application.yml
설정 파일도 만들어준다.
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: org.h2.Driver # 해당 DB 드라이버
url: jdbc:h2:tcp://localhost/~/h2데이터베이스 경로
username: DB 아이디
password: DB 비밀번호
jpa:
hibernate:
ddl-auto: create
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
이 때 h2 Database를 사용해서 저렇게 적는데 오라클이나 MySQL은 따로 드라이버를 변경해서 설정해주면 된다.
이 상태로 스프링을 시작하여 localhost8080 으로 들어가면 인덱스 페이지가 나온다.
localhost:8080/user 를 들어가려고하니 로그인을 하라고 http://localhost:8080/loginForm 경로로 자동으로 들어가진다.
회원가입을 하지 않았으므로 회원가입을 한다.
회원가입을 클릭하면 위 경로로 들어가서 회원가입을 하게 된다.
그 후 로그인페이지로 가서 로그인을 하려고하는데 여기서 넘어가지 않는다.
로그인 부분을 살펴보자.
UserDetails는 시큐리티에서 사용자 정보를 담는 인터페이스이다.
로그인 진행이 완료되면 시큐리티 세션을 만들어주는데 이 안에는 Authentication 타입만 들어올 수 있다.
Authentication 안에는 User 정보가 있어야하는데 이 때 UserDetails와 OAuth2User 타입이 들어올 수 있다.
이 때 일반 로그인과 OAuth 로그인(SNS 소셜로그인)으로 나뉘는데 이러면 각각 구현해야하므로 이 두 타입을 구현한 클래스를 생성한다.
PrincipalDetails 클래스
/config/auth/PrincipalDetails
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user;
private Map<String, Object> attributes;
// 일반 로그인 생성자
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth 로그인 생성자
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // User 권한을 리턴하는 메서드
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() { // User 비밀번호 리턴
return user.getPassword();
}
@Override
public String getUsername() { // User PK 또는 고유한 값을 리턴
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() { // User 계정 만료 여부 리턴
return true; // true : 만료 안됨
}
@Override
public boolean isAccountNonLocked() { // User 계정 잠김 여부 리턴
return true; // treu : 잠기지 않음
}
@Override
public boolean isCredentialsNonExpired() { // User 비밀번호 만료 여부 리턴
return true; // true : 만료 안됨
}
@Override
public boolean isEnabled() { // User 계정 활성화 여부 리턴
return true; // true : 활성화 됨
}
// === OAuth2User 메서드 === //
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
}
OAuth2User 메서드에서 attributes가 Map<String, Object> 타입인데 이것은 구글, 네이버처럼 소셜 로그인을 시도하면 정보가
{ "id" : "1234",
"email" : "user@naver.com"}
이런 식으로 오기 때문이다.
이 클래스는 시큐리티가 사용자의 정보를 담는 클래스라고 보면 된다.
PrincipalDetailsService 클래스
/config/auth/PrincipalDetailsService
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity != null) {
return new PrincipalDetails(userEntity);
}
return null;
}
}
시큐리티에서 유저의 정보를 가져오는 클래스이다.
SecurityConfig에서 loginProcessingUrl("/login")을 설정했는데 이 /login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC가 되어있는 loadUserByUsername 함수가 실행된다.
loadUserByUsername 메서드로 로그인한 유저가 DB에 있는지 찾는 것이다.
찾으면 UserDetails와 OAuth2User를 구현한 PrincipalDetails를 반환하여 SecurityContextHolder에 저장할 수 있게 한다.
즉, 시큐리티 세션(내부 Authentication(내부 PrincipalDetails)) 인 것이다.
메서드가 종료되면 @AuthenticationPrincipal 어노테이션이 만들어진다.
IndexController 코드 추가
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
System.out.println("principalDetails : " + principalDetails.getUser());
return "user";
}
지금까지 만든 것으로 스프링을 시작하여 일반 회원가입을 하면 인덱스페이지로 들어가지고
localhost:8080/user 까지는 들어가지지만 manager와 admin은 들어갈 수 없을 것이다.
일단 회원가입으로 admin과 manager를 만들어주고 DB에서 값을 변경하자.
update user set role = 'ROLE_ADMIN' where username = 'admin';
update user set role = 'ROLE_MANAGER' where username = 'manager';
이렇게 role의 값을 변경해준다음에 각각 로그인하여 localhost:8080/admin과 /manager로 들어가보자.
로그아웃은 localhost:8080/logout 이다.
지금까지 일반로그인과 회원가입을 해보았다. 다음에는 소셜 로그인을 구현해보자.
'Spring > Spring' 카테고리의 다른 글
[Lombok] @NoArgsConstructor, @ToString (0) | 2022.04.11 |
---|---|
스프링부트 시큐리티 - SNS 로그인과 SNS 회원가입 (0) | 2022.03.17 |
IntelliJ 파일 업로드 주의사항 (2) | 2022.03.16 |
Entity 보다는 DTO로 반환하자. (0) | 2022.03.16 |
스프링부트 View 환경설정 (0) | 2022.01.24 |