최주호님의 위 강의를 듣고 정리합니다.
(프로젝트를 하며 복잡한 로그인 구현 구조에대해
이해할 수 있는 알찬 강의라고 생각합니다.)
spring start io로 검색하면
스프링을 만들 수 있는 사이트가 나옵니다.
✅ config > WebMvcConfig
이 파일의 용도는 Controller에서 return값이 String인 경우 우리가 의존성을 Mustache로 해놓았기 때문에 .mustache로 가는데 여기로 가지 않고 html파일이 호출되도록 설정하는 곳이다.
WebMvcConfigurer이라는 클래스를 MVC를 쉽게 구현할 수 있는 여러가지 메소드를 제공하는데
여기에서는 configureViewResolver(ViewResolverRegistry registry)라는 메서드를 이용하여
prefix와 suffix, 그리고 인코딩과 디코딩 방식을 추가해주었다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
MustacheViewResolver resolver = new MustacheViewResolver();
resolver.setCharset("UTF-8");
resolver.setContentType("text/html; charset=UTF-8");
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
registry.viewResolver(resolver);
}
}
✅ login 페이지
내가 /login 페이지를 구현하지 않았어도 security가 /login을 낚아채서 본인의 /login 페이지를 보여준다.
✅config > SecurityConfig
SecurityConfig를 통해서 로그인한 사람은 이 페이지에 접근할 수 있다.
로그인도 하면서 이 권한을 가진 사람만 이 페이지에 접근할 수 있다.
만약 로그인을 하지 않은 상태에서 로그인을 해야하는 페이지에 접근한다면
로그인 페이지로 전환된다.
로그인에 성공하면 이 페이지로 전환된다.
등의 필터를 넣을 수 있는 SecurigyConfig 클래스 파일은 아래와 같습니다.
@Configuration
@EnableWebSecurity//활성화 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)//secured 어노테이션 활성화 // preAuthorize 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
@Bean
public BCryptPasswordEncoder encodePwd(){
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated() // "user"는 로그인을 해야
.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 주소가 호출되면 시큐리타가 낚아채서 대신 로그인츨 진행 -> 따라서 우리가 /login을 구현하지 않아도 된다.
.defaultSuccessUrl("/");//login이 완려되면 메인페이지로
// 너가 그냥 loginForm에서 로그인에 성공하면 "/"
// 그러나 어떤 특정 페이지인 "/user"를 요청했다가 거절당해서 로그인 폼으로 돌아오면
// 로그인 성공하고 나서 "/"이 아닌 너가 요청했던 그 특정 페이지로 돌려준다
// "/loginForm"->"/"
//"/user"-> 거절->"/loginForm"->"/user"
}
}
⏺️ 로그인
✅ IndexController의 "/loginForm"
//ResponseBody로 설정해도 스프링 시큐리티가 해당 주소를 낚아채서
//json이 나오지 않고 로그인 페이지로 간다.
//-> 그러나 SecurityConfig 후에는 작동하지 않아 json이 보인다.
@GetMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
IndexController에는 "/login"이 없습니다.
"/loginForm"만 있습니다.
"/login"이 없어도 스프링 시큐리티가 구현해주기 때문입니다.
하지만 "login"이 진행될 때 필요한 session에 들어갈 내용 등은 개발자가 구현해야합니다.
물론 초반에는 "/login"으로 해 놓고 스프링 시큐리티를 간접적으로 경험할 수 있습니다.
우리가 login.html을 만들어 놓지 않고 Controller에 GetMapping("/login")을 해 놓아도
스프링 시큐리티가 낚아채서 시큐리티의 로그인 페이지로 넘깁니다.
✅ resources/templates > loginForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username"/><br/>
<input type="password" name="password" placeholder="Password"/><br/>
<button>로그인</button>
</form>
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
✅ application.yml의 DB 설정 부분
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Seoul
username:
password:
코드를 입력하세요
security라는 DB가 필요합니다.
또한 DB에 user table을 추가해야 합니다.
따라서 아래와 같이 User라는 Entity를 만들어주도록 하겠습니다.
✅ model> User
```java
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role;
@CreationTimestamp
private Timestamp createDate;
}
run을 하면
security라는 DB 아래에 user라는 테이블이 위와 같은 컬럼과 속성에 맞게 만들어집니다.
⏺️ 회원가입
✅ resources/templates > loginForm.html
...
<h1>로그인 페이지</h1>
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username"/><br/>
<input type="password" name="password" placeholder="Password"/><br/>
<button>로그인</button>
</form>
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>
위와 같이 링크가 걸려있기 때문에 "/joinForm"으로 가는 Controller를 만들어줘야 합니다.
✅ IndexController의 "/joinForm" : 회원가입 페이지로 이동
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
"/joinForm"이 호출되면 joinForm.html인 회원가입을 할 수 있는 페이지로 이동합니다.
✅ resources/templates > joinForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<form action="/join" method="POST">
<input type="text" name="username" placeholder="Username"/><br/>
<input type="password" name="password" placeholder="Password"/><br/>
<input type="email" name="email" placeholder="Email"/><br/>
<button>회원가입</button>
</form>
</body>
</html>
위에 보면 회원가입 버튼을 누르면 Post로 "/join"을 호출합니다
"/join"이 호출되면 form으로 받은 데이터를 User DB에 저장하는 과정이 필요합니다.
✅ IndexController의 "/join" : 회원가입하여 user 정보를 user DB에 저장하는 과정
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/join")
public String join(User user) {
System.out.println(user);
user.setRole("ROLE_USER");
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encPassword);
userRepository.save(user);
// 회원가입은 잘됨 비밀번호 =>1234
//그러나 시큐리티로 로그인할 수 없음
// 이유는 패스워드가 암호화가 안되었기 때문이다.
return "redirect:/loginForm";
}
위에 보면 userRepository.save
DB에 insert 할 수 있는 함수가 등장합니다.
✅ repository > UserRepository
//CRUD 함수를 JpaRepository가 들고 있음
//@Repository라는 어노테이션이 없어도 IoC가 된다.
//이유는 JpaRepository를 상속했기 때문에 가능하다
public interface UserRepository extends JpaRepository<User,Integer> {
//findBy 규칙 ->Username 문법
//select * from user where username=?
public User findByUsername(String username);
}
✅ SecuriyConfig : 스프링 시큐리티를 이용하기 위해 패스워드 암호화
@Configuration
@EnableWebSecurity//활성화 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)//secured 어노테이션 활성화 // preAuthorize 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
@Bean
public BCryptPasswordEncoder encodePwd(){
return new BCryptPasswordEncoder();
}
...
✅ config > auth > PrincipalDetails
시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킵니다.
로그인 진행이 완료가 되면 session을 만들어 줍니다.
시큐리티의 session이 있습니다.
우리가 알고 있는 그 session이며 같은 session 공간인데 시큐리티가 자신만의 session 공간을 갖습니다. (Security ContextHolder)
여기에 들어갈 수 있는 정보는
오브젝트가 정해져 있습니다. => Authentication 타입의 객체
Authentication => User정보가 있어야 됨
User오브젝트타입=> UserDetails 타입 객체
Security Session 영역 => 여기 들어갈 객체 Authentication => 여기에 저장된 User 정보UserDetails
public class PrincipalDetails implements UserDetails {
//PrincipalDetail Type = UserDatails
//그러면 PrincipalDetails 객체를 Authentication 객체에 넣을 수 있다.
private User user;//콤포지션
public PrincipalDetails(User user){
this.user=user;
}
//해당 User의 권한을 리턴하는 곳!!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//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
public boolean isAccountNonExpired() {
return true;
}
// 계정 안잠겼니?
@Override
public boolean isAccountNonLocked() {
return true;
}
//계정 만든지 1년 안지났니?
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정 활성화되었니?
@Override
public boolean isEnabled() {
//우리 사이트 1년 동안 회원이 로그인을 안하면
//휴면 계정으로 만들기
//user.getLoginDate -> 현재시간 - 로그인 시간 1년 시간을 초과하면
return true;
}
}
✅ config > auth > PrincipalDetailsService : 회원인가 확인하고 있으면 Security session 생성
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
//시큐리시 session (Authentication( UserDetails))
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(username!=null){
return new PrincipalDetails(userEntity);
}
return null;
}
}
SecurityConfig
에서 loginProcessingUrl("/login")을 걸어두었습니다.
로그인에 성공하면 "/login"을 호출하도록 되어 있는데
"/login" 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 객체의loadUserByUsername함수를 실행합니다.
이는 규칙입니다.
loadUserByUsername의 매개변수명이 아주 중요합니다.
String username이라는 변수명은
loginForm.html
의 <input type="text" name="username" placeholder="Username"/><br/>
name 속성과 변수명 통일해야합니다!
만약 다르다면 SecurityConfig
에서
.loginPage("/loginForm")
.usernameParameter("input의 name 변수명") // 추가됨
.loginProcessingUrl("/login")
위와 같이 usernameParamet라는 메서드를 추가해서 변수명을 명시해줘야 합니다.
권한에 따라 접근할 수 있는 페이지를 글로벌하게 설정할 수도 있지만
글로벌하지 않게 딱 하나의 페이지에 설정하는 방법도 있습니다.
✅ config > SecurityConfig
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)//secured 어노테이션 활성화 // preAuthorize 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
바로 @EnableGlobalMethodSecurity를 이용하는 방법입니다.
✅ controller > IndexController
@Secured("ROLE_ADMIN") // 개인적으로 걸 때 //@EnableGlobalMethodSecurity(securedEnabled = true)
@GetMapping("/info")
public @ResponseBody String info(){
return "개인정보";
}
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") // data라는 메소드가 실행되기 직전에 실행
@GetMapping("/data")
public @ResponseBody String data(){
return "데이터정보";
}