※본 강의는 데어프로그래밍님 스프링부트 블로그프로젝트 강의 수강 후 작성한 내용입니다.
[xss와 csrf란?]
xss란 자바스크립트 공격읻. 예를들어 게시판이라고 했을 때 글쓰기에서 제목부분에 자바스크립트로 alert를 5만번 나오게 하는 코드를 작성해서 글쓰기를 완료하면 게시판에서 제목부분의 자바스크립트를 읽어서 alert가 5만번이 뜨며 페이지가 다운되게 하는 공격이다. 이 공격을 막기위해서는 <나 <script>를 막으면 되는데 lucy xss filter에서 이 공격을 막기위한 필터를 제공한다.
csrf에 대해서도 그 예를들어 간단하게 적어보자면 뭔가 중요한 정보를 파라미터로 받아서 update하는 url이 있다고 했을 때 해당 url은 권한이 ADMIN인 사람만 들어갈 수 있도록 프로그래밍을 해둘 것이다. 공격자는 관리자가 어떤 이미지를 클릭하도록 유도하고 그 이미지에 해당 url로 들어가는 하이퍼링크를 달아둔다. 이미지에 <a href="ADMIN만 접속가능한 url"><img src="~"></a> 를 해두는 것이다. 이럴경우 관리자가 해당 url로 접속하게 되기 때문에 공격자가 원한대로 정보가 update된다.
이 공격을 막기위해서 일단 첫번째로 이러한 요청은 GET방식이 아닌 POST방식으로 받아야한다. 또 다른 방법으로는 같은 도메인 상에서 요청이 들어오지 않는다면 차단하도록 하는 Referrer검증방법, 사용자 세션에 csrf토큰을 저장해서 요청에 대해 서버단이 검증하는 방법 등이 있다. 토큰을 사용하는경우 사용자는 요청을 할때마다 그 토큰을 같이 보내게 되는데 서버측에서 요청을 보고 토큰이 정상적으로 왔으면 정상적인 사용자임을 판단한다.
[스프링 시큐리티를 이용한 로그인 구현 코드]
com.cos.blog.auth패키지 생성
해당패키지에 PrincipalDetail.java, PrincipalDetailService.java 클래스 생성
PrincipalDetail.java
package com.cos.blog.auth;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.cos.blog.model.User;
//스프링시큐리티가 로그인요청을 가로채서 로그인을 진행하고 완료가 되면 User Details타입의 오브젝트를
//스프링시큐리티의 고유한 세션저장소에 저장을 해준다. 그때 저장되는게 UserDetails타입의 PrincipalDetail이 저장되는 것이다.
//그래서 이게 저장될때 우리가 만들어둔 User가 포함되어있어야한다.
public class PrincipalDetail implements UserDetails{
private User user; //이렇게 user객체를 품고있는걸 콤포지션이라고 함.
//alt+shift+s해서 override method하면 한번에 다 해올수있음
public PrincipalDetail(User user) {
this.user = user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
//계정이 만료되지 않았는지를 리턴한다. true:만료안됨
@Override
public boolean isAccountNonExpired() {
return true;
}
//계정이 잠겨있지 않았는지 리턴한다(true:잠기지않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
//비밀번호가 만료되지 않았는지 리턴한다(true:만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정이 활성화가 되어있는지 리턴(true:계정활성화됨)
@Override
public boolean isEnabled() {
return true;
}
//계정권한리턴. 리턴타입 주의. 권한이 여러개 있을 수 있어서 루프를 돌아야하는데 우리는 한개만
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
// collectors.add(new GrantedAuthority() {
//
// @Override
// public String getAuthority() {
// return "ROLE_"+user.getRole(); //스프링에서 role받을때 규칙. 앞에 ROLE_을 꼭 붙여서 리턴해주어야함.
// }
// });
//위에꺼를 람다식 사용해서 표현한것. 들어갈수있는게 하나씩밖에 없기때문에 가능.
collectors.add(()->{return "ROLE_"+user.getRole();});
return collectors;
}
}
주석에도 적혀있지만 스프링에서는 Role을 리턴할 때 앞에 ROLE_을 붙여줘야한다! 그래야 스프링이 role이라고 인식함.
PrincipalDetailService.java
package com.cos.blog.auth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;
@Service //Bean에 등록
public class PrincipalDetailService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
//스프링이 로그인요청을 가로챌 때, username, password 변수 2개를 가로채는데
//password부분 처리는 알아서한다. username이 db에 있는지만 확인해주면됨. 그 확인을 아래함수에서 한다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(()->{
return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다:"+username);
});
return new PrincipalDetail(principal);
}
}
SecurityConfig.java
package com.cos.blog.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.cos.blog.auth.PrincipalDetailService;
//이 세개가 시큐리티 세트임
@Configuration //빈등록:스프링컨테이너에서 객체를 관리할 수 있게 하는것. IoC로 관리됨.
@EnableWebSecurity //시큐리티 필터로 등록이 된다.
@EnableGlobalMethodSecurity(prePostEnabled = true) //특정주소로 접근하면 권한 및 인증을 미리 체크하겠다.
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private PrincipalDetailService principalDetailService;
//Bean 어노테이션은 메서드에 붙여서 객체 생성시 사용
@Bean //IoC가 된다. 즉, 이 함수가 리턴하는 값을 스프링이 관리한다. 그래서 필요할때마다 가지고와서 쓰면된다.
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder(); //이객체를 통해서 .encode("1234") 해서 hash할 수 있음
}
//시큐리티가 대신 로그인해주는데 password를 가로채기를하는데
//해당 password가 뭘로 해쉬가 되어 회원가입이 되었는지 알아야
//같은 해쉬로 암호화해서 db에 있는 해쉬랑 비교할 수 있음. 이거안하면 password비교못함
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
//필터링
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //csrf tocken비활성화(테스트시 걸어주는것이 좋음)
.authorizeRequests() //request가 들어오면
.antMatchers("/","/css/**","/images/**","/js/**","/auth/**") //이 주소로 들어오는 애들은 누구나 들어올 수 있다
.permitAll()
.anyRequest().authenticated() //이게아닌 다른요청은 인증이 되어야한다. 그래서 loginForm으로 간다.
.and()
.formLogin().loginPage("/auth/loginForm")
.loginProcessingUrl("/auth/loginProc") //스프링시큐리티가 해당주소로 오는 로그인을 가로채서 대신 로그인해준다. 이거때문에 userDetails type을 가지고있는 user object를 만들어야함.
.defaultSuccessUrl("/"); //로그인끝나면 이 주소로 가게해줌
}
}
코드에보면 .csrf().disable()이 있는데 우리는 ajax 자바스크립트를 이용해서 요청했기 때문에 csrf토큰이 없다. 따라서 서버에서 막아버리기 때문에 csrf token을 disable해둔것이다. 나중에 이 방법을 사용하고 싶다면 csrf token을 날리도록 설정하고 해당 코드를 지우면된다.
UserApiController.java
package com.cos.blog.controller.api;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.cos.blog.dto.ResponseDto;
import com.cos.blog.model.RoleType;
import com.cos.blog.model.User;
import com.cos.blog.service.UserService;
@RestController
public class UserApiController {
@Autowired
private UserService userService; //@Service해놔서 IoC에 등록되기 때문에 dependency 인잭션?해서 사용할 수 있음
//spring 시큐리티가 /auth/loginProc가로채게 할것이기 때문에 여기에 안만듦
@PostMapping("/auth/joinProc")
public ResponseDto<Integer> save(@RequestBody User user) {
System.out.println("UserApiController: save호출됨");
//return new ResponseDto<Integer>(200,1); //data가 1, status가 200. 200은 통신이 정상적으로 작동했다는 뜻.
//user.setRole(RoleType.USER); //Service에서 하는걸로 변경!
userService.회원가입(user); //받은걸 그대로 넣어서 회원가입 하면됨.
return new ResponseDto<Integer>(HttpStatus.OK.value(),1); //위에꺼랑 같은 의미.
//이렇게 하려면 ResponseDto의status type을 HttpStatus로 해야함. 이게 위에꺼보다 안전함. 데이터부분은 나중에 디비에 저장하고 리턴된 값이 들어갈것임
}
}
UserRepository.java
package com.cos.blog.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.cos.blog.model.User;
//받은 데이터를 db에 저장하기 위한 interface
//자동으로 bean등록이 되기때문에 @Repository 생략가능
public interface UserRepository extends JpaRepository<User, Integer>{
//해당 인터페이스는 user table이 관리하는 repository이고, user table의 primary key는 integer라는 뚯
//JPA naming 쿼리
Optional<User> findByUsername(String username);
}
UserService.java
package com.cos.blog.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.cos.blog.model.RoleType;
import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;
@Service //이 어노테이션이 있어야 스프링이 컴포넌트 스캔을 통해서 Bean에 등록해준다.IoC를 해준다는 뜻
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder encoder;
@Transactional
public void 회원가입(User user) {
String rawPassword = user.getPassword(); //password원문
String encPassword = encoder.encode(rawPassword); //hash된 password
user.setPassword(encPassword);
user.setRole(RoleType.USER);
userRepository.save(user);
}
}
스프링 시큐리티를 사용해서 로그인하려면 회원가입할 때 비밀번호를 hash해서 DB에 insert해야한다.
loginForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ include file="../layout/header.jsp" %>
<div class="container">
<form action="/auth/loginProc" method="post">
<div class="form-group">
<label for="Username">Username</label>
<input type="text" name="username" class="form-control" placeholder="Enter username" id="username">
</div>
<div class="form-group">
<label for="pwd">Password</label>
<input type="password" name="password" class="form-control" placeholder="Enter password" id="password">
</div>
<button id="btn-login" class="btn btn-primary">로그인 완료</button>
</form>
</div>
<br />
<%@ include file="../layout/footer.jsp" %>
지난시간에 button이동했던걸 다시 form 안으로 넣었다.
'SpringBoot > Blog프로젝트 with JPA &데어프로그래밍님' 카테고리의 다른 글
21.1.9 TIL - 스프링 JPA의 OSIV전략과 전통적인 로그인 만들기(JPA Naming 쿼리) (0) | 2021.01.11 |
---|---|
21.1.7 TIL - 트랜잭션과 서비스, 데이터베이스 격리수준 (0) | 2021.01.07 |
21.1.5 TIL - ajax통신 이유 (0) | 2021.01.07 |
21.1.4 TIL - JPA를 이용한 database update, delete, 영속성 컨텍스트와 더티체킹 (0) | 2021.01.04 |
21.1.3 TIL - 데이터 select와 JPA paging (0) | 2021.01.04 |