| 스프링 시큐러티란?
- 스프링 시큐러티란, 스프링 기반의 애플리케이션의 보안을 담당하는 하위 프레임워크로,
Filter를 통해 인증과 권한을 처리한다.
- HTTP URI를 통해 접근 가능한 경로를 제한할 수 있으며, 제한된 경로로 접속 시, user/password로 로그인을 함으로써 인증(Authentication) 후, 인가(Ahthorization)을 한다.
- 보다 자세한 사항에 대해서는 아래의 블로그가 잘 설명하고 있는 것 같다.
https://catsbi.oopy.io/c0a4f395-24b2-44e5-8eeb-275d19e2a536
스프링 시큐리티 기본 API및 Filter 이해
목차
catsbi.oopy.io
| 스프링 시큐러티 사용해보기
1. dependency에 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 의존성을 추가한 후에 application을 실행하면, default password가 콘솔에 uuid 코드로 나타난다.
* default username은 참고로 user이며, password는 restart할때마다 랜덤으로 생성되서 나타난다.
Using generated security password: d3ca14ad-22bd-4243-8432-c0874f82a693
- 이 상태에서 application 홈으로 가면 아래와 같이 나타난다. 여기에 user / password를 작성해서 들어가면 된다.
- 그런데 위와 같이 하게 되면 콘솔에 매번 새롭게 password가 뜨기 때문에 불필요할 뿐더러 보안상 이슈가 발생한다.
- 그렇기에 하나의 방법으로 application.yml에 username과 password를 저장해줄 수도 있다.
그러나 이 방법 또한 기본적인 보안 외에는 세부적인 보안이 포함되지 않으므로, 사용자 정의 보안 기능을 구현하는 것이 좋다고 한다.
spring.security.user.name=user
spring.security.user.password=1234
2. 사용자 정의 보안 기능 구현
- 수업을 따라서 WebSecurityConfugurerAdapter를 사용하려하니, deprecated되었다고 떴다.
- 블로그와 spring 공식문서에 있는 내용에 따라가보고, 다른 벨둥 내용도 확인해보았는데, 사용법은 비슷하지만
아래와 같이 약간의 차이가 있는 것 같았다.
- 수업 내용이 많다보니 시간상 구버전으로 먼저 배우는 걸로 하고, 신규 버전은 차후에 다시 시도해보기로 했다.
5.7.0-M2 이전 | 5.7.0-M2 이후 | |
클래스 | @Configuration, @EnableSecurity 추가 | |
WebSecurityDonfigurerAdapter을 상속 | (상속 없음) | |
httpSecurity 설정 | configure(http) : void를 @Override | filterChain(http) : SecurityFilterChain 를 통해 반환 인스턴스를 @Bean으로 등록 |
LDAP authentication | configure(AuthenticationManagerBuilder) : void 를 @Override |
@Bean으로 아래 두가지 등록 EmbeddedLdapServerContextSourceFactoryBean AuthenticationManager |
설정 클래스에 @Configuration/@EnableWebSecurity 추가
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
[ 공식 문서의 내용 ]
(1) HttpSecurity Configuring
// deprecated 버전
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
}
}
// 최신 버전
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
}
(2) LDAP authentication
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDetailsContextMapper(new PersonContextMapper())
.userDnPatterns("uid={0},ou=people")
.contextSource()
.port(0);
}
}
@Configuration
public class SecurityConfiguration {
@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean =
EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
contextSourceFactoryBean.setPort(0);
return contextSourceFactoryBean;
}
@Bean
AuthenticationManager ldapAuthenticationManager(
BaseLdapPathContextSource contextSource) {
LdapBindAuthenticationManagerFactory factory =
new LdapBindAuthenticationManagerFactory(contextSource);
factory.setUserDnPatterns("uid={0},ou=people");
factory.setUserDetailsContextMapper(new PersonContextMapper());
return factory.createAuthenticationManager();
}
}
(1) HttpSecurity Configuring
- 말그대로 HTTP 보안 설정을 하는 것을 말한다.
- Client가 URI를 통해 HTTP 요청을 보낼 때, 권한 없는 이를 거르고, CSRF와 같은 해킹을 방지하는 조치를 취한다.
- 처음 개발을 할 때는 csrf().disabled()를 한 상태에서 개발 후, WebSecurity를 추가할 때 삭제하는게 좋다.
해보니 이걸 안 한 상태로 개발을 먼저 시도하면 계속 401 에러 (authentication exception)가 나타났다.
HttpSecurity API
* 출처의 내용에 추가 : https://yeonyeon.tistory.com/185
method | description |
csrf().disabled() | CSRF(cross-site request forgery) 방지 해제 * @EnableWebSecurity에 CSRF 방지 기능이 지원된다. * CRSF : 사이트간 요청 위조를 말하는 것으로, 사용자의 의지와 무관하게 의도된 행위를 요청한다. (옥션의 개인정보 유출 사건, 2008) * CSRF는 사이트간 요청이 잦은 경우 필요하며, 불필요한 경우 CSRF를 해제한다. |
authorizeRequests() | HttpServletRequest 요청 URL에 따라 접근 권한 설정 |
antMatchers("url") | 요청 URL 경로 패턴 지정 |
authenticated() | 인증 유저만 접근 허용 |
permitAll() | 모든 유저에게 접근 허용 |
anonymous() | 인증 안한 유저만 접근 허용 |
denyAll() | 모든 유저의 접근 불가 |
and() | HttpSecurity로 반환타입 변경 |
formLogin() | form login 설정 |
loginPage("url") | 커스텀 로그인 페이지 경로와 로그인 인증 경로 등록 |
failureHandler(handler) | 로그인 실패 시 처리해줄 핸들러 |
loginProcessingUrl("url") | 사용자 이름과 암호를 제출할 URL |
defaultSuccessUrl("url") | 로그인 성공 시 이동 페이지 (디폴트 페이지) |
logout() | 로그아웃 |
logoutUrl("url") | 사용자 정의 로그 아웃 |
logoutRequestMatcher( new AntPathRequestMatcher("url")) |
로그아웃 요청 처리 경로 |
logoutSuccessUrl("url") | 로그아웃 성공 시 이동 경로 |
invalidHttpSession(boolean) | 로그아웃 후 세션 전체 삭제 여부 |
[1] URL 접속 권한 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Http Security
* 모두 접근 가능 : 홈, 회원 가입, 메일 인증
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/",
"/member/register",
"/member/email-auth"
)
.permitAll();
super.configure(http);
}
}
[2] 사용자 지정 로그인 & 로그인 실패 핸들러 추가
[2-1] SimpleUrlAuthenticationFailureHandler를 상속한 FailureHandler 생성
- onAuthenticationFailure() 메소드를 Override하여 인증 실패 시 처리할 내용을 작성한다.
public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
String msg = "로그인에 실패하였습니다.";
// 이메일 비활성화시
if (exception instanceof InternalAuthenticationServiceException) {
msg = exception.getMessage();
}
setUseForward(true);
setDefaultFailureUrl("/member/login?error=true");
request.setAttribute("errorMessage", msg);
System.out.println("로그인에 실패하였습니다.");
super.onAuthenticationFailure(request, response, exception);
}
}
[2-2] 설정 클래스에 팩토리 메소드로 FailureHandler @Bean 등록
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
UserAuthenticationFailureHandler getFailureHandler() {
return new UserAuthenticationFailureHandler();
}
/**
* Http Security
* 모두 접근 가능 : 홈, 회원 가입, 메일 인증
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/",
"/member/register",
"/member/email-auth"
)
.permitAll();
http.formLogin()
.loginPage("/member/login")
.failureHandler(getFailureHandler())
.permitAll();
super.configure(http);
}
}
[2-3] 컨트롤러를 통해 로그인 프론트 페이지 매핑 & 로그인 프론트 페이지 작성
(이 부분은 생략)
(2) LDAP authentication
LDAP란? Lightweight Directory Access Protocol로, 경량화된 DAP를 말한다.
- LDAP는 핵심적인 사용자 정보를 저장한다. (core user identities)
- LDAP는 CRUD 중에서도 검색(조회)에 최적화되어 있으며, 바이너리 프로토콜이면서, 비동기 프로토콜이다.
rf. 디렉토리 서비스란, 이름을 기준으로 대상을 찾아 조회하거나 편집할 수 있는 서비스를 말하며,
그 예로는 DNS 프로토콜, DAP/LDAP 가 있다.
LDAP authentication이 작동하는 방식
(1) client에서 LDAP database에 credentials와 함께 정보를 request
(2) 만약 client가 보낸 crendentials가 database의 core user identities와 일치하면 LDAP access 허가
- LDAP authentication은 LDAP database에 대한 접근 권한이 있는 자만 사용자정보를 조회하도록 하는 인증 단계이다.
[1] 회원 서비스에서 UserDetailService 상속하기
- 회원 서비스에서 UserDetailService를 상속한뒤 loadUserByUsername 메소드를 override한다.
- loadUserByUsername() 은 LDAP 데이터베이스에 로그인 정보를 로드하겠다는 의미이다.
- 차후 로그인 정보를 Principal의 getName() 통해 가져올 때 여기서 저장한 User 정보를 가져온다.
// 1. UserDetailDervice를 상속
public interface MemberService extends UserDetailsService {
}
@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {
// 2. loadUserByUsername(String username) : userDetails
// -- username은 user id(pk)를 말한다.
// -- User객체 안에 (id, password, Role)을 넣어 반환한다.
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findById(username);
if (!optionalMember.isPresent()) {
throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
}
Member member = optionalMember.get();
if (!member.isEmailAuthYn()) {
throw new MemberNotEmailAuthException("이메일 활성화 이후 로그인 해주세요.");
}
// Role
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(member.getUserId(), member.getPassword(), grantedAuthorities);
}
}
- 이메일 활성화에 관해서는 사용자 지정 예외를 만들었었다.
package com.example.fastlms.member.exception;
public class MemberNotEmailAuthException extends RuntimeException {
public MemberNotEmailAuthException(String error) {
super(error);
}
}
[2] BCrypt를 통해 비밀번호 암호화하기
- LDAP authentication 과정에서 BCripty를 사용하면 비밀번호를 암호화할 수 있다.
- 이 과정을 하는 이유는, 본래 HTTP가 Text 기반으로 데이터를 주고 받기 때문에 해킹에 취약하기 때문이다.
(우리는 HashMap을 배울 때, 해싱 기법을 배웠었다. 여기서도 해싱을 사용해서 암호화를 한다.)
- 만약, BCrypt로 데이터를 인코딩 해서 변환하였다면, 그걸 조회할 때도 마찬가지로 동일한 변환과정을 통해 조회한다.
- 따라서, BCrypt를 사용할 때는, Spring Security 설정 클래스와 더불어, 회원 서비스 객체의 register() 메소드 안에도 동일하게 비밀번호를 BCrypt로 인코딩해주는 것이 필요하다.
* 등록 시에 암호화를 걸지 않으면, BCrypt로 비밀번호를 가져올 때에 동일한 비밀번호로 인식하지 못한다.
[2-1] 회원 가입 메소드에서 비밀번호를 BCriyt로 암호화한다.
String encPassword = BCrypt.hashpw(request.getPassword(), BCrypt.gensalt());
@Override
public boolean register(MemberRegister.Request request) {
Optional<Member> optionalMember = memberRepository.findById(request.getUserId());
if (optionalMember.isPresent()) {
return false;
}
// 추가한 encPassword
String encPassword = BCrypt.hashpw(request.getPassword(), BCrypt.gensalt());
String uuid = UUID.randomUUID().toString().replace("-","");
memberRepository.save(
Member.builder()
.userId(request.getUserId())
.password(request.getPassword())
.userName(request.getUserName())
.phoneNumber(request.getPhoneNumber())
.emailAuthYn(false)
.emailAuthKey(uuid)
.build()
);
// 인증메일
String email = request.getUserId();
String subject = "fastlms 사이트 가입을 축하드립니다.";
String text = "<div><p>fastlms 사이트 가입을 축하드립니다.</p>" +
"<p>아래 링크를 클릭하시어, 가입을 완료하세요.</p>" +
"<a href = 'http://localhost:8080/member/email-auth?id="
+ uuid +"'>링크</a></div>";
mailComponents.sendMail(email, subject, text);
return true;
}
[2-2] Spring Security 설정 클래스에서 회원서비스를 주입하고, 팩토리 메소드로 BCripyPasswordEncoder를 @Bean으로 등록한다.
- 등록 후에는 configure 메소드를 override 하면 된다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final MemberService memberService;
@Bean
UserAuthenticationFailureHandler getFailureHandler() {
return new UserAuthenticationFailureHandler();
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* LDAP authentication
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService)
.passwordEncoder(getPasswordEncoder());
super.configure(auth);
}
}
| 정리
- 스프링 시큐러티란, 애플리케이션 보안을 담당하는 하위 프레임워크로, Filter를 통해 인증 및 권한을 처리하는 프레임워크다. - 스프링 시큐러티는 HttpSecurity, WebSecurity, LDAP authentication, JDBC Authentication.. 등등 인증에 관한 다양한 하위 기능들을 가지고 있다. - 사용자 정의 보안 기능을 구현하기 위해서는, 기본적으로 (1) HttpSecurity를 통해 사용자 요청 URL에 대해 로그인을 전제할 것인지를 설정하고, (2) LDAP authentication을 통해 로그인 시 username과 password가 valid한 것인지를 확인하는게 필요하다. - 절차 (1) Spring Security 설정 클래스에 @Configuration과 @EnableWebSecurity를 달아준다. (2) configure(HttpSecurity http) 메소드에서 URL 접속권한/사용자 지정 로그인 및 로그인 실패 핸들러를 추가한다. (3) configure(AuthenticationManagerBuilder auth) 메소드를 통해 user 정보를 가져올 서비스 객체를 호출하고, BCrypt 인코더를 통해서 비밀번호 조회 시 암호화된 상태의 비밀번호를 조회할 수 있도록 한다. |
[ 출처 및 참조 ]
부트캠프 수업 내용을 들은 후 정리한 내용
spring security 사용기 https://yeonyeon.tistory.com/185
change default username and password https://www.yawintutor.com/spring-boot-security-step-by-step-2/
LDAP란? https://yongho1037.tistory.com/796
LDAP와 LADP authentication https://jumpcloud.com/blog/what-is-ldap-authentication
'Framework > 프로젝트로 스프링 이해하기' 카테고리의 다른 글
[LMS 만들기] 관리자 로그인 구현 (2) | 2022.10.04 |
---|---|
[LMS 만들기] 비밀번호 초기화 요청 및 메일 링크를 통한 초기화 (1) | 2022.10.04 |
[LMS 만들기] 회원가입 페이지 만들기 (0) | 2022.10.02 |
[LMS 만들기] 스프링 컨트롤과 주소 매핑 (0) | 2022.09.30 |
[LSM 만들기] Maven 프로젝트 환경 보기 (0) | 2022.09.29 |