Spring Security 를 실습하고 기록해보자.
Spring Security 란?
Spring 에서 제공해주는 보안 솔루션 이다. 개발자가 직접 보완 관련 코드를 짤 필요없기 때문에 매우 간편하다. Spring Security 에서는 인증 (Authentication) 과 권한 (Authorization) 기능을 모두 제공하고 있어, 이 개념을 알아야 한다.
인증과 권한
인증 (Authentication) 과 권한 (Authorization) 을 살펴보면, 인증은 '나'='나' 라는 것을 확인하는 절차이고, 권한은 '나' 는 어느범위까지 사용이 가능한 지를 결정하는 것이다.
Spring Security 를 사용하는 이유
Require authentication to every URL in your application
Generate a login form for you
Allow the user with the Username user and the Password password to authenticate with form based authentication
Allow the user to logout
CSRF attack prevention
Session Fixation protection
Security Header integration
Integrate with the following Servlet API methods
>>출처<<
예제
예제 설명
Spring Security 를 이용하여 간단한 회원가입 / 로그인 기능 구현해보도록 하자. 이 과정에서 Spring Data JPA 도 사용되는데 이 주제는 다음에 다뤄보도록 하자.
총 4개의 화면으로 구성된다. + H2 Console 화면 (실제 사용자 정보가 저장되는 지 확인해보자)
로그인 화면
회원가입 화면
사용자 화면
Admin 화면
총 6개의 package 로 소스코드를 관리한다.
config
domain
repository
dto
service
controller
시작
1. Spring project 를 하나 생성하자.
https://start.spring.io
Gradle
Spring Boot 2.6.6
Java 8
Jar
2. 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
}
3. domain - Entity 정보 생성
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class UserInfo implements UserDetails {
@Id
@Column(name = "code")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long code;
@Column(name = "email", unique = true)
private String email;
@Column(name = "password")
private String password;
@Column(name = "auth")
private String auth;
@Builder
public UserInfo(String email, String password, String auth) {
this.email = email;
this.password = password;
this.auth = auth;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
for(String role : auth.split("," )) {
roles.add(new SimpleGrantedAuthority(role));
}
return roles;
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
회원가입 시 [이메일, 패스워드, 권한] 이 저장될 것이다. 여러개의 권한을 가질 수 있고, 콤마[,]로 구분되어 저장된다. 로그인이 될 때 권한 리스트를 불러와 Spring Security 가 인식할 것이다.
한가지 주목해야 할 점은 UserDetails 라는 인터페이스를 상속받는 것이다. 이것은 Spring 제공 하는 것으로 인증과 권한을 담당한다.
4. repository 생성
public interface UserRepository extends JpaRepository<UserInfo, Long> {
Optional<UserInfo> findByEmail(String email);
}
Repository 는 interface 로 생성하고 JpaRepository 를 상속 받는다.
5. dto 생성
@Getter
@Setter
public class UserInfoDto {
private String email;
private String password;
private String auth;
}
6. service 생성
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public Long save(UserInfoDto userInfoDto) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
userInfoDto.setPassword(encoder.encode(userInfoDto.getPassword()));
return userRepository.save(UserInfo.builder()
.email(userInfoDto.getEmail())
.password(userInfoDto.getPassword())
.auth(userInfoDto.getAuth()).build()
).getCode();
}
@Override
public UserInfo loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(email));
}
}
UserDetailsService 를 상속받고 loadUserByUsername 오버라이드 함수를 사용한다.
7. config 생성
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private final UserService userService;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/signup", "/user").permitAll()
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/").hasRole("USER")
.antMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf()
.ignoringAntMatchers("/h2-console/**")
.and()
.headers()
.frameOptions().sameOrigin()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.and()
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
}
WebSecurityConfigurerAdapter 를 상속 받는다. 세 개의 configure 메서드는 역할이 각각 다르다.
configure(WebSecurity web)
// -> static 하위 resource 디렉토리를 접근 불가한 리스트에서 제외할 수 있다.
configure(HttpSecurity http)
// -> http 관련 인증 설정을 담당한다.
antMatchers // 경로 권한 설정을 담당한다.
permitAll // 누구나 접근 가능
hasRole // 기재된 권한이 있는 경우 접근 가능
anyRequest // antMatchers 외 경로
authenticated // 권한 있으면 접근 가능
formLogin() // 로그인 설정
loginPage("/login") // 로그인 화면
defaultSuccessUrl("/") // 로그인 성공 시 이동할 화면
logout() // 로그아웃 설정
logoutSuccessUrl("/") // 로그아웃 시 이동할 화면
invalidateHttpSession(true); // 로그아웃 시 세션 제거 여부
configure(AuthenticationManagerBuilder auth)
// -> 로그인 인증을 담당하고, 사용자 정보를 불러온다.
8. controller 생성
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
@PostMapping("/user")
public String signup(UserInfoDto userInfoDto) {
userService.save(userInfoDto);
return "/login";
}
@GetMapping("/logout")
public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "/login";
}
}
GET 메소드로도 로그아웃이 가능하다. SecurityContextLogoutHandler 을 사용하면 된다.
이제 View 화면을 구성해보자.
login.html
signup.html
main.html
admin.html
Controller에 등록을 해도 되지만, WebMvcConfigurer 을 사용하여 이동할 화면을 매핑을 해보자.
9. MVC config 생성
@Configuration
public class MvcConfig implements WebMvcConfigurer{
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("main");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/admin").setViewName("admin");
registry.addViewController("/signup").setViewName("signup");
}
}
10. View 생성
login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h1>Login</h1> <hr>
<img src="/img/info.jpeg" />
<form action="/login" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
email : <input type="text" name="username"> <br>
password : <input type="password" name="password"> <br>
<button type="submit">Login</button>
</form> <br>
<a href="/signup">Go to join! →</a>
</body>
</html>
signup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>sign up</title>
</head>
<body>
<h1>Sign Up</h1> <hr>
<form th:action="@{/user}" method="POST">
email : <input type="text" name="email"> <br>
password : <input type="password" name="password"> <br>
<input type="radio" name="auth" value="ROLE_ADMIN,ROLE_USER"> admin
<input type="radio" name="auth" value="ROLE_USER" checked="checked"> user <br>
<button type="submit">Join</button>
</form> <br>
<a href="/login">Go to login →</a>
</body>
</html>
main.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
</head>
<body>
<h2>회원 전용 페이지</h2>
ID : <span sec:authentication="name"></span><br>
소유 권한 : <span sec:authentication="authorities"></span><br>
<form id="logout" action="/logout" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="submit" value="로그아웃"/>
</form>
</body>
</html>
admin.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>관리자 전용 페이지</h2>
ID : <span sec:authentication="name"></span> <br>
소유 권한 : <span sec:authentication="authorities"></span> <br>
<form id="logout" action="/logout" method="POST">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="submit" value="로그아웃"/>
</form>
</body>
</html>
11. Spring boot 실행
localhost:8080/ 접속하면 main.html 로 이동이 될 것이다. 근데 로그인 정보가 없으니 /login 화면으로 이동 됨.
사용자 정보가 없기 때문에 signup 을 해보자.
localhost:8080/signup
admin 을 선택하는 경우 DB 에 ROLE_USER, ROLE_ADMIN 두개가 들어간다.
H2 console 에서 조회해보면 아래와 같이 조회된다.
이제 로그인을 해보자.
ADMIN, USER 두 권한이 모두 존재하기 때문에, 회원용, admin용 화면 두 곳 모두 접속이 가능한 것을 볼 수있다.
로그아웃하면 다시 로그인 화면으로 이동한다.
다음으로는 사용자로 가입을 하고 화면 접근을 확인해보자.
localhost:8080/admin 으로 접속 시도 시 위와 같이 403 오류가 발생하는 것을 볼 수 있다.
이것으로 실습을 마치자..
Reference : https://shinsunyoung.tistory.com/78