JPA Entity Class
# User.java
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
int id;
@Column(name = "userId")
String loginId;
String password;
String name;
String email;
boolean enabled;
String userType;
@ManyToOne
@JoinColumn(name = "departmentId")
Department department;
}
Repository Class
# UserRepository.java
import net.skhu.security_mvc.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Integer> {
User findOneByLoginId(String loginId);
}
DB에 loginId로 조회한 레코드가 2개 이상이면,
findOneByLoginId(loginId) 메소드에서 에러가 발생한다.
loginId 값이 중복되는 레코드가 user 테이블에 저장되지 않도록 강제하려면,
loginId 필드에 unique index를 생성하면 된다.
하지만, 아래 코드처럼 DB에 삽입하기 전에 userid 값이 중복되는지 먼저 검사해서
입력 폼에 에러를 표시해 주는 것이 바람직하다.
User user = userRepository.findByUserid(userModel.getUserid());if(user !=null) {
bindingResult.rejectValue("userid", null, "사용자 아이디가 중복됩니다.");
return true;
}
Service Class
# UserService.java
import net.skhu.security_mvc.domain.User;
import net.skhu.security_mvc.repository.UserRepository;
import net.skhu.security_mvc.utils.EncryptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public User login(String loginId, String password){
User user = userRepository.findOneByLoginId(loginId);
if(user==null) return null;
// DB에서 pw가 MD5 암호화로 되어잇다.
String pw = EncryptionUtils.encryptMD5(password);
if(user.getPassword().equals(pw)==false) return null;
return user;
}
}
# MyAuthenticationProvider.java
import net.skhu.security_mvc.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
// MyAuthenticationProvider 클래스는 사용자가 입력한 아이디, 비밀번호를 검사할 때 사용되는 클래스이다.
public class MyAuthenticationProvider implements AuthenticationProvider {
@Autowired UserService userService;
// AuthenticationProvider 인터페이스 구현하니까 authentication 메서드를 Override.
// Attempts to authenticate the passed Authentication object,// returning a fully populated Authentication object (including granted authorities) if successful.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException{
String loginId = authentication.getName();
String password = authentication.getCredentials().toString();
// 간단하게는 이 메소드에서 바로 비교하여 올바른 객체를 리턴하겠지만 커스터마이징.
return authentication(loginId,password);
}
// 커스터마이징한 비교 메소드.
// 사용자가 입력한 로그인 아이디와 비밀번호를 검사해야 할 때,
// spring security 엔진에 의해서 이 클래스의 authenticate 메소드가 자동으로 호출된다.
// 사용자가 입력한 로그인 아이디와 비밀번호가 이 메소드의 파라미터로 전달된다
public Authentication authentication(String loginId, String password) throws AuthenticationException{
User user = userService.login(loginId,password);
if(user==null) return null;
List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
String role="";
switch (user.getUserType()){
case "관리자" : role= "ROLE_ADMIN"; break;
case "교수" : role = "ROLE_PROFESSOR"; break;
case "학생" : role = "ROLE_STUDENT"; break;
}
grantedAuthorities.add(new SimpleGrantedAuthority(role));
// 여기서 UsernamePasswordAuthenticationToken을 생성해서 바로 리턴해도 괜찮다.
return new MyAuthentication(loginId, password, grantedAuthorities, user);
}
// AuthenticationProvider 인터페이스 구현하니까 supports 메서드를 Override한다.
// Returns true if this AuthenticationProvider supports the indicated Authentication object.
@Override
public boolean supports(Class<?> authentication){
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
public class MyAuthentication extends UsernamePasswordAuthenticationToken{
private static final long serialVersionUID = 1L;
User user;
public MyAuthentication(String loginId, String password,
List<GrantedAuthority> grantedAuthorities, User user){
super(loginId, password, grantedAuthorities);
this.user = user;
}
public User getUser(){
return user;
}
public void setUser(User user){
this.user = user;
}
}}
GrantedAuthority => 현재 사용자(principal)가 가지고 있는 권한.
UserDetailsService에 의해 보통 로드된다.
사용자가 가지고 있는 권한인 Autorities들을 가져와서 특정 자원에 권한이 있는지를
체크하여 접근을 허용할지 말지를 체크하는데 쓰인다.
private static final long serialVersionUID = 1L;
=> 직렬화란?
JVM 힙 영역에 존재하는 객체를 한 줄로 늘어선 바이트의 형태로 만드는 것을 객체의 직렬화,
객체의 형태로 복원하는 작업을 역직렬화라고 한다.
직렬화 복원은 serialVersionUID = 1L 이 숫자를 사용하여 로드 된 클래스가
직렬화 된 객체와 정확히 일치하는지 확인한다.
일치하는 항목이 없으면 an InvalidClassException이 throw된다.
Java Config Class
# SecurityConfig.java
import net.skhu.security_mvc.service.MyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
// Spring Security 설정을 위한 Java Config 클래스.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenticationProvider myAuthenticationProvider;
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/res/**");
}
// 권한 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
// 권한 설정 시작
http.authorizeRequests()
.antMatchers("/admin/**").access("ROLE_ADMIN")
.antMatchers("/professor/**").access("ROLE_PROFESSOR")
.antMatchers("/guest/**").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/**").authenticated();
// CSRF 공격 검사를 하지 않겠다는 설정이다.
http.csrf().disable();
// 로그인 페이지 설정 시작
http.formLogin()
.loginPage("/guest/login")
.loginProcessingUrl("/guest/login_processing")
.failureUrl("/guest/login?error")
.defaultSuccessUrl("/user/index", true)
.usernameParameter("loginId")
.passwordParameter("passwd");
// 로그아웃 설정 시작
http.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout_processing"))
.logoutSuccessUrl("/guest/login")
.invalidateHttpSession(true);
http.authenticationProvider(myAuthenticationProvider);
}
}
공식 가이드 : WebSecurityConfigurerAdapter
리소스 파일 무시
@Override
public void configure(WebSecurity web) throws Exception
{
web.ignoring().antMatchers("/res/**");
}
=> /res/** 패턴의 URL은 보안 검사를 하지 말고 무시하라는 설정이다.
여기에는 *.css *.js *.png 등등 리소스 파일이 들어있다.
웹브라우저가 이 파일들을 요청할 때는 보안 검사를 할 필요가 없다.
권한 설정
http.authorizeRequests() : 권한 설정 시작
.antMatchers () : 주어진 패턴( 파라미터 )의 URL은
.access () : "ROLE_ADMIN" 권한을 소요한 사용자만 요청할 수 있다는 설정.
로그인된 현재 사용자가 ROLE_ADMIN 권한을 소유하고 있지 않다면
/admin/** 패턴의 URL 요청은 spring security 엔진에 의해서 거부된다.
----------------------------------------------------------------------
.antMatchers() : /guest/** 패턴의 URL은
.permitAll() : 모든 사용자에게 허용된다는 설정이다.
로그인하지 않은 사용자에게도 허용된다.
----------------------------------------------------------------------
.antMatchers() : /** 패턴의 URL은
.authenticated() : 로그인된 사용자에게만 허용된다는 설정이다.
CSRF 공격
CSRF 공격(Cross Site Request Forgery) : 웹 어플리케이션 취약점 중 하나로, 인터넷 사용자(희생자)가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 만드는 공격.
CSRF 공격이 이뤄지려면 다음 조건이 만족되어야 한다.
- 1. 위조 요청을 전송하는 서비스에 희생자가 로그인 상태
- 2. 희생자가 해커가 만든 피싱 사이트에 접속
로그인 페이지 설정
http.formLogin() : 로그인 페이지 설정 시작
.loginPage("/guest/login") : 로그인 페이지 URL 설정. 이 URL의 액션 메소드와 뷰 파일을 구현해야 한다.
.loginProcessingUrl("/guest/login_processing")
:
로그인 페이지에서 '로그인' 버튼(submit button)을 눌렀을 때 요청할 URL 설정.
이 URL이 요청되면 (즉 '로그인' 버튼이 눌려지면),
spring security 엔진이 MyAuthenficationProvider의 authenticate 메소드를 호출하여
로그인 검사를 수행한다.
즉 이 URL이 요청되면, 로그인 절차는 spring security 엔진에 의해서 자동으로 진행된다.
.failureUrl("/guest/login?error") : 로그인 아이디, 비밀번호가 일치하지 않았을 때 넘어갈(redirect) URL 설정
.defaultSuccessUrl("/user/index", true) : 로그인이 성공했을 때. 넘어갈(redirect) URL 설정
.usernameParameter("loginId") : 로그인 페이지 (뷰 파일)에서, 로그인 아이디 input 태그의 name 값 설정
.passwordParameter("passwd") : 로그인 페이지 (뷰 파일)에서, 비밀번호 input 태그의 name 값 설정
로그아웃 설정
http.logout() : 로그아웃 설정 시작
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout_processing"))
:
로그아웃 버튼이나 링크를 눌렀을 때 요청할 URL 설정.
이 URL이 요청되면, 로그아웃 절차는 spring security 엔진에 의해서 자동으로 진행된다
.logoutSuccessUrl("/guest/login") : 로그아웃된 후 넘어갈(redirect) URL 설정
.invalidateHttpSession(true) : 로그아웃할 때, 세션(session)에 들어있는 데이터를 전부 지우라는 설정
View
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
=> Spring Security 확장 태그를 사용하기 위한 선언이다.
<sec:authorize access="not authenticated">
<a class="btn btn-default" href="${R}guest/login">로그인</a>
</sec:authorize>
=> 현재 사용자가 로그인하지 않았을 경우에 위 태그가 출력된다.
<sec:authorize access="authenticated">
<a class="btn btn-default" href="${R}user/logout_processing">로그아웃</a>
</sec:authorize>
=> 현재 사용자가 이미 로그인했을 경우에는 위 태그가 출력된다.
<a class="btn btn-default" href="${R}guest/login">로그인</a>
=> "guest/login" 부분은, 로그인 페이지 URL 이어야 한다
<a class="btn btn-default" href="${R}user/logout_processing">로그아웃</a>
=> "user/logout_processing" 부분은, SecurityConfig.java 에서 설정한, 로그아웃 처리 URL과 일치해야 한다.
<sec:authentication property="user.loginId" />
=> 현재 로그인된 사용자 객체의 loginId 속성값을 출력한다.
즉, User 객체의 getLoginId() 메소드 리턴값이 출력된다.
<form method="post" action="login_processing">
=> SecurityConfig.java 에서 설정한 내용과 일치해야 한다
'Spring' 카테고리의 다른 글
@Setter (0) | 2019.04.18 |
---|---|
토비의 스프링3.1 - 템플릿 메소드 패턴, 팩토리 메소드 패턴 (0) | 2019.03.03 |
Spring Security (0) | 2019.02.03 |
spring core(핵심) 기능 (0) | 2019.01.31 |
ModelAttribute (0) | 2019.01.30 |