Spring Security : Spring MVC 기반 애플리케이션의 인증과 인가 기능을 지원하는 보안 프레임워크
적용하기 위해서는 의존성을 추가해줘야한다.
implementation 'org.springframework.boot:spring-boot-starter-security'
Principal주체 : 애플리케이션에서 작업을 수행하는 유저, 디바이스, 시스템 등등. 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 말한다
Authentication인증 : 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차. 인증을 위해서는 Credential(신원증명정보. 사용자를 식별하기 위한 정보)가 필요하다
Authorization(인가, 권한부여) : Authentication이 수행된 사용자에게 하나 이상의 authority권한을 부여해 특정 리소스에 접근할 수 있게 허가하는 과정
Access Control접근 제어 : 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것
Spring Security를 추가해주는 것만으로도 제공해주는 디폴트 로그인 페이지, 로그인 정보가 있다.
디폴트 로그인 정보의 username은 user,
password는 로그에 출력된다.
Using generated security password: 39666a24-34e7-46cb-a34a-02d9130b4106
This generated password is for development use only. Your security configuration must be updated before running your application in production.
2023-07-10 11:06:28.291 INFO 9156 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@26d02dc6, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@24d7c365, org.springframework.security.web.context.SecurityContextPersistenceFilter@147892be, org.springframework.security.web.header.HeaderWriterFilter@3e7fc07e, org.springframework.security.web.csrf.CsrfFilter@64a8851a, org.springframework.security.web.authentication.logout.LogoutFilter@402fdef1, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@261b27db, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@dc24732, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@26b150cd, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@48f95f96, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@3533d790, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@23e1f610, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@62ff3028, org.springframework.security.web.session.SessionManagementFilter@1a61f634, org.springframework.security.web.access.ExceptionTranslationFilter@28d37d43, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@68908be7]
디폴트 로그인 정보를 설정할 수 있다.
UserDetailsManager 객체를 빈으로 등록하면 해당 빈의 인증 정보가 넘어올 경우, 인증 프로세스를 성공적으로 통과한다. 다만, 이런 방법은 테스트 환경에서만 사용해야한다.
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsManager userDetailsService() {
UserDetails userDetails =
User.withDefaultPasswordEncoder()
.username("example@example.com")
.password("1234")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
커스텀 로그인 페이지 지정하기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//파라미터가 HttpSecurity이고, 반환값이 SecurityFilterChain인 메서드를 정의하면
//HTTP 보안 설정을 구성할 수 있다.
http
.csrf().disable() //CSRF 공격에 대한 설정을 비활성화
.formLogin() //인증 방법을 폼 로그인 방식으로 지정
.loginPage("/auths/login-form") //Spring Security가 제공하는 디폴트가 아닌, 지정해둔 로그인 페이지를 사용하도록 설정
.loginProcessingUrl("/process_login") //로그인 인증 요청을 수행할 요청 URL 지정
.failureUrl("/auths/login-form?error") //로그인 인증에 실패할 경우 리다이렉트 지정
.and()
.authorizeHttpRequests() //클라이언트의 요청이 온다면 접근 권한을 확인하겠다고 정의
.anyRequest() // 모든 요청에 대해
.permitAll(); //접근을 허용한다
return http.build();
}
Role을 통해 URI접근 권한 부여하기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied") //권한이 없는 사용자가 접근을 시도한다면 403에러를 발생시킨다
.and()
.authorizeHttpRequests(authorize -> authorize //request URI에 접근 권한을 부여한다
.antMatchers("/orders/**").hasRole("ADMIN") //ADMIN만 /orders/와 그 하위에 접근할 수 있다
.antMatchers("/members/my-page").hasRole("USER") //USER만 my-page에 접근 가능하다
.antMatchers("/**").permitAll() //앞서 설정한 경로 외의 모든 URL은 모두 접근이 가능하다.
);
return http.build();
}
로그인, 로그아웃 표시를 위한 header.html 수정
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <!--사용자의 인증 정보나 권한 정보를 이용해 로직을 처리하기 위해 sec 태그를 사용하기 위한 XML 네임 스페이스-->
<body>
<div align="right" th:fragment="header">
<a href="/members/register" class="text-decoration-none">회원가입</a> |
<span sec:authorize="isAuthenticated()"> <!-- 현재 페이지에 접근한 사용자가 인증에 성공한 사용자인지 체크 -->
<span sec:authorize="hasRole('USER')"> <!-- Role이 USER인 사용자에게만 표하도록 설정 -->
<a href="/members/my-page" class="text-decoration-none">마이페이지</a> |
</span>
<a href="/logout" class="text-decoration-none">로그아웃</a> <!-- 인증에 성공한 사용자라면 로그인한 사용자이므로 로그아웃 메뉴 표시 -->
<span th:text="${#authentication.name}">홍길동</span>님 <!-- 로그인한 사용자의 name 표시 -->
</span>
<span sec:authorize="!isAuthenticated()"> <!-- 로그인한 사용자가 아니라면 로그인 메뉴 표시 -->
<a href="/auths/login-form" class="text-decoration-none">로그인</a>
</span>
</div>
</body>
</html>
sec 태그를 사용하려면 build.gradle에 의존성을 추가해줘야한다.
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
로그아웃을 위한 추가 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout() //로그아웃 추가 설정을 위한 메서드. LogoutConfigurer를 리턴한다
.logoutUrl("/logout") //로그아웃을 위한 request URL 지정
.logoutSuccessUrl("/") //로그아웃 후 리다이렉트할 URL 지정
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
회원가입을 통해 인메모리에 유저 등록
1. PasswordEncoder Bean 등록
2. MemberService Bean 등록을 위한 JavaConfiguration 구성
3. InMemoryMemberService 클래스 구현
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public interface MemberService {
Member createMember(Member member);
}
DB에 User를 등록하기 위한 DBMemberService 클래스를 일단 만들어준다.
@Transactional
public class DBMemberService implements MemberService {
public Member createMember(Member member) {
return null;
}
}
JavaConfiguration을 구성해준다.
JavaConfiguration에서는 MemberService의 구현 클래스인 InMemoryMemberService를 빈으로 등록한다.
이 때, User를 등록해야하니 UserDetailsManager가,
등록 전에 패스워드를 암호화해야하니 PasswordEncoder가 필요하므로 DI해준다.
@Configuration
public class JavaConfiguration {
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
public class InMemoryMemberService implements MemberService {
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
}
@Override
public Member createMember(Member member) {
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
//유저를 등록하기 위해서는 유저의 권한을 지정해줘야한다. 유저의 권한목록을 생성해주고 있다.
String encryptedPassword = passwordEncoder.encode(member.getPassword()); //패스워드 암호화
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
userDetailsManager.createUser(userDetails);
return member;
}
private List<GrantedAuthority> createAuthorities(String... roles) {
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
+Spring Security에서는 SimpleGrantedAuthority를 이용해 권한을 지정할 때, ROLE_{권한 이름} 으로 지정해줘야한다. 그렇지 않으면 권한 매핑이 이루어지지 않는다.
위 방법은 인메모리에 등록하는 방법.
DB에 등록하기 위해서 사용할 수 있는 방법 중 하나가 Custom UserDetailsService
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // 추가된 부분.
.and()
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
@Configuration
public class JavaConfiguration {
@Bean
public MemberService dbMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder);
}
}
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public DBMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
}
@Component
//UserDetailsService 를 구현한다
public class HelloUserDetailsServiceV1 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
//DB에서 조회한 User 객체를 리턴하면 Spring Security가 이 정보를 이용해서 인증 절차를 수행한다
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
@Component
public class HelloAuthorityUtils {
//application.yml에 추가한 프로퍼티를 가져오는 표현식
//@Value는 lombok의 value가 아니다... org.springframework.beans.factory.annotation.Value 이다.
@Value("${mail.address.admin}")
private String adminMailAddress;
//관리자의 권한 목록, 유저의 권한 목록을 생성한다.
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
//관리자라면, 관리자 권한을 부여하고 아니라면 유저 권한 부여
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
application.yml의 프로퍼티에서 관리자 이메일을 가져오므로 application.yml에 추가해준다.
mail:
address:
admin: admin@example.com