Springboot Maven 환경에서 간단하게 적용하는법을 노트하였습니다.
1. pom.xml 에서 아래 코드를 추가해준다(필자는 security, jpa, mysql, lombok, Mustache 라이브러리를 사용함)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
2. application.yml 파일에 아래 설정을 추가해줌
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://본인ip:포트/db명?serverTimezone=Asia/Seoul
username: user명
password: 비밀번호
jpa:
hibernate:
ddl-auto: create #create update none # 첫 프로젝트 run시 create로 해서 Table생성 후 update로 변경
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
3. cofing 패키지를 만들고 WebMvcConfig 클래스를 생성해서 아래의 코드를 넣어준다.(view를 설정하는 Config class)
@Configuration // 이 자바파일을 IOC로 등록하기 위한 어노테이션
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
MustacheViewResolver resolver = new MustacheViewResolver();
resolver.setCharset("UTF-8");
resolver.setContentType("text/html; charset=UTF-8");
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
registry.viewResolver(resolver);
}
}
4. 객체로 쓸 Model 클래스를 생성한다(User)
>> @Entity 어노테이션을 사용하였기때문에 여기까지 작성하고 springboot App을 run하면 DB에 User 테이블이 생성되고, 아래의 객체들이 column으로 생성된다.(생성된 것 확인 후, application.yml에서 jpa:hibernate:ddl-auto를 create에서 update로 변경)
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.CreationTimestamp;
import lombok.Data;
@Entity
@Data
public class User {
@Id //primary key
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role; //ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
@CreationTimestamp
private Timestamp createDate;
}
5. Jpa를 사용하여서 DB와 통신할 것이기때문에 JpaRepository를 상속받은 Repository를 생성한다.(Mapper역할을 함)
import org.springframework.data.jpa.repository.JpaRepository;
import com.cos.security1.model.User;
// CRUD 함수를 JpaRepository가 들고 있음.
// @Repository라는 어노테이션이 없어도 IoC가 됨. 이유는 JpaRepository를 상속했기 때문에(자동 빈 등록됨)
public interface UserRepository extends JpaRepository<User, Integer> {
// findBy규칙 -> Username문법
// select * from user where username =1?
public User findByUsername(String username); //Jpa Query methods
// select * from user where email = ?
// public User findByEmail();
}
6. 로그인, 회원가입 화면단을 만든다(매우 간단하게)
index.html(초기화면)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>인덱스페이지</title>
</head>
<body>
<h1>인덱스페이지입니다.</h1>
</body>
</html>
loginForm.html(로그인 화면)
> input name을 userRepository의 객체명(ex. username)과 맞춰야함
<!DOCTYPE html>
<html>
<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="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
joinForm.html(회원가입 화면)
<!DOCTYPE html>
<html>
<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>
7. Controller를 하나 만들어서 아래의 코드를 추가해준다(로그인, 회원가입 페이지 빼고는 html 페이지는 만들지 않아서 @ResponseBody로 Return String값을 Json데이터로만 출력되게 하였다)
@Controller
public class IndexController {
@Autowired
private UserRepository userRepository;
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
public IndexController(BCryptPasswordEncoder bCryptPasswordEncoder) {
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@GetMapping({"","/"})
public String index() {
// 머스테치 기본폴더 src/main/resources/
// 뷰리졸버 설정 : templates (prefix), .mustache(suffix) 생략가능!! -> pom.xml에 mustache 의존성 주입했기 때문에
return "index"; // src/main/resources/tmplates/index.mustache
}
@GetMapping("/user")
public @ResponseBody String user() {
return "user";
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "admin";
}
@GetMapping("/manager")
public @ResponseBody String manager() {
return "manager";
}
// 스프링시큐리티가 해당주소를 낚아챔 - SecurityConfig 파일 생성 후 작동안함 -> formLogin부분 추가해줬음
@GetMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
@PostMapping("/join")
public String join(User user) {
user.setRole("ROLE_USER"); //회원가입 할때마다 ROLE_MANAGER, ROLE_ADMIN으로 수정하는 식으로 테스트
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user);
return "redirect:/loginForm";
}
그리고 BCryptPasswordEncoder를 spring bean에 등록하고, 주입받기 위해 클래스를 하나 생성해준다.
@Component
public class CustomBCryptPasswordEncoder extends BCryptPasswordEncoder {
}
8. 여기까지 진행했으면, 대망의 securityConfig(시큐리티 설정 관련) 클래스를 생성해준다.
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨
public class SecurityConfig {
// @Bean어노테이션은 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
// security 암호화 관련 메서드
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated() // 인증만 되면 들어갈 수 있는 주소
.antMatchers("/manager/**").access("hasAnyRole('ROLE_MANAGER','ROLE_ADMIN')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해줌
.defaultSuccessUrl("/");
return http.build();
}
}
9. 추가사항!! PrincipalDetails, PrincipalDetailsService 클래스를 만들어줘야한다.
이유는 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시키는데,
로그인 진행이 완료가 되면 시큐리티 session을 만들어준다. (Security ContenxtHolder라는 키값에 세션정보를 저장)
시큐리티가 가지고 있는 세션에 들어갈 수 있는 오브젝트는 정해져있는데, 그 오브젝트가 Authentication 타입 객체이다!
그 Authentication 안에는 User정보가 있어야 된다.
User오브젝트의 타입은 UserDetails 타입 객체이고, PrincipalDetails클래스에서 UserDetails를 implements 하여 Authentication 객체에 넣어야한다.
// 시큐리티가 /login을 낚아채서 로그인을 진행시킨다.
// 로그인을 진행이 완료가 되면 시큐리티 session을 만들어줍니다. (Security ContextHolder에 세션 정보 저장)
// 오브젝트 타입 => Authentication 타입객체
// Authentication 안에 User 정보가 있어야 됨.
// User오브젝트타입 => UserDetails 타입 객체
// Security Session => Authentication => UserDetails(PrincipalDetails)
@Data
public class PrincipalDetails implements UserDetails {
private User user; //콤포지션
private Map<String, Object> attributes;
public PrincipalDetails(User user) {
this.user = user;
}
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
// 해당 User의 권한을 리턴하는 곳!!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
// 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 한 경우에
// 현재시간 - 로긴시간 => 1년을 초과하면 return false;
return true;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return null;
}
}
또 여기서, Authentication 객체를 만드는 클래스가 필요한데, 이것이 PrincipalDetailsService 클래스이다.
// 시큐리티 설정에서 loginProcessingUrl("/login");
// /login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// 시큐리티 session(내부 Authentication(내부 UserDetails))
// 함수 종료시 @AuthenticationPrincipal 어노테이션이 만들어진다.
@Override // 중요!! html에 input name을 username으로 해줘야함!!!!
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity != null) {
return new PrincipalDetails(userEntity);
}
return null;
}
}
여기까지 설정했으면, 다시 springboot-app을 run해서 회원가입 후 테스트 진행 하면된다. 첫 회원가입 시 권한이 ROLE_USER기 때문에, user페이지만 접속가능하고, /logout url 호출시 security가 자동으로 로그아웃 시켜준다.
**다음 포스팅 에서는 OAuth2를 통한 소셜로그인(구글)을 알아보겠다!
'Languages | Frameworks > Spring' 카테고리의 다른 글
[Springboot] spring-security 적용기(2) - OAuth2 구글 소셜 로그인 (2) | 2022.11.02 |
---|---|
@Autowired, @Component, @Service, @Repository 등 스프링 어노테이션에 관해.. (0) | 2022.11.02 |
[Spring] 세션 타임아웃 (0) | 2022.06.02 |
[Spring] jsessionid를 url에 노출안시키기(쿠키를 통해서만 세션 유지) (0) | 2022.06.02 |
Cookie를 사용한 로그인/로그아웃(세션쿠키) + 보안 (0) | 2022.05.30 |