728x90
스프링 부트 개념과 활용
4. 스프링 부트 활용
포스팅 참조 정보
GitHub
공부한 내용은 GitHub에 공부용 Organizations에 정리 하고 있습니다
해당 포스팅에 대한 내용의 GitHub 주소
실습 내용이나 자세한 소스코드는 GitHub에 있습니다
포스팅 내용은 간략하게 추린 핵심 내용만 포스팅되어 있습니다
해당 포스팅 참고 인프런 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard
실습 환경
- Java Version: Java 11
- SpringBoot Version: 2.1.2.RELEASE
11. 스프링 시큐리티 2부: 시큐리티 설정 커스터마이징
스프링부트가 제공하는 기본설정 두가지
WebSecurityConfigureAdapter
설정- Form인증과 Basic Authentication을 활성화 시켜줌
- 모든 요청이 다 Authentication이 필요하다고 정의
UserDetailsService
설정- 기본으로 랜덤한 유저를 생성해주는 건
UserDetailsServiceAutoConfiguration
- 기본으로 랜덤한 유저를 생성해주는 건
1단계 실습 - Web Security Configuration
- hello나 index페이지는 아무나 접근
- my 페이지만 인증을 한 사용자가 방문하도록
WebSecurityConfigurerAdapter
의 빈을 등록하면 SpringBoot가 제공하는SecurityAutoConfiguration
은 사용이 안됨- 정의 하는대로 동작함
thymeleaf, security 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Controller 작성
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/my")
public String my() {
return "my";
}
}
src/resources/template
에 index, hello, my html 생성
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Title</title>
</head>
<body>
<h1>Hello Spring Boot Security</h1>
<a href="/hello">Hello</a>
<a href="/my">My</a>
</body>
</html>
SecurityConfig 생성
- index와 hello는 인증을 예외해서 접근이 가능
- my는 Accept Header에 html이 있으므로 formLogin에 걸려서 로그인 인증요청 화면으로 이동
- html이 없는 경우에는 httpBasic에 걸림
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// index와 hello는 인증없이 접근이 가능
.antMatchers("/","/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
2단계 실습 - UserDetailsService 구현
스프링 부트에서 자동으로 유저를 만들어주는 것이 아닌 직접 Account를 관리 하도록
JPA와 H2 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
Account 클래스 생성
@Entity
public class Account {
@Id @GeneratedValue
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
AccountRepository 생성
JPA 설정이 되었고 H2를 추가하였으므로 자동으로 H2를 사용하도록 설정됨
public interface AccountRepository extends JpaRepository<Account, Long> {
}
AccountRunner 생성
로그인할 테스트 유저를 임의로 생성하기 위해 Runner 클래스를 생성함
@Component
public class AccountRunner implements ApplicationRunner {
@Autowired
AccountService accountService;
@Override
public void run(ApplicationArguments args) throws Exception {
Account freelife = accountService.createAccount("freelife", "1234");
System.out.println(freelife.getUsername() + " password: " + freelife.getPassword());
}
}
AccountService 생성
- 보통 Service에
UserDetailService
클래스를 상속 받아서 오버라이딩 해서 구현 UserDetailService
빈이 등록되면 스프링 부트 시큐리티가 더이상 랜덤으로 유저를 생성하지 않음
UserDetailService
interface 검증 순서
- 오버라이딩된 loadUserByUsername 에서 입력받은 username이 들어옴
- username에 해당하는 실제 유저정보를 확인(UserDetails)
- 확인된 유저정보의 패스워드와 입력한 패스워드가 같으면 로그인 처리 다르면 예외 처리
loadUserByUsername 로직 처리 과정
- byUsername에 실제 데이터가 없으면 UsernameNotFoundException 를 예외로 던짐
- 있으면 account가 실제 account로 나옴
- return은 UserDetails라는 인터페이스 구현체를 return
- UserDetails라는 인터페이스는 서비스에 구현되어있는 유저정보들의 인터페이스
- 가장 핵심적인 유저정보들의 중요한 정보들을 담고있는 인터페이스
- 이 인터페이스의 기본 구현체를 스프링 시큐리티가 User라는 이름으로 제공해줌
- 특정권한을 가진 유저임을 설정하고 우리가 가지고 있는 asccount 정보를 UserDetails로 변환하는 과정
- 변환을 하면 스프링 시큐리티가 username과 password를 확인해서 로그인할때 입력한 사용자 정보가 유효한지 확인
@Service
public class AccountService implements UserDetailsService {
@Autowired
private AccountRepository accountRepository;
public Account createAccount(String username, String password) {
Account account = new Account();
account.setUsername(username);
account.setPassword(password);
return accountRepository.save(account);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Account> byUsername = accountRepository.findByUsername(username);
Account account = byUsername.orElseThrow(() -> new UsernameNotFoundException(username));
return new User(account.getUsername(), account.getPassword(), authorities());
}
private Collection<? extends GrantedAuthority> authorities() {
// ROLE_USER 이라는 권한을 가진 유저임을 설정
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
}
3단계 실습 - PasswordEncoder 설정 및 사용
PasswordEncoder 예외 발생
로직을 다 구현하고 테스트 해보면 아래와 같은 예외사항이 발생
ava.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
- 스프링 시큐리티 버전이 올라가면서 다양한 인코딩을 지원
- password 포맷이
{noop}password
여야지 아무것도 인코딩하지 않는 패스워드 인코더로 디코딩을 시도 - 아무것도 없는 경우 위와 같이 예외가 발생
NoOpPasswordEncoder
- 실제로는 사용하면 안되지만 예외적으로 회피할 수 있는
NoOpPasswordEncoder
로 로직 처리 - 스프링 시큐리티가 사용할 Encoder가 더이상 기본 패스워드가 아닌
NoOpPasswordEncoder
로 변환됨
패스워드 앞에 있는 prefix 값을 보고 어떤 Encoding인지 확인 한 다음에 Encoding Decoding하는 똑똑한 패스워드가 아니라
일부러 그 빈을 Encoding Decoding 아무것도 하지 않는 NoOp 그런 패스워드 Encoder로 바꾼 것임
이렇게는 절대로 사용하면 안됨
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
권장하는 PasswordEncoder
아래는 스프링 시큐리티가 권장하는 PasswordEncoder
으로 설정
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
SecurityConfig에 PasswordEncoder 빈으로 등록하도록 설정 추가
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// index와 hello는 인증없이 접근이 가능
.antMatchers("/", "/hello").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
AccountService PasswordEncoder를 사용하도록 로직 수정
PasswordEncoder
을 주입을 받아서 Encoding 된 password를 저장하도록 수정
@Autowired
private PasswordEncoder passwordEncoder;
public Account createAccount(String username, String password) {
Account account = new Account();
account.setUsername(username);
account.setPassword(passwordEncoder.encode(password));
return accountRepository.save(account);
}
결과 확인
아래와 같이 bycrypt로 Encoding 된 것을 확인 할 수 있다
freelife password: {bcrypt}$2a$10$hPZ8YCYcvoVtNxwp/FttWuwzAxx.9SVtDfNY.TGl44IOgFLB9.SN2
728x90
'개발강의정리 > Spring' 카테고리의 다른 글
[스프링 부트 개념과 활용] 4-12. 스프링 REST 클라이언트 2부: 커스터마이징 (0) | 2019.12.01 |
---|---|
[스프링 부트 개념과 활용] 4-12. 스프링 REST 클라이언트 1부: RestTemplate과 WebClient (0) | 2019.11.30 |
[스프링 부트 개념과 활용] 4-11. 스프링 시큐리티 1부: spring-boot-starter-security (0) | 2019.11.28 |
[스프링 부트 개념과 활용] 4-10. 스프링 데이터 12부: 정리 (0) | 2019.11.27 |
[스프링 부트 개념과 활용] 4-10. 스프링 데이터 11부: Neo4j (0) | 2019.11.26 |
댓글