1. 인증과 인가
- 인증(Authentication)은 애플리케이션에서 특정 Resource(디렉터리, 파일 등)에 대해 특정 작업을 수행할 수 있는 주체인지 확인하는 과정
- 인가(Authorization)는 권한부여 또는 허가와 같은 의미로 인증 이후의 과정
- 일반적으로 인증을 통해 사용자를 식별하고(로그인) 인가(Role)를 통해 시스템 자원에 대한 접근을 통제
2. Spring Security란
- 인증과 인가 관련 처리와 보안 관련 처리를 제공하는 라이브러리
- Servlet Filter 기반으로 동작하고, 다양한 기능들을 Filter(컨트롤러 앞단에서 사용되는 컴포넌트)로 제공
✔ UsernamePasswordAuthenticationFilter(인증)
✔ FilterSecurityInterceptor(인가)
3. Spring Security 동작방식
- Spring Security는 Security Filter들의 상호작용에 의해 처리
- UsernamePasswordAuthenticationFilter는 사용자가 입력한 정보를 이용해 인증을 처리
- FilterSecurityInterceptor는 인증에 성공한 사용자가 해당 리소스에 접근할 권한이 있는지를 검증(인가)
[실습] Spring Security
프로젝트 생성
Security 설정은 설정파일(application.properties)에서 하는게 아니라 별도의 ClathPath에서 설정할것임.
설정파일에서는 기본 설정과 DataSource, JPA 설정, MyBatis설정, Logger설정을 해줌.
# 기본 Setting
spring.devtools.livereload.enabled=true
spring.thymeleaf.cache=false
server.servlet.context-path=/Ch09
# DataSource 설정 (MySQL)
spring.datasource.url=jdbc:mysql://ip주소:3306/db명
spring.datasource.username=user
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### DataSource 설정 (Oracle) ###
#spring.datasource.url=jdbc:oracle:thin:@127.0.0.1:1521/orcl
#spring.datasource.username=scott
#spring.datasource.password=tiger
#spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
# JPA 설정
# ddl-auto=create : 엔티티를 기준으로 기존 테이블 삭제 후 다시 생성
# ddl-auto=update : 엔티티를 기준으로 기존 테이블 수정(개발용)
# ddl-auto=none : 어떠한 테이블도 생성하지 않음(운영용)
spring.jpa.database=mysql
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.use_sql_comments=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.ddl-auto=update
# MyBatis 설정
mybatis.mapper-locations=classpath:mappers/**/*.xml
# Logger 설정
logging.level.root=info
logging.level.jdbc.sqlonly=info
logging.level.jdbc.sqltiming=info
logging.level.jdbc.connection=info
logging.level.jdbc.resultsettable=info
logging.level.org.hibernate=info
logging.level.org.springframework.security=debug
logging.file.name=log/Ch09.log
인덱스 페이지와 컨트롤러 생성 후
Run시켜보면 자동으로 로그인 페이지가 뜸
시큐리티 라이브러리를 프로젝트 생성 시 추가했었는데 자동으로 적용됐기 때문
필터가 먼저 인증된 사용잔지 확인을 하는 것
인증,인가에 대한 설정이 안돼있으니까 스프링에서 자체적으로 로그인페이지를 띄워주는 것
username과 password를 콘솔에서 확인할 수 있으나 설정파일에서 설정해주었다.
이렇게도 설정 가능!
로그인을 하면 인증되어서 메인페이지로 넘어감
기본적인 시큐리티가 잘 작동함을 알 수 있다.!
- Spring Security 설정
인증처리과정에서 컴포넌트가 실행이되는데, Manager가 실행되면서 Provider를 또 실행.
Provider에서 비밀번호 검사가 끝나고 UserDetailService에서의 loadUserByUsername(String username)으로 username이 들어오는것!
이게 또 UserDetailService 실행 (=SecurityUserService.java) 이 컴포넌트가 하는 역할은 DB에서 확인.
인증이 됐다면(아이디비번이 맞다면) UserDetails라는 인증객체를 생성. 이게 MyUserDetails.java
즉, 그림으로 보면
★★★★★스프링 시큐리티 로직★★★★★
1. 사용자가 로그인 정보와 함께 인증 요청 (Http Request)
2. AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성함.
3. AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달
4. AuthenticationManager는 등록된 AuthenticationProvider(들)을 조회해서 인증을 요구함.
5. 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨줌.
6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듦.
7. AuthenticationProveder(들)은 UserDetails를 넘겨받고 사용자 정보를 비교함.
8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환함.
9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됨.
10. Authentication 객체를 SecurityContext에 저장함.
최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장함.
사용자 정보를 저장한다 = Spring Security가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다
-- MyUserDetails.java
- implements UserDetails
- 속성 추가 가능
- 권한 리턴
- sessUser가 된다고 생각해도 무방
package kr.ch09.security;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@Builder
@ToString
public class MyUserDetails implements UserDetails{
private static final long serialVersionUID = -5532680704133363159L;
// 속성 추가
private String uid;
private String pass;
private String name;
private int age;
private String hp;
private String role; // USER, MANAGER, ADMIN
private LocalDateTime regDate;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한을 리턴해주는 메서드(권한이 여러개 있는 유저가 있기때문에 리스트반환)
// 계정이 갖는 권한 목록
List<GrantedAuthority> authorities = new ArrayList<>();
// 접두어로 "ROLE_" 입력하면 hasRole(), hasAnyRole() 메서드가 처리되고,
// "ROLE_" 접두어를 쓰지않으면 hasAuthority(), hasAnyAuthority() 메서드로 처리됨
authorities.add(new SimpleGrantedAuthority(role));
return authorities; // 권한목록을 리스트로 리턴
}
@Override
public String getPassword() { // 비밀번호를 리턴하는 Getter
// 계정이 갖는 비밀번호
return pass;
}
@Override
public String getUsername() { // 시큐리티에서 유저네임은 사용자이름을 의미하는게 아니라 Id를 의미
// 계정이 갖는 아이디
return uid;
}
@Override
public boolean isAccountNonExpired() {
// 계정 만료 여부(true:만료X, false:만료), false=해당계정 로그인 불가
return true;
}
@Override
public boolean isAccountNonLocked() {
// 계정 잠김 여부(true:안잠김, false:잠김)
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// 계정 비밀번호 만료 여부(true:만료X, false:만료), 비밀번호 변경하게끔
return true;
}
@Override
public boolean isEnabled() {
// 계정 활성화 여부(true:활성화, false:비활성화)
return true;
}
}
-- SecurityConfigration.java
- 시큐리티 전역설정
- SecurityUserService(아래) 주입받음
- filterChain @Bean 등록
ㄴ 인가설정, 사이트위변조 방지설정, 로그인/아웃 설정, 사용자 인증처리 컴포넌트 등록 - passwordEncoder @Bean 등록
ㄴ 비밀번호 인코더 메서드
package kr.ch09.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.AuthorizeHttpRequestsDsl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfigration {
@Autowired
private SecurityUserService service;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 인가 설정
http.authorizeHttpRequests(
AuthorizeHttpRequests ->
AuthorizeHttpRequests
.requestMatchers("/").permitAll()
.requestMatchers("/admin/**").hasAuthority("ADMIN")
.requestMatchers("/manager/**").hasAnyAuthority("ADMIN", "MANAGER")
.requestMatchers("/user/**").permitAll()
);
// 사이트 위변조 방지 설정
http.csrf(CsrfConfigurer::disable);
// 로그인 설정
http.formLogin(
login -> login
.loginPage("/user/login")
.defaultSuccessUrl("/user/loginSuccess")
.failureUrl("/user/login?success=100")
.usernameParameter("uid")
.passwordParameter("pass")
);
// 로그아웃 설정
http.logout(
logout -> logout
.invalidateHttpSession(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/user/login?success=200")
);
// 사용자 인증처리 컴포넌트 등록
http.userDetailsService(service);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
-- SecurityUserService.java
- 서비스객체. 라서 Service 어노테이션 선언
- implements UserDetailsService
- UserRepository 주입 받음
- loadUserByUsername 오버라이딩
- loadUserByUsername 메서드가 DB로 질의를 보냄
ㄴ 사용자 인증객체 생성 (세션에 저장)
package kr.ch09.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import kr.ch09.entity.UserEntity;
import kr.ch09.repository.UserRepository;
@Service
public class SecurityUserService implements UserDetailsService {
@Autowired
private UserRepository repo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 패스워드에 대한 검사는 이전 컴포넌트(AuthenticationProvider)에서 처리되어 사용자 아이디만 넘어옴
UserEntity user = repo.findById(username).get();
// 사용자 인증객체 생성(세션에 저장)
UserDetails userDetails = MyUserDetails.builder()
.uid(user.getUid())
.pass(user.getPass())
.name(user.getName())
.age(user.getAge())
.hp(user.getHp())
.role(user.getRole())
.regDate(user.getRegDate())
.build();
return userDetails;
}
}
- 기타 MVC 설정
-- Entity 생성
-- UserRepository 생성
-- service 생성
DTO는 생성안함 (생성한다면 지난 실습때처럼 entity와 dto간 변환 필요)
-- 확인
Entity로 생성된 테이블
테스트 실행을 위해 templates 생성
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org"
xmlns:sec="http://thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Ch09::index</title>
</head>
<body>
<h3>Ch09. Spring Security 실습</h3>
<h4>인증 테스트</h4>
<!-- 비로그인 사용자만 보이는 블럭 -->
<th:block sec:authorize="isAnonymous()">
<a th:href="@{/user/register}">User 회원가입</a>
<a th:href="@{/user/login}">User 로그인</a>
</th:block>
<!-- 권한이 있는 사용자만 보이는 블럭 -->
<th:block sec:authorize="isAuthenticated()">
<a th:href="@{/user/logout}">로그아웃</a>
</th:block>
<h4>인가 테스트</h4>
<!-- 권한이 ADMIN인 사람만 보이는 블럭 -->
<th:block sec:authorize="hasAuthority('ADMIN')">
<a th:href="@{/admin/index}">관리자 페이지</a>
</th:block>
<!-- 권한이 ADMIN, MANAGER인 사람만 보이는 블럭 -->
<th:block sec:authorize="hasAnyAuthority('ADMIN', 'MANAGER')">
<a th:href="@{/manager/index}">매니저 페이지</a>
</th:block>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>admin::index</title>
</head>
<body>
<h3>Admin 페이지</h3>
<p>
admin 인증 확인
<a href="@{/}">메인으로</a>
</p>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>manager::index</title>
</head>
<body>
<h3>manager 페이지</h3>
<p>
manager 인증 확인
<a href="@{/}">메인으로</a>
</p>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>user::login</title>
</head>
<body>
<h3>User 로그인</h3>
<a th:href="@{/}">메인으로</a>
<form action="#" method="post">
<input type="text" name="uid">
<input type="password" name="pass">
<input type="submit" value="로그인">
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://thymeleaf.org"
xmlns:sec="http://thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>user::success</title>
</head>
<body>
<h3>로그인 성공</h3>
<!-- 권한이 있는 사용자만 보이는 블럭 -->
<p sec:authorize="isAuthenticated()">
아이디 : <span>[[${#authentication.principal.uid}]]</span><br>
이름 : <span>[[${#authentication.principal.name}]]</span><br>
나이 : <span sec:authentication="principal.age"></span><br>
휴대폰 : <span sec:authentication="principal.hp"></span><br>
권한 : <span sec:authentication="principal.role"></span><br>
가입일 : <span sec:authentication="principal.regDate"></span><br>
<a th:href="@{/user/logout}">로그아웃</a>
</p>
<a th:href="@{/}">메인으로</a>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>user::register</title>
</head>
<body>
<h3>User 회원가입</h3>
<a th:href="@{/}">메인으로</a>
<form th:action="@{/user/register}" method="post">
<table border="1">
<tr>
<td>아이디</td>
<td><input type="text" name="uid"></td>
</tr>
<tr>
<td>비밀번호</td>
<td><input type="password" name="pass"></td>
</tr>
<tr>
<td>이름</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>나이</td>
<td><input type="number" name="age"></td>
</tr>
<tr>
<td>휴대폰 번호</td>
<td><input type="text" name="hp"></td>
</tr>
<tr>
<td colspan="2" align="right">
<input type="submit" value="회원가입">
</td>
</tr>
</table>
</form>
</body>
</html>
-- UserController 생성
권한에 따라 페이지에 보여지는 내용이 달라지게 됨