SpringSecurity 란 : https://postitforhooney.tistory.com/entry/SpringSecurity-%EC%B4%88%EB%B3%B4%EC%9E%90%EA%B0%80-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-Spring-Security-%ED%8D%BC%EC%98%B4
SpringSecurity sample 공식 문서 : https://spring.io/guides/topicals/spring-security-architecture/
SpringSecurity Reference : https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#web-app-security
백기선님 SpringSecurity 강좌 : https://www.youtube.com/watch?v=fG21HKnYt6g
1. SpringSecurity 로그인 과정
1.1 사용자가 요청을 한다.
1.2 사용자가 요청한 서비스가 로그인이 필요하다.
1.3 SpringSecurity는 SpringSecurityContext에서 Authentication
이라는 객체를 찾는다.
1.4 이때 Authentication 객체가 존재하지 않는다면 사용자에게 Login 페이지를 보여준다.
1.5 사용자가 로그인 정보를 입력하고 로그인을 하면 사용자가 입력한 Id에 해당하는 UserDetails
를 읽어와서 사용자가 입력한 정보들과 비교를 한다. (UserDetails는 각기 다른 어플리케이션의 User Model을 추상화한 것.)
1.6 로그인에 성공하면 Authentication
객체를 SpringSecuritContext에 담는다.
2. SpringSecurity 관련 객체
2.1 UserDetails
앞서 말한 것처럼 우리 어플리케이션내의 User
에 해당하는 Model에 UserDetails
구현하여 SpringSecurity가 이해할 수 있는 형태의 User로 만들어주어야 합니다.
2.2 UserDetailsService
UserDetailsService
는 DataBase로부터 사용자를 가져와 로그인처리 로직을 구현하는 역할을 합니다.
UserDetailsService를 구현하면 loadUserByUsername()
을 구현하게 되며, 이때 당연히 return 값으로 UserDetails
를 구현한 객체를 return해야 합니다.
이때 우리 어플리케이션의 User
를 나타내는 객체에 UserDetails
를 구현해도 되고, 이를 좀더 편리하게 사용할 수 있도록 SpringSecurity에서 UserDetails를 구현하여 만든 클래스로 User
라는 클래스가 존재하는데 이를 확장하여 그것을 리턴해도 됩니다. (이때 Authority의 경우에는 DB의 정보를 보고 직접 만들어 전달해주어야 합니다.)
3. Role
허가(Permission)
을 다루기 위해서 SpringSecurity에서는 Authority를 사용합니다. 예를 들어 User
의 Role을 가지는 계정은, /admin/**
이하의 요청이 불가능하며, ADMIN
Role 을 가지는 계정만이 요청이 가능한다던지의 처리를 할 때 필요한 개념입니다. 예제에서 다룰 것입니다.
4. PasswordEncoder
패스워드 저장시 암호화를 돕는 객체입니다.
Spring 5.X 버전 이후 부터 변화된 암호화 정책에 의해서 Password의 암호화 알고리즘을 변환시 이 암호화를 풀어서 다시 암호화 해야하는데 이것을 돕기 위해 SpringSecurity 에서 DelegatingPasswordEncoder
를 사용하게 되었습니다.
이러한 변화 때문에 암호화시 패스워드의 앞단에 {id}
가 붙어서 저장이 되게 됩니다. 그런데 이때 패스워드를 입력받은 그대로 저장한다면 당연히 패스워드 앞에 {id}
가 존재하지 않기 때문에, 위와 같은 에러를 내뱉습니다.
5. 예제
5.1 SpringMVC Config
public class SpringMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/home").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
우선 테스트를 위한 것이므로 핸들러를 직접 생성하여 여러 설정하는 것을 생략하기 위해, WebMvcConfigurer
를 구현하고, addViewControllers
를 구현하여, 간단히 테스트에 사용될 요청들을 추가합니다.
5.2 Template 작성
위에서 SpringMvcConfig
에서 추가한 Controller에 대응하는 Template들을 생성합니다. login.html은 추후에 작성하고, 나머지 home, hello
를 먼저 작성합니다.
home.html
x
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
hello.html
xxxxxxxxxx
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
우선 여기까지 작성한 후 실행한 결과입니다. 성공적으로 페이지가 나타납니다.
5.3 SpringSecurity Config
xxxxxxxxxx
implementation 'org.springframework.boot:spring-boot-starter-security'
이제 본격적으로 SpringSecurity 관련 설정을 진행하겠습니다. 우선 build.gradle
에 spring-boot-starter-security
를 추가합니다.
xxxxxxxxxx
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home", "/login").permitAll()
.antMatchers("/hello").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout();
}
}
그 후, SpringSecurity에게 우리 어플리케이션이 어느 요청에는 인증이 필요한지 안한지
를 알려주어야 합니다. SpringSecurity는 Servlet의 Filter를 기반으로 동작하는데, 위의 설정이 즉 custom filter를 생성하는 것 입니다.
우선 SpringSecurityConfig 클래스를 생성합니다. 그 후 @Configuration, @EnableWebSecurity
어노테이션을 부여합니다. 그 후 설정을 위해 WebSecurityConfigurerAdapter
를 확장합니다.
configure(HttpSecurity http)
를 오버라이딩 하여 설정을 진행합니다. antMatchers().permitAll()
는 인증없이 사용자의 접근을 허용하는 url을 작성합니다.
.anyRequest().authenticated()
의 경우 나머지 모든 요청에 대해서 인증을 요구하도록 하는 코드 입니다.
formLogin().loginPage("/login").permitAll()
은 사용자가 인증이 필요한 페이지에 접근하여 리다이렉팅 되는 url입니다. 우리는 앞서 viewController를 설정할 때 /login
으로 접근하는 사용자를 우리가 작성한 login.html
을 보여주도록 했습니다. 또한 permitAll()
을 꼭해주어야 사용자가 /login
url로 접근이 가능합니다.
5.4 login.html 폼
xxxxxxxxxx
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password : <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
username, password 를 name으로 가지는 input을 통해서 사용자가 로그인정보를 spring security에게 전달하도록 합니다. 또한 요청하는 url로 앞서 SpringSecurityConfig
에서 지정한 /login
으로 폼을 제출하도록 합니다.
5.5 우리 App에서 사용할 User Model 생성
xxxxxxxxxx
package com.study.security2.account;
public class Account {
private Integer id;
private String email;
private String password;
private String authority;
// getter setter ..
}
우리 App에서 사용자를 의미하는 모델입니다. 기본적으로 식별자로 사용될 id를 가지며, userid 로 사용될 이메일과 password, 그리고 허가를 부여할 때 사용될 authority를 추가합니다.
5.6 회원 추가 로직 구현
AccountRepository
xxxxxxxxxx
public class AccountRepository {
private Map<String, Account> accounts = new HashMap<>();
public Account save(Account account) {
Random random = new Random();
account.setId(random.nextInt());
accounts.put(account.getEmail(), account);
return account;
}
public Account findByEmail(String username) {
return accounts.get(username);
}
}
임시 Database의 역할을 하게 될 Repository 클래스 입니다. Map을 통해서 사용자들을 가지게 됩니다.
xxxxxxxxxx
public class AccountController {
private AccountRepository accountRepository;
"/create") (
public Account createAccount() {
Account account = new Account();
account.setEmail("galid1@naver.com");
account.setPassword("1234");
account.setAuthority("ROLE_USER");
return accountRepository.save(account);
}
}
회원 가입 페이지와 로직을 구현하기 귀찮기 때문에, GetMapping
을 통해 /create
로 요청이 오게 되면 임시로 사용자를 생성하여 Repository에 저장하도록 합니다. 중요한 점은 authority에는 꼭 ROLE_*
과 같은 형태로 저장을 해야한다는 것 입니다. Spring Security에서 권한을 다룰때 자체적으로 PREFIX로 ROLE_
을 추가하기 때문입니다.
xxxxxxxxxx
.antMatchers("/", "/home", "/login", "/create").permitAll()
사용자가 /create
로 요청을 보낼 수 있도록 SpringSecurityConfig
클래스에 /create
를 추가합니다.
여기까지 작성한 후 테스트를 해보면 성공적으로 사용자가 생성되어 리턴되는것을 확인할 수 있습니다.
5.7 UserDetailsService를 이용하여 로그인 처리 구현
UserDetailsService
를 구현하면 SpringSecurity 이용시 우리의 DataBase에서 User들을 찾아서 로그인을 하도록 구현할 수 있습니다.
xxxxxxxxxx
public class AccountService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
별도의 로그인 처리 클래스를 생성하여 UserDetailsService를 구현해도 되지만 Service클래스에 구현하여 처리를 한다고도 합니다. 저는 Service 클래스에 구현하였습니다.
우선 로그인이 어떻게 되어야 할지 생각을 해봅시다. 사용자가 email을 전달한다면 해당 email과 매칭되는 우리 DataBase에 저장되어있는 계정의 정보를 가져와야 할 것입니다.
AccountRepository
xxxxxxxxxx
public Account findByEmail(String username) {
return accounts.get(username);
}
때문에 accountRepository
에 email을 통해 계정을 가져올 수 있는 findByEmail() 메소드를 구현했습니다.
AccountService
xxxxxxxxxx
public class AccountService implements UserDetailsService {
private AccountRepository accountRepository;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = repository.findByEmail(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(account.getAuthority()));
return new User(account.getEmail(), account.getPassword(), authorities);
}
}
UserDetailsService를 구현하면 loadUserByUsername()
을 구현하게 되는데 이 메소드에서 우리 App의 계정 모델을 SpringSecurity에서 처리가능한 계정의 형태인 UserDetails
형태로 변환하여 반환을 해야합니다.
저는 SpringSecurity에서 제공하는 UserDetails를 구현한 User를 이용했습니다. User는 app에서 사용되는 계정 모델을 UserDetails로의 변환을 조금더 수월하게 할 수 있도록 도와주며 생성자에는 3가지 매개변수를 필요하는데 각각 아이디, 비밀번호, 권한리스트 입니다.
우선 클래스에서 방금 생성한 findByEmail() 메소드를 이용해 Account
객체를 가져옵니다. 그후 가져온 account에서 email, password를 꺼내어 User
의 첫번째, 두번째 매개변수로 전달합니다.
User
의 마지막 매개변수인 List<GrantedAuthority>
는 사용자가 가지고있는 권한들이 담긴 리스트로 GrantedAuthority 객체들이 담겨있습니다. 권한리스트에는 GrantedAuthority를 구현한 SimpleGrantedAuthority를 생성하여 담아주면 됩니다.
여기까지 진행한 후 /create
로 다시 요청을 보내어 계정을 생성한 뒤 로그인을 시도합니다. 이때 아무런 반응이 나타나지 않을 것인데 이때 Spring 을 확인하면..
위 그림과 같은 에러가 타나났을 것입니다. 이부분이 바로 4.PasswordEncoder
부분에서 다루었던 내용입니다.
5.8 PasswordEncoder
위의 에러를 제거하기 위해서는 PasswordEncoder를 사용하여 사용자가 입력한 password에 암호화를 거쳐서 {id}
가 자동으로 붙도록 만들거나, Encoder를 사용하지 않을 것이라면 암호 앞에 임의로 {noop}
을 추가로 부여하면 됩니다.
5.8.1 {noop}
위와 같이 계정을 생성하는 요청을 처리하는 핸들러에서 암호 앞부분에 {noop}
을 추가하면 끝입니다.
성공적으로 로그인이 됩니다.
5.8.2 PasswordEncoder 사용
SpringSecurityConfig
xxxxxxxxxx
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
SpringSecurityConfig 클래스에 PasswordEncoder Bean을 추가적으로 등록하는 코드를 작성합니다.
AccountService
xxxxxxxxxx
private AccountRepository repository;
private PasswordEncoder passwordEncoder;
public Account save(Account account) {
return repository.save(account);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = repository.findByEmail(username);
account.setPassword(passwordEncoder.encode(account.getPassword()));
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(account.getAuthority()));
//test
System.out.println(account.getPassword());
return new User(account.getEmail(), account.getPassword(), authorities);
}
AccountService클래스에 위와같이 PasswordEncoder를 사용하여 Password를 암호화하는 로직을 추가합니다. 암호화 결과를 보기 위해 account의 password를 콘솔에 출력합니다.
console을 확인하면 위 그림과 같이 암호 앞쪽에 자동으로 {bcrypt}
가 추가된것을 볼 수 있으며, 로그인에 성공하는 것을 볼 수 있습니다.
'FrameWork > Spring Security' 카테고리의 다른 글
Spring Security - SpringSecurity 이용하기 4 (Database를 이용한 Login 구현) (4) | 2020.02.23 |
---|---|
Spring Security - SpringSecurity 이용하기 3 (logout 기능 추가하기) (0) | 2020.02.23 |
Spring Security - SpringSecurity 이용하기 2 (login Success Handle) (0) | 2020.02.22 |
Spring Security - SpringSecurity 이용하기 1 (custom login form 구현) (2) | 2020.02.12 |
SpringSecurity - Kakao OAuth2 Client 사용하기 (8) | 2019.07.06 |