7 Spring Security - Password Encoding 비밀번호 암호화

|


[목차]

views


project 전체 코드 보기


[0] 비밀번호 암호화

비밀번호의 경우 반드시 단방향 암호화/해싱을 사용해야 한다.

한번 암호화된 비밀번호는 절대로 다시 복호화 할 수 없도록 만들어야 한다.

Spring Security 패키지에서는 Password를 암호화할 수 있는 PasswordEncoder Interface가 있다.

기본으로 제공되는 PasswordEncoder중에 복호화가 되지 않는 클래스는

BCryptPasswordEncoder, DelegatingPasswordEncoder, SCryptPasswordEncoder, Pbkdf2PasswordEncoder 가 있다.

이 중 BCryptPasswordEncoder를 사용해 암호화를 사용했다!

BCryptPasswordEncoder는 단순히 입력을 1회 해시하는 것이 아니라 ,랜덤의 소트(salt)을 부여하여 여러번 해시를 적용하여 원래의 암호를 추측하기 어럽게 한다.

BCryptPasswordEncoder는 입력값이 같아도 매번 다른 값을 return해주기 때문에 matches함수를 잘 확인하고 사용해야한다.


[1] config 버전에서 설정하기

  • WebSecurityConfigurerAdapter를 구현한 클래스의 configure메소드에 설정한다.

  • 암호화 해시는 PasswordEncoder를 DaoAuthenticationProvider로 설정하면 된다.

SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Autowired
	private UserDetailsService userDetailsService;

    ...
        
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 사용자 세부 서비스를 설정하기 위한 오버라이딩이다.
        auth
            .userDetailsService(userDetailsService) // 로그인 관리

            // 프로바이더 하나 만들기
            .and()
            .authenticationProvider( authenticationProvider() ); // Password Encoding 관리
    }

    // authenticationProvider()구현
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService); 
        // 주입된 UserDetailsService에 passwordEncoder를 설정한다.
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    // passwordEncoder() 구현
    // 암호를 해시시키는 경우 BCryptPasswordEncoder를 사용한다. 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 클래스를 살펴보면,

  • encode시 솔트를 사용하는 것을 볼 수 있다.
views
  • matches라는 메서드를 사용하여, 입력된 비밀번호와 DB에 저장된 값을 비교 할 수있다.
views


[2] service 에서 passwordEncoder 처리하기

회원가입 시 Dao를 가기위해 거쳐가는 Service의 joinUser메소드에서 passwordEncoder를 처리한다.

@Service
public class UserService {

    ...
        
    public Boolean joinUser(UserVo userVo) {
        userVo.setPassword(passwordEncoder.encode(userVo.getPassword()));
        return userDao.insert(userVo);
    }
    
    ...
        
}


[3] 회원가입 후 데이터 확인하기

views

암호화된 비밀번호가 들어간 것 확인!



6 Spring Security - Access Denied Handler(error page 처리)

|




project 전체 코드 보기


[1] java config 버전 Access Denied Handler 설정

- error page 처리

[1] xml 버전에서 설정하기

<!-- SS:begin -->	
<http 
      security-context-repository-ref="securityContextRepository" 
      auto-config="false" request-matcher="regex" 
      entry-point-ref="authenticationEntryPoint" disable-url-rewriting="true">
	...
    <access-denied-handler ref="accessDeniedHandler" />		
  	...
</http>

<!-- SS:end -->

<beans:bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
    <beans:property name="errorPage" value="/WEB-INF/views/error/403.jsp" />
</beans:bean>


[2] config 버전에서 설정하기

WebSecurityConfigurerAdapter을 구현한 클래스에 DeniedPage 설정을 해줘야한다.

SecurityConfig.java

@Override 
protected void configure(HttpSecurity http) throws Exception {
    
    ...
        
    http
    .exceptionHandling()
    .accessDeniedPage("/WEB-INF/views/error/403.jsp");
}

.accessDeniedPage("/WEB-INF/views/error/403.jsp"); 설정한 경로에 403.jsp 파일을 만들어준다.

views
<h1>OOOOOOOOOOooops~~~~~~~~~~~~!</h1>
<p>
    403 죄송합니다! <br> 접근할 수 없습니다~!ㅇㅅㅇ/
</p>

위의 설정을 하면,

일반회원이 관리자 경로 admin/**에 접근했을 때(즉, 403 Forbidden 페이지), 아래의 view가 나온다.

(관리자 권한페이지 설정하기)

views




5 Spring Security - 자동로그인(remember-me), crsf 처리

|


[ 목차 ]

views


project 전체 코드 보기


[1] 자동로그인 remember-me 설정

- 설정하기


config 버전에서 설정하기 : 내가 사용하는 버전

WebSecurityConfigurerAdapter를 구현한 클래스에 rememberME()를 추가한다.

SecurityConfig.java

@Override 
protected void configure(HttpSecurity http) throws Exception {
    ...
        //
        // 5. RememberMe
        //
        .and()
        .rememberMe()
        .key("mysite03") 
        .rememberMeParameter("remember-me");
}

- form에서 처리하기

로그인 jsp의 form안에 remember-me 설정

<label class="block-label">자동로그인</label>
<input name="remember-me" type="checkbox" value="">
views

- ajax에서 처리하기

var params = "email=" + $('#email').val() + "&password=" + $('#password').val()
+ "&remember-me=" +  $("#remember-me").prop("checked"); 

넘겨줄 data의 params에 remember-me를 넘겨주면 된다.

views



[2] csrf form 처리

이전에 csrf 토큰 처리를 막기위해 임시로 disable을 통해 막아놨었다.

WebSecurityConfigurerAdapter를 구현한 SecurityConfig.java

@Override 
protected void configure(HttpSecurity http) throws Exception {
    ...
    
    // Temporary for Testing
    http.csrf().disable();
    
    ...
}

이제 이 disable을 지우고 csrf 처리를 해야한다.

vies의 jsp파일에서 post방식으로 처리를 하는 form밑에 코드를 추가해주어야한다.


[1] 먼저 해당 jsp파일에 taglib을 추가한다.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>


[2] form안에 csrf 처리 토큰을 넣어준다.

<sec:csrfInput />

views



[3] csrf ajax(javascript) 처리

위의 form의 로그인을 ajax로 구현했다면 자바스크립트를 이용해 csrf토큰을 넘겨주어야한다.


[1] csrfMetaTags 태그를 추가한다.

<sec:csrfMetaTags />

이 태그를 추가하면 해당페이지에서 개발자 모드를 통해 부여된 csrf 토큰을 확인할 수 있다.

views


[2] csrf 토큰을 가져와야한다.

var csrfParameter = $('meta[name="_csrf_parameter"]').attr('content')
var csrfHeader = $('meta[name="_csrf_header"]').attr('content')
var csrfToken = $('meta[name="_csrf"]').attr('content')  


[3] ajaxSetup을 통해 csrf토큰을 처리한다.

$.ajaxSetup({ 
    beforeSend: function(xhr) { 
        xhr.setRequestHeader(csrfHeader, csrfToken); 
    }  
}) 
views



4 Spring Security - 인증 정보 유지(Security Context Holder)

|


[ 목차 ]

views


project 전체 코드 보기


[1] MVCConfig에 Argument Resolver 추가

WebMvcConfigurerAdapter를 상속받은 MVCConfig.java클래스에 Argument Resolver 코드를 추가해준다.

@Configuration
@EnableWebMvc
public class MVCConfig extends WebMvcConfigurerAdapter {
	
	...
	
	// Argument Resolver
	@Bean
	public AuthUserHandlerMethodArgumentResolver authUserHandlerMethodArgumentResolver() {
		return new AuthUserHandlerMethodArgumentResolver();
	}
	
	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
		argumentResolvers.add(authUserHandlerMethodArgumentResolver());
	}
}

위에서 return할 AuthUserHandlerMethodArgumentResolver클래스를 구현해야한다.


[2] Security Context Holder에 Details User 정보 등록

- AuthUserHandlerMethodArgumentResolver 구현

views

AuthUser.java : @interface로 어노테이션으로 쓰일 클래스

AuthUserHandlerMethodArgumentResolver : HandlerMethodArgumentResolver 구현 클래스

@AuthUser 어노테이션이 달린 메소드에 SecurtiyUser객체를 세션(Security Context Holder)에 등록할 것이다.

AuthUser.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthUser {
} 

AuthUserHandlerMethodArgumentResolver.java

package com.cafe24.mysite.security;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class AuthUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public Object resolveArgument(
        MethodParameter parameter, 
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, 
        WebDataBinderFactory binderFactory) throws Exception {

        Object principal = null;

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if(authentication != null ) {
            principal = authentication.getPrincipal();
        }

        if(principal == null || principal.getClass() == String .class) {
            return null;
        }

        return principal;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        AuthUser authUser = parameter.getParameterAnnotation(AuthUser.class);
        if(authUser==null) {
            return false;
        }

        if(parameter.getParameterType().equals(SecurityUser.class)==false) { 
            return false;
        }
        
        return true;
    }
}

코드 해석

[1] 메소드 파라미터를 검사해 @AuthUser가 붙은 경우, 또 그 어노테이션이 붙은 파라미터의 타입이 SecurityUser인 경우 를 검사해야한다.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    AuthUser authUser = parameter.getParameterAnnotation(AuthUser.class);

    // @AuthUser가 안붙어있으면
    if(authUser==null) {
        return false; // 난 그 파라미터 관심없어 return false!
    }
    
    // @AuthUser는 붙어있는데 SecurityUser가 아니면 return false!
    if(parameter.getParameterType().equals(SecurityUser.class)==false) { //클래스 객체 비교
        return false;
    }
    return true;
}


[2] 위의 supportsParameter메소드를 통과하면 resolveArgument 메소드에서 처리할 수 있다.

    @Override
    public Object resolveArgument(
        MethodParameter parameter, 
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest, 
        WebDataBinderFactory binderFactory) throws Exception {

        Object principal = null;
        
		// SecurityContextHolder에서 갖고있는 인증 정보를 확인한다.
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 인증 정보가 없는 경우 인증 정보 등록
        if(authentication != null ) {
            principal = authentication.getPrincipal();
        }
	
        // 인증 정보가 없다면 return null
        if(principal == null || principal.getClass() == String .class) {
            return null;
        }

        return principal;
    }


[3] 인증을 필요로하는 메소드 파라미터 수정

회원정보 수정을 해야하는 경우, 회원이 로그인상태(인증상태)이여야 하고, 해당 회원의 회원 정보만 수정할 수 있어야 한다. 이제 @AuthUser 어노테이션을 통해 인증정보를 확인 할 수 있다.

예시코드

@RequestMapping(value = "/update", method = RequestMethod.GET)
public String update(@AuthUser SecurityUser securityUser, Model model) {
    UserVo userVo = userService.getUser(securityUser.getNo());
    	...
}

@RequestMapping(value = "/update", method = RequestMethod.POST)
public String update(@AuthUser SecurityUser securityUser, @ModelAttribute UserVo userVo, Model model) {
	...
    securityUser.setName(userVo.getName());
    ...
}




3 Spring Security - Authorization(권한) 설정(ROLE), TagLib authorize 추가

|


[ 목차 ]

views


project 전체 코드 보기


Authorization(권한) 설정하기 - ROLE

지난 포스팅

[1] 테이블 수정

ROLE을 아래의 형태로 받을 예정이다.

  • 회원 권한 : ROLE_USER
  • 관리자 권한 : ROLE_ADMIN

위의 형태로 받기 위해서 table의 role컬럼(기존 enum(‘USER’, ‘ADMIN’) 형태)과 데이터를 수정했다.

alter table user 
change column role 
role enum('ROLE_USER','ROLE_ADMIN');

update user set role="ROLE_USER";

update user set role="ROLE_ADMIN" where name = "관리자";

수정 후 데이터

views


[2] SecurityUser에 field 추가하기

SecurityUser는 회원 정보를 저장하는 역할이다. (UserDetails를 구현한 클래스)

SecurityUser.java의 기존 field

private String name;  // biz data
private Collection<? extends GrantedAuthority> authorities;
private String username; 
private String password; 

no filed 추가 후 getter, setter 추가

// domain fields(principal: 보호할 사용자 중요 데이터)
private Long no;
private String name;  // biz data

// security fields
private Collection<? extends GrantedAuthority> authorities;
private String username;  // credential(email)
private String password;  // credential

...

public Long getNo() {
    return no;
}
public void setNo(Long no) {
    this.no = no;
}

...

인증을 위한 security fields와, 회원의 중요 데이터 domain fields 가 있다.

다른 회원 정보를 추가하기 위해서 domain fields 쪽에 필드를 추가해주면 된다.


[3] UserDetailsServiceImpl 수정하기

이전 코드

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserVo userVo = userDao.get(username); 

        SecurityUser securityUser = new SecurityUser();

        if ( userVo != null ) { 
            securityUser.setName(userVo.getName());         
            securityUser.setUsername(userVo.getEmail());     // credential
            securityUser.setPassword(userVo.getPassword());  // credetial

            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority(userVo.getRole()));
        }
        return securityUser;
    }
}

이전 코드에서는 securityUser의 no field가 없어 no를 넣어주지 못했다.

no추가와 권한추가 코드(authorities)를 수정했다.

수정 후 코드

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserVo userVo = userDao.get(username); 

        SecurityUser securityUser = new SecurityUser();

        if ( userVo != null ) { 
            securityUser.setNo(userVo.getNo());   			// setNo코드 추가
            securityUser.setName(userVo.getName());         
            securityUser.setUsername(userVo.getEmail());     // credential
            securityUser.setPassword(userVo.getPassword());  // credetial
            securityUser.setAuthorities(Arrays.asList(new SimpleGrantedAuthority(userVo.getRole()))); 				// 한줄로 요약
        }
        return securityUser;
    }
}


[4] UserDao수정하기

이전코드

UserDao.java 에서 임시 데이터를 return 했다.

public UserVo get(String email) {
    UserVo vo = new UserVo();
    vo.setName("이정은");
    vo.setNo(1L);
    vo.setEmail("aaa");
    vo.setPassword("1234");
    vo.setRole("ROLE_USER");
    return vo;
}

수정 후 코드 : mybatis를 통해 db에서 실제 데이터를 가져옴

username대신 email이 credential이다.

public UserVo get(String email) {	
    return sqlSession.selectOne("user.getByEmail", email);
}


여기 까지 수정했다면 이제 SercurityUser 객체(?)는 ROLE 권한(setAuthorities)을 갖게된다.

일반 회원일 경우 ROLE_USER, 관리자일 경우 ROLE_ADMIN이다.

해당 권한은 WebSecurityConfigurerAdapter를 구현한 SecurityConfig.java클래스에서 사용할 수 있다.

지난 포스팅에서

아래의 그림과 같이,

views

antMatchers를 통해 /admin/** 경로는 ROLE_ADMIN 권한을 갖게 했었다.

이제 DB를 통해서 정말로 사용자 데이터의 ROLE을 가져오기 때문에,

권한을 통해 오직 관리자(ROLE_ADMIN 권한을 가진 회원)만 /admin/** 경로에 접근 할 수 있게 되었다.

(이 설정을 하기 전에는 모~든 회원이 Forbidden 403페이지가 떴었다.)


[5] jsp에 SpringSecurity TagLib authorize 추가하기

태그 라이브러리 사용을 위해 pom.xml에 라이브러리가 추가 되어있어야한다.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>${org.springsecurity-version}</version>
</dependency>

로그인 회원, 비 로그인 회원 분리

<sec:authorize access="isAuthenticated()">를 통해 인증된(로그인한) 회원이라면 회원정보 수정과 로그아웃을 보여준다. 비 로그인 회원일 경우 로그인과 회원가입 버튼을 보여준다.

taglib추가 후 코드 수정하기

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<sec:authorize access="isAuthenticated()">
    <li><a href="${pageContext.servletContext.contextPath}/user/update">회원정보수정</a></li>
    <li><a href="${pageContext.servletContext.contextPath}/user/logout">로그아웃</a></li>
    <li><sec:authentication property="name" />님 안녕하세요 ^^</li>			
</sec:authorize>
<sec:authorize access="!isAuthenticated()">
    <li><a href="${pageContext.servletContext.contextPath}/user/login">로그인</a><li>
    <li><a href="${pageContext.servletContext.contextPath}/user/join">회원가입</a></li>
</sec:authorize>

해당 회원의 정보(name)을 가져오기 위해서는 <sec:authentication property="name" />를 사용하면 된다.

views

하지만, 우리는 credential의 username 인증을 이메일로 했기 때문에 이메일이 뜨게된다.

UserDetails를 구현한 SecurityUser의 fields 정보

views

여기서 회원의 정보 principal의 name을 가져오기 위해서는 아래의 코드를 적으면 된다.

<sec:authentication property="principal.name" />

views


[5] jsp에 권한 제한 하기

- 사용

페이지에서 F12(개발자모드) 혹은 소스코드보기를 통해 코드를 감추고 싶을 때 taglib를 이용해 권한을 제한할 수 있다.

[1] 일단 태그라이브러리를 추가한다.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>


[2] 가리고 싶은 코드를 <sec:authorize access="hasRole('ROLE_ADMIN')" >로 감싸준다.

views

소스코드 보기로 확인

views


[3] html 태그에도 적용이 가능하다.

  • 관리자가 아닐 경우 업로드 방지를 위해 업로드 버튼을 숨긴다.
views
  • 해당 버튼의 url경로를 막는다.

    SecurityConfig.java에서 설정

    @Override 
    protected void configure(HttpSecurity http) throws Exception {
        
        ...
            
        .antMatchers("/gallery/upload", "/gallery/delete/**").hasAuthority("ROLE_ADMIN")
    }
    


권한에 따라 다르게 보이는 화면

views