스프링 시큐리티를 사용, 간단한 로그인 예제를 구현한다.
@Configuration
@EnableWebSecurity// 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableGlobalMethodSecurity(securedEnabled = true , prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login") // login 주소가 호출이 되면 시큐리티가 대신 낚아서 호출해준다
.defaultSuccessUrl("/");
}
}
@EnableGlobalMethodSecurity(securedEnabled = true , prePostEnabled = true)은 @Secured("ROLE_ADMIN")과
@preAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN)")를 사용하기 위해 추가한 어노테이션이다.
antMatchers를 통해 해당 경로에 대한 접근 설정을 할 수 있다.
loginProcessingUrl은 로그인 진행 시 어떤 경로에서 인증 시스템을 진행할 것인지를 설정하는 메소드이다.
WebSecurityConfigurerAdapter를 상속 받아 오버라이드 할 수 있다
접근 설정
Login 관련 설정
Logout 관련 설정
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
BCryptPasswordEncoder를 securityconfig 안에 작성하는 예시가 많았는데 몇몇 경우에서 순환 참조가 일어나 오류가 발생.
@Service
public class encoderService {
// Password 인코딩 방식에 BCrypt 암호화 방식 사용
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
따로 분리하여 진행하였다.
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String role;
@CreationTimestamp
private Timestamp createDate;
}
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}
security 흐름을 파악하기 위해 최대한 간단한 형태로 진행했다.
public class PrincipalDetails implements UserDetails {
private User user; // 콤포지션
public PrincipalDetails(User user){
this.user = user;
}
// 해당 User의 권한을 리턴하는 곳!!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//user.getRole()이 String이라 변환해야 됨
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
//이 계정 만료 되었는가? false 만료/ true 유효
public boolean isAccountNonExpired() {
return true;
}
@Override
//잠겼는가
public boolean isAccountNonLocked() {
return true;
}
@Override
//비밀번호를 오래 썼는가
public boolean isCredentialsNonExpired() {
return true;
}
@Override
//활성화 여부
public boolean isEnabled() {
return true;
}
}
시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
로그인을 진행이 완료가 되면 시큐리티 session을 만들어준다.(Security ContextHolder)
Security Session => Authentication => UserDetails 순으로 전환해서 저장한다.
로그인을 위한 username (혹은 id, email) 이 DB에 있는지 확인하는 메서드 loadUserByUsername 메서드를 작성합니다.
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity != null){
return new PrincipalDetails(userEntity);
}
return null;
}
}
user 계정하나를 만들어 테스트를 해보겠습니다.
@Controller
public class indexController {
@Autowired
private UserRepository userRepository;
@Autowired
PasswordEncoder passwordEncoder;
@GetMapping({ "", "/" })
public @ResponseBody
String index() {
return "index";
}
@GetMapping("/admin")
public @ResponseBody String admin() {
return "어드민 페이지입니다.";
}
//@PostAuthorize("hasRole('ROLE_MANAGER')")
//@PreAuthorize("hasRole('ROLE_MANAGER')")
//@Secured("ROLE_MANAGER")
@GetMapping("/manager")
public @ResponseBody String manager() {
return "매니저 페이지입니다.";
}
@GetMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
@PostMapping("/join")
public @ResponseBody String join( User user) {
user.setRole("ROLE_USER");
String rawRassword = user.getPassword();
String encPassword = passwordEncoder.encode(rawRassword);
user.setPassword(encPassword);
userRepository.save(user);
return "redirect:/loginForm";
}
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
@GetMapping("/info")
public @ResponseBody String info(){
return "개인정보";
}
}
securityconfig에서 .loginPage("/loginForm") 설정을 했기 때문에 인증이 되지 않았을 때 loginForm이 실행된다.
만들어지 계정으로 로그인을 하면
index 페이지로 잘 이동하는 것을 확인 할 수 있다.
하지만 ROLE_USER 권한으로는 인가되지 안은 /admin으로 이동 시
403 forbidden 오류가 난 것을 확인 할 수 있다.
이상으로 정말 간단한 형태의 security의 세션 방식 로그인을 구현했습니다.
정리용으로 포스트 했고 아주 간단한 내용인데요 생각보다 시간이 오래 걸렸습니다.
다음 포스트는 oauth2로 sns인증과 jwt토큰을 사용한 인증을 구현하려고 합니다