목차
Spring Security란?
Spring을 사용할 때 애플리케이션에 대한 인증, 권한 부여 등의 보안 기능을 제공하는 프레임워크이다.
다양한 로그인 방법(Form태그, OAuth2, JWT...)에 대해 Spring이 어느정도 구현해두었으니 수정(확장)해서 사용만 하면된다.
Spring Security 동작방법
간단히 요약하면
1. 아이디, 비밀번호를 가진 요청이 들어온다
2. Form 로그인이면 UserDetailsService의 loadUserByUsername메서드가 실행되고
OAuth2 로그인이면 OAuth2UserService의 loadUserByUsername메서드가 실행된다.
3. loadUserByUsername메서드는 "이런 정보가 들어왔는데 얘 혹시 회원이야?" 라고 묻는 메서드이다.
= loadUserByUsername에서는 회원을 찾아주는 로직을 구현하면된다.
4. 이때, 회원정보는 Form 로그인이면 UserDetails타입으로, OAuth2 로그인이면 OAuth2User타입으로 반환해준다.
5. UserDetails 또는 OAuth2User를 반환하면 Spring에서 알아서 Session에 저장해준다.
조금 더 정확하게 말하자면,
6. Spring Security의 in-memory 세션 저장소인 SecurityContextHolder에 인증객체를 저장한다.
7. SecurityContextHolder에 들어갈 수 있는 인증객체는 Authentication타입 1가지이다.
8. Authentication은 AbstractAuthenticationToken으로 구현되어있고, AbstractAuthenticationToken을
UsernamePasswordAuthenticationToken과 OAuth2LoginAuthenticationToken이 구현하고있다.
9. 우리가 5번에서 UserDetails, OAuth2User를 반환하면 Spring이 알아서
UserDetails는 UsernamePasswordAuthenticationToken으로,
OAuth2User는 OAuth2LoginAuthenticationToken으로 변환하고
10. UsernamePasswordAuthenticationToken과 OAuth2LoginAuthenticationToken은
Authentication의 자식이니 SecurityContextHolder에 저장할 수 있게된다.
Spring Security 및 Thymeleaf설정하기
build.gradle > dependency 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
패키지 > DemoApplication > BCryptPasswordEncoder 빈으로 등록
Spring Security는 BCryptPasswordEncoder를 반드시 사용해야한다. (비밀번호 보안때문)
@SpringBootApplication
public class DemoApplication {
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Spring Security - Form Login하기
완성된 모습
Form Login설정
패키지 > config 폴더 > SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
// user주소에 대해서 인증을 요구한다
.antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
// manager주소는 ROLE_MANAGER권한이나 ROLE_ADMIN권한이 있어야 접근할 수 있다.
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
// admin주소는 ROLE_ADMIN권한이 있어야 접근할 수 있다.
.anyRequest().permitAll(); // 나머지주소는 인증없이 접근 가능하다
}
}
.authorizeRequests() | Security처리에 HttpServletRequest를 이용한다 ExpressionUrlAuthorizationConfigurer을 불러온다. |
.antMatchers("경로") | 특정 경로 지정 |
.anyRequest() | 설정한 경로 외의 모든 경로 |
.authenticated() | 인증된 사용자만 접근할 수 있다 |
.permitAll() | 인증없이 접근할 수 있다 |
.denyAll() | 인증없이는 접근할 수 없다 |
.access(String str) | SpEL표현식의 결과가 true이면 접근할 수 있다 |
.hasRole(String role) | 사용자가 해당되는 Role이 있다면 접근할 수 있다 |
.hasAnyRole(String role) | 사용자가 가진 Role 중 해당되는 Role이 하나라도 존재한다면 접근할 수 있다 |
.anonymous() | 익명사용자가 접근할 수 있다 |
.rememberMe() | rememberMe인증 사용자가 접근할 수 있다. |
패키지 > config 폴더 > SecurityConfig.java 내용 추가
Spring Security는 별다른 설정 없이도 "/login", "/logout" URL을 등록하고 처리방법도 모두 구현해두었다.
따라서 커스텀한 로그인페이지를 구성하고자하면 아래와 같이 추가한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/admin/**").hasRole("ROLE_ADMIN")
.anyRequest().permitAll()
.and() //추가
.formLogin() // form기반의 로그인인 경우
.loginPage("/loginForm") // 인증이 필요한 URL에 접근하면 /loginForm으로 이동
.usernameParameter("id") // 로그인 시 form에서 가져올 값(id, email 등이 해당)
.passwordParameter("pw") // 로그인 시 form에서 가져올 값
.loginProcessingUrl("/login") // 로그인을 처리할 URL 입력
.defaultSuccessUrl("/") // 로그인 성공하면 "/" 으로 이동
.failureUrl("/loginForm") //로그인 실패 시 /loginForm으로 이동
.and()
.logout() // logout할 경우
.logoutUrl("/logout") // 로그아웃을 처리할 URL 입력
.logoutSuccessUrl("/"); // 로그아웃 성공 시 "/"으로 이동
}
}
.formLogin() | form기반으로 로그인 할 경우의 설정을 추가할 수 있다 FormLoginConfigurer를 불러온다. |
.loginPage(String url) | 로그인 페이지 경로를 호출한다. default값은 "/login" 이다. |
.usernameParameter(String str) | 스프링 시큐리티에서 사용자를 구분할 수 있는 값을 가져온다. default값은 "username"으로, <form>의 <input name="username">이면 작성하지 않아도 된다. <input name="id">인 경우에 작성해준다. |
.passwordParameter(String str) | 스프링 시큐리티에서 사용자를 인증할 수 잆는 값을 가져온다. default값은 "password"로, <form>의 <input name="password">이면 작성하지 않아도 된다. |
.loginProcessingUrl(String url) | 로그인을 처리할 url을 설정한다. default값은 "/login" 이다. <form> 태그의 action속성과 맞추어준다. |
.defaultSuccessUrl(String url) | 로그인 성공 시 url로 이동한다 |
.failureUrl(String url) | 로그인 실패 시 url로 이동한다. default값은 "/login?error" 이다. |
.logout() | 로그아웃 시 설정을 추가할 수 있다. LogoutConfigurer을 불러온다. |
.logoutUrl(String url) | 로그아웃을 처리할 url을 설정한다. default값은 "/logout" 이다. 로그아웃 버튼의 href속성과 맞추어준다. |
.logoutSuccessUrl(String url) | 로그아웃 성공 시 url로 이동한다 |
+ 참고
loginProcessingUrl은 사용자가 입력한 url에 인증과정을 추가해준다.
//AbstractAuthenticationFilterConfigurer.java
public T loginProcessingUrl(String loginProcessingUrl) {
this.loginProcessingUrl = loginProcessingUrl;
this.authFilter.setRequiresAuthenticationRequestMatcher(createLoginProcessingUrlMatcher(loginProcessingUrl));
// authFilter = UsernamePasswordAuthenticationFilter이다. (formLogin에 걸리는 체인이니까)
return getSelf();
}
FormLogin Authentication UserDetails
패키지 > auth 폴더 > PrincipalDetails.java
Form Login에 사용되는 Authentication은 UserDetails이다.
추후 OAuth2 확장을 위해 UserDetails를 implements한다
package com.example.demo.auth;
@Getter
@ToString
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
/**
* UserDetails 구현
* 해당 유저의 권한목록 리턴
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole().toString();
}
});
return collect;
}
/**
* UserDetails 구현
* 비밀번호를 리턴
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* UserDetails 구현
* PK값을 반환해준다
*/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* UserDetails 구현
* 계정 만료 여부
* true : 만료안됨
* false : 만료됨
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* UserDetails 구현
* 계정 잠김 여부
* true : 잠기지 않음
* false : 잠김
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* UserDetails 구현
* 계정 비밀번호 만료 여부
* true : 만료 안됨
* false : 만료됨
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* UserDetails 구현
* 계정 활성화 여부
* true : 활성화됨
* false : 활성화 안됨
*/
@Override
public boolean isEnabled() {
return true;
}
}
FormLogin loadUserByUsername
패키지 > auth 폴더 > PrincipalDetailsService.java
UserDetailsService는 FormLogin시 loadUserByUsername 메서드로 로그인한 유저가 DB에 저장되어있는지를 찾는다.
찾으면 앞에서 구현한 Authentication(UserDetails를 구현한 PrincipalDetails)을 반환하여 SecurityContextHolder에 저장할 수 있게 한다.
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User byUsername = userRepository.findByUsername(username);
if(byUsername != null){
return new PrincipalDetails(byUsername);
}
return null;
}
}
FormLogin Domain구현
패키지 > domain 폴더 > Role.java
package com.example.demo.domain;
public enum Role {
ROLE_USER, ROLE_MANAGER, ROLE_ADMIN
}
패키지 > domain 폴더 > User.java
package com.example.demo.domain;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.*;
import java.sql.Timestamp;
import java.time.LocalDateTime;
@Entity
@Getter @ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@Setter
private String password;
private String email;
@Enumerated(EnumType.STRING)
@Setter
private Role role;
@CreationTimestamp //자동으로 만들어준다
private Timestamp createTime;
@Builder(builderClassName = "UserDetailRegister", builderMethodName = "userDetailRegister")
public User(String username, String password, String email, Role role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
}
FormLogin Controller 구현(Controller에서 인증 객체 가져오기)
패키지 > controller 폴더 > UserController.java
package com.example.demo.controller;
@Controller
public class UserController {
@Autowired private UserRepository userRepository;
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/loginForm")
public String loginForm(){
return "login";
}
@GetMapping("/joinForm")
public String joinForm(){
return "join";
}
@PostMapping("/join")
public String join(@ModelAttribute User user){
user.setRole(Role.ROLE_USER);
String encodePwd = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(encodePwd);
userRepository.save(user); //반드시 패스워드 암호화해야함
return "redirect:/loginForm";
}
@GetMapping("/user")
@ResponseBody
public String user(){
return "user";
}
@GetMapping("/manager")
@ResponseBody
public String manager(){
return "manager";
}
@GetMapping("/admin")
@ResponseBody
public String admin(){
return "admin";
}
// !!!! OAuth로 로그인 시 이 방식대로 하면 CastException 발생함
@GetMapping("/form/loginInfo")
@ResponseBody
public String formLoginInfo(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails){
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
User user = principal.getUser();
System.out.println(user);
//User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
User user1 = principalDetails.getUser();
System.out.println(user1);
//User(id=2, username=11, password=$2a$10$m/1Alpm180jjsBpYReeml.AzvGlx/Djg4Z9/JDZYz8TJF1qUKd1fW, email=11@11, role=ROLE_USER, createTime=2022-01-30 19:07:43.213, provider=null, providerId=null)
//user == user1
return user.toString();
}
}
FormLogin View구현 (타임리프)
resources > templates > join.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>
resources > templates > login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<h2>로그인 유저 : </h2>
<p sec:authentication="principal"></p>
<div sec:authorize="isAnonymous()" style="background-color:pink; padding:1em;">
<form action="/login" method="post" >
<input type="text" name="username" />
<input type="password" name="password" />
<button>로그인</button>
</form>
<br>
<a href="/joinForm">회원가입하기</a><br>
</div>
<div sec:authorize="isAuthenticated()" style="background-color:pink; padding:1em;">
<a href="/logout">로그아웃</a>
</div>
<br><br>
<a href="/user">유저</a><br>
<a href="/manager" sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')">매니저</a><br>
<a href="/admin" sec:authorize="hasRole('ROLE_ADMIN')">어드민</a><br>
<br><br>
<a href="/form/loginInfo">Form 로그인 정보</a>
</body>
</html>
sec:authentication="principal" | 인증(=로그인)된 사용자의 정보를 모두 출력한다 |
sec:authorize="isAnonymous()" | 인증되어있지 않다면 해당 부분을 볼 수 있다. (반대로 인증되어있으면 보이지 않는다) |
sec:authorize="isAuthenticated()" | 인증되어있으면 해당 부분을 볼 수 있다. (반대로 인증되어있지 않다면 보이지 않는다) |
sec:authorize="hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')" | ROLE_MANAGER 또는 ROLE_ADMIN 권한이 있다면 해당 부분을 볼 수 있다. |
OAuth2 동작과정
Spring Security - OAuth Google 로그인
너무 길어져서 다음 게시글로..
Spring Security - OAuth 네이버 로그인
Spring Security - OAuth 카카오 로그인
참고
spring security 동작방법, spring security 동작원리, 스프링 시큐리티, Form로그인, FormLogin, OAuth로그인, OAuth2로그인, OAuthLogin, OAuth2Login, 스프링 시큐리티 구글로그인, 스프링 시큐리티 네이버로그인, 스프링 시큐리티 카카오로그인, spring security google login, spring security naver login, spring security kakao login, 구글로그인 연동, 네이버로그인 연동, 카카오로그인 연동, 스프링시큐리티 기본개념, 스프링시큐리티 기본원리
'Backend' 카테고리의 다른 글
[Spring Security] OAuth 네이버 로그인하기 (10) | 2022.02.02 |
---|---|
[Spring Security] OAuth 구글 로그인하기 (0) | 2022.02.02 |
[JPA] Java Persistence API 등장배경, 사용방법 (0) | 2022.01.28 |
[MyBatis] 동작원리, 사용방법 정리 (0) | 2022.01.27 |
[JDBC] 사용방법 (0) | 2022.01.26 |