위처럼 자원에 대한 접근을 풀고, 밑에서는 세부적인 설정을 따로 넣어준다. 일단은 원활한 진행을 위해 모든 경로에 대해 permitAll을 줬다.
적용된 Security 기반 회원가입 로직 추가하기
회원가입 이전에, 먼저 User정보를 구성할 테이블을 생성해야하는데 Security의 내부적으로 사용하는 클래스의 이름이 User이다. 겹치기 때문에 Account나 Member라는 이름을 사용해서 구현하자.
Member.java
@Getter
@Setter
@Entity
@EqualsAndHashCode(of = "uid")
@ToString
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true, length=50)
private String uid;
@Column(nullable = false, length=200)
private String upw;
@Column(nullable = false, unique = true, length=50)
private String uemail;
@CreationTimestamp
private Date regdate;
@UpdateTimestamp
private Date updatedate;
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
@JoinColumn(name="uid")
private List<MemberRole> roles;
}
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER) 설정은, cascade의 경우에는 엔티티들의 영속관계를 한번에 처리하지 못하기 때문에 이에 대한 cascade 설정을 추가하는것이고, member와 member_role을 둘다 동시에 조회하기 위해서 fetch 설정을 즉시 로딩으로 EAGER 설정을 주어야 에러가 발생하지 않습니다.
MemberRole.java
@Getter
@Setter
@Entity
@EqualsAndHashCode(of = "rno")
@ToString
public class MemberRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long rno;
private String roleName;
}
MemberRepository.java
public interface MemberRepository extends CrudRepository<Member, Long> {
}
Member가 제대로 동작하는지 확인을 위한 테스트 코드 작성
@RunWith(SpringRunner.class)
@SpringBootTest
@Log
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Test
public void insertTest() {
for(int i=0; i<100; i++) {
Member member = new Member();
member.setUid("user" + i);
member.setUpw("pw" + i);
member.setUemail("hihi@" + i);
MemberRole role = new MemberRole();
if(i <= 80) {
role.setRoleName("BASIC");
}else if(i <= 90) {
role.setRoleName("MANAGER");
}else {
role.setRoleName("ADMIN");
}
member.setRoles(Arrays.asList(role));
memberRepository.save(member);
}
}
@Test
public void testMember() {
Optional<Member> result = Optional.ofNullable(memberRepository.findOne(85L));
result.ifPresent(member -> log.info("member " + member));
}
}
회원가입 로직 구현하기
위에서 제작한 Member와 MemberRole을 기반으로 회원가입 로직을 구현해보자.
MemberController.java
@Controller
@RequestMapping("/member")
public class MemberController {
@Autowired
MemberRepository memberRepository;
@PostMapping("")
public String create(Member member) {
MemberRole role = new MemberRole();
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
member.setUpw(passwordEncoder.encode(member.getUpw()));
role.setRoleName("BASIC");
member.setRoles(Arrays.asList(role));
memberRepository.save(member);
return "redirect:/";
}
}
스프링 시큐리티에서 지원해주는 BCryptPasswordEncoder를 이용해 회원 비밀번호를 암호화하고, MemberRole을 정의해 Member에 넣어주고 save를 했다.
각각 Member의 파라미터에 해당하는 값을 매칭하고, thymeleaf를 사용한 csrf 토큰 관련 설정을 추가해주면 된다. 이후 Get으로 signUp페이지에 연결하고 회원가입을 진행하면 정상적으로 회원가입이 될 거다.
정상적으로 구현했다면, 위와같이 암호화된 비밀번호가 저장된것을 확인할 수 있다.
스프링 시큐리티 로그인 기능 구현하기
스프링 시큐리티에서 로그인 처리를 구현하려면 SecurityConfig에서 AuthenticationManagerBuilder를 주입해서 인증에 대한 처리를 해야 한다.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
}
위와같은 설정을 SecurityConfig에 넣고, 저기서 UserDetailsService를 이용해 로그인 인증 처리를 해주어야 한다.
이 과정에서 우리의 데이터베이스를 바탕으로 인증을 처리하기 위해서는 UserDetailsService를 구현하고 이를 HttpSecurity객체가 사용하도록 지정하면 우리가 만든 인증 로직을 바탕으로 동작하게 된다.
CustomUserDetailsService를 구현하면서 MemberRepository와 연동해 우리의 데이터베이스에 저장된 회원정보를 바탕으로 인증을 구현해야하는데 스프링 시큐리티의 User와 우리의 Member, GrantedAuthority와 우리의 MemberRole의 타입이 일치하지 않는 문제가 생긴다.
public class CustomUserDetailsService implements UserDetailsService{
@Autowired
MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;//반환할 타입이 Member와 맞지 않는다.
}
}
위처럼 loadUserByUsername 메소드는 UserDetails를 반환하는데, MemberRepository에서 얻어온 Member와는 타입이 맞지 않는다. 이 타입을 맞춰주기 위해 Spring Security의 User를 상속하는 커스텀 유저 클래스를 추가해주자.
SecurirtyMember 구현(SpringSecurity의 User 상속)
@Getter
@Setter
public class SecurityMember extends User {
private static final String ROLE_PREFIX = "ROLE_";
private static final long serialVersionUID = 1L;
public SecurityMember(Member member) {
super(member.getUid(), member.getUpw(), 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;
}
}
위에서 SecurityUser의 생성자로 Member를 받아서 Spring Security의 User타입과 우리가 구현한 Member의 값들을 매칭시켜 주자.
이제 로그인 뷰 페이지에 username으로 name을 지정해주어야 정상적으로 받을 수 있다.
그리고 csrf 관련 설정을 위해 thymeleaf를 이용해 csrf토큰을 추가해주어야 요청을 정상적으로 받을 수 있다.
로그인 성공시의 부가적인 작업 추가하기
로그인 성공 시, 리다이렉트되기 전의 페이지로 이동시켜준다거나 하는 부가작업을 추가하기 위해서는 SecurityConfig의 successHandler에 핸들러를 등록해주어야 한다, 등록할 핸들러의 타입은 AuthenticationSuccessHandler 타입이기 때문에 AuthenticationSuccessHandler를 구현하면서 로그인 성공시 사용되는 핸들러인 SavedRequestAwareAuthenticationSuccessHandler를 상속받아 구현하고 SecurityConfig에 등록시켜 주면 된다.
SecurityConfig.java에 successHandler를 등록하고, 빈 형태로 등록해두면 된다.
http.successHandler(successHandler())
@Bean
public AuthenticationSuccessHandler successHandler() {
return new CustomLoginSuccessHandler("/");//default로 이동할 url
}
이후 로그인 시에만 해당 페이지로 접근 가능하도록 막아두려면 아래와 같은 설정을 추가하면 된다.
.antMatchers("/board/**").authenticated()
이제 /board의 경로에는 로그인 시에만 접근이 가능하도록 막히게 된다.
다른 페이지에서 board에 접근해보고, 로그인 시에 원하는 페이지로 정상적으로 이동되는지 테스트해보자.
전체 SecurityConfig 파일
@EnableWebSecurity
@EnableOAuth2Client
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
OAuth2ClientContext oauth2ClientContext;
@Override
public void configure(WebSecurity web) throws Exception
{
web.ignoring().antMatchers("/css/**", "/script/**", "image/**", "/fonts/**", "lib/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/board/**").authenticated()
.antMatchers("/**").permitAll()
.and().formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.successHandler(successHandler())
.failureUrl("/login")
.and()
.logout()
.and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
}
@Bean
public AuthenticationSuccessHandler successHandler() {
return new CustomLoginSuccessHandler("/defaultUrl");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filters.add(ssoFilter(github(), "/login/github"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(), client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
class ClientResources {
@NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
@NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
}