Spring Security를 기존 프로젝트에 적용하기

Reading time ~4 minutes

기존 프로젝트에 Spring Security를 적용 해보자.

Spring Security 적용하기

Maven

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Gradle

compile("org.springframework.boot:spring-boot-starter-security")
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}

WebSecurityConfig를 만들면 모든 접근에 대해 인가가 필요하게 됩니다.

인증(Authentication) : 해당 사용자가 본인이 맞는지 확인하는 절차.
인가(Authorization) : 인증된 사용자가 요청된 자원에 접근 가능한지를 결정하는 절차.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  	protected void configure(HttpSecurity http) throws Exception {
  		http.csrf().disable();
  		http.headers().frameOptions().disable();
  		http.httpBasic();

  		http
  			.authorizeRequests()
  			.antMatchers("/", "/create", "/login**", "/logout**")
  			.permitAll()
  			.antMatchers("/boards/**", "/myboards", "/api/boards**", "/api/decks/**", "/api/boards/**", "/api/users**")
  			.hasRole("USER");

  }
}

자신이 설정 할 정책들을 적용 합니다.

위와 같이 설정 해두면 .hasRole("USER")에 해당 하는 곳들은 권한이 없는 사용자는 접근이 불가능 하게 됩니다.

(조금 있다 .hasRole("USER")에 해당 하는 롤을 만들 겁니다)

만약 인가때문에 문제가 된다면 일단은 .permitAll()으로 모두를 설정 해두고 작업을 하면 됩니다.


기존 유저 모델

@Getter
@Setter
@Entity
@EqualsAndHashCode(of = "uid")
@ToString
public class Member {

	@Id
	@GeneratedValue
	@Column(name = "MEMBER_ID")
	private long id;

	@Size(min = 3, max = 20)
	@Column(nullable = false, length = 20)
	private String name;

	@Column(nullable = false)
	@JsonIgnore
	private String password;

	@Email
	@Size(min = 6, max = 50)
	@Column(unique = true, nullable = false, length = 50)
	private String email;
}

일단 Role을 만들어야 합니다.

@Getter
@Setter
@Entity
@Table(name = "member_roles")
@ToString
public class MemberRole {
	public static final String USER = "USER";

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	private String roleName;

	public MemberRole() {
	}

	public MemberRole(String roleName) {
		this.roleName = roleName;
	}

그리고 위의 Member에 적용 되어야 할 롤들을 추가합니다.

@Getter
@Setter
@Entity
@EqualsAndHashCode(of = "uid")
@ToString
public class Member {

	@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
	@JoinColumn(name="member")
	private List<MemberRole> roles = new ArrayList<>();

}

cascade=CascadeType.ALL은 엔티티들의 영속관계를 한 번에 처리하지 못하기 때문에 설정 하는 것이고, fetch=FetchType.EAGERmembermember_roles 테이블을 둘 다 조회해야 하기 때문에 즉시 로딩을 이용해서 조인을 하는 방식으로 처리 합니다.


자 이제 재료는 다 준비 되었으니 회원 가입 부분을 수정 해 봅시다.

@Controller
@RequestMapping("/member")
public class MemberController {

	@Resource(name = " memberRepository")
	private MemberRepository memberRepository;

	@PostMapping("")
	public String create(Member member) {
		MemberRole role = new MemberRole();
		BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
		member.setPassword(passwordEncoder.encode(member.getPassword()));
		role.setRoleName("USER");
		member.setRoles(Arrays.asList(role));
		memberRepository.save(member);
		return "redirect:/";
	}

Spring Security에서 지원해 주는 BCryptPasswordEncoder를 이용하면 손쉽게 비밀번호를 암호화 할 수 있습니다.

MemberRole을 정의해서 Member에 넣어주고 save를 합니다.

<div class="signup-box z-depth-2">
     <h6 id="signUpFail" ></h6>
         <h4>Create a Account</h4>

         <form class="signup-form" action="/members" method="POST">
           <div class="row">
               <div class="input-field col s12">
                 <input id="name" name="name" type="text" class="validate">
                 <label for="name">Username</label>
               </div>
           </div>

           <div class="row">
               <div class="input-field col s12">
                   <input id="email" name="email" type="email" class="validate">
                   <label for="email">Email</label>
                 </div>
               </div>

          <div class="row">
               <div class="input-field col s12">
                 <input id="password" name="password" type="password" class="validate">
                 <label for="password">Password</label>
               </div>
          </div>
             <input class="signup-btn waves-effect waves-light btn" type="submit" value="가입하기" />
         </form>

     </div>

위와 같이 하면 이제 Role을 가지고 있는 멤버가 생성 됩니다.


로그인 구현 하기

회원 가입은 잘 되는데 로그인이 안돼서 아직 제대로 된 확인이 안됩니다. 그럼 본격 적으로 로그인 기능을 구현 해 봅시다.

WebSecurityConfig에서 AuthenticationManagerBuilder를 주입 해서 인증 처리를 해야 합니다.

@Resource(name="securityMemberService")
private SecurityMemberService securityMemberService;


public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
  auth.userDetailsService(securityMemberService).passwordEncoder(passwordEncoder());
}

위와 같이 설정 하면 UserDetailsService를 구현한 securityMemberServiceHttpSecurity 객체가 사용하게 되면서 우리가 만든 인증 로직을 바탕으로 동작 하게 됩니다.

그럼 securityMemberService를 제작해 봅시다.

securityMemberService

(주의 : 저같은 경우는 email로 로그인을 합니다.)

@Service("securityMemberService")
public class SecurityMemberService implements UserDetailsService{

	@Resource(name="memberRepository")
	private MemberRepository memberRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Member member = memberRepository.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("아이디 혹은 비밀번호를 잘 못 입력 하셨습니다."));
		return new SecurityMember(member);
	}
}

저같은 경우는 email을 통해 로그인을 하기때문에 받아온 username에 email이 담겨 있습니다. 그리고 그걸 이용하여 db에서 email을 통해 member를 가져와서 로그인을 절차를 거치게 됩니다.

위와 같은 방법으로 로그인시 Id 혹은 email등 자신이 원하는 형태를 설정 할 수 있습니다.

UserDetailsService 인터페이스를 보면

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetails를 반환하는 하나의 메소드만을 가지고 있습니다. 그렇기때문에 기존에 구현한 Member와는 타입이 맞지 않습니다. 이를 해결 하기 위한 방법 중 Spring Security의 User를 상속하는 새로운 멤버 클래스를 추가 합니다. (이때문에 Member가 아닌 User라는 이름으로 클래스를 만들면 문제가 생길수 있습니다)

SecurityMember

@Getter
@Setter
public class SecurityMember extends User{
	private static final long serialVersionUID = 1L;
	private static final String ROLE_PREFIX = "ROLE_";

	public SecurityMember(Member member) {
		super(member.getEmail(), member.getPassword(), makeGrantedAuthority(member.getRoles()));
	}

	private static List<GrantedAuthority> makeGrantedAuthority(List<MemberRole> roles){
		List<GrantedAuthority> list = new ArrayList<>();
		roles.forEach(
			role -> list.add(
					new SimpleGrantedAuthority(ROLE_PREFIX + role.getRoleName())));
		return list;
	}
}

위에도 언급했듯이 저같은 경우는 email로 로그인을 하기때문에 User클래스에 생성자의 username 위치에 member.getEmail()를 사용합니다.

다시 WebSecurityConfig

@Configuration  //이번에는 추가해 주자
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Resource(name="securityMemberService")
	private SecurityMemberService securityMemberService;

  @Override
  	protected void configure(HttpSecurity http) throws Exception {
  		http.csrf().disable();
  		http.headers().frameOptions().disable();
  		http.httpBasic();

  		http
  			.authorizeRequests()
  			.antMatchers("/", "/create", "/login**", "/logout**")
  			.permitAll()
  			.antMatchers("/boards/**", "/myboards", "/api/boards**", "api/login**", "/api/decks/**", "api/boards/**", "/api/boards/**", "/api/users**")
  			.hasRole("USER");

  		http
  			.formLogin()
  			.loginPage("/login")
        .defaultSuccessUrl("/")
        .failureUrl("/login");

  		http
  			.logout()
  			.logoutUrl("/logout")
  			.invalidateHttpSession(true);

  }

  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(securityMemberService).passwordEncoder(passwordEncoder());
  }
}

loginPage : 로그인 뷰 페이지

loginProcessingUrl : Post로 로그인을 처리할 Url

defaultSuccessUrl : 로그인 성공 후 이동할 페이지

failureUrl : 실패 후 이동할 페이지

login.html

<div class="login-box z-depth-2">

		<h6 id="loginFail" ></h6>
		<h4>Login To Trello</h4>

		<form class="login-form" action="/login" method="POST">
			<div class="row">
				<div class="input-field col s12">
					<input id="username" name="username" type="email" class="validate">
					<label for="username">Email</label>
				</div>
			</div>
			<div class="row">
				<div class="input-field col s12">
					<input id="password" name="password" type="password"
						class="validate"> <label for="password">Password</label>
				</div>
			</div>
			<button class="login-btn waves-effect waves-light btn" type="submit">로그인</button>
		</form>

		<p class="alternatives-separator">or</p>

		<a class="github-login waves-effect waves-light btn" href="#">
			<div class="github-icon"></div>
			<div class="github-login-text">github로 로그인</div>
		</a>

</div>

Spring Security의 기본 설정으로 인해 지금은 name 속성을 usernamepassword로 해둬야 제대로 동작 합니다.