-
스프링부트 with JPA 블로그 12강 - 시큐리티 코드 짜기스프링부트 with JPA 블로그 2020. 6. 9. 10:05
com.cos.blog.config 이하에 파일 생성
주소 세팅
- / (컨텍스트 삭제)
- /auth/joinProc
- /auth/loginProc
- /auth/joinForm
- /auth/loginForm
header.jsp joinForm.jsp user.js UserApiController.java UserController.java
SecurityConfig.java
로그인 페이지 커스터마이징
package com.cos.blog.config; import org.springframework.context.annotation.Configuration; 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.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; @Configuration // 빈 등록 (객체 생성) @EnableWebSecurity // 필터 체인에 등록 (스프링 시큐리티 활성화) @EnableGlobalMethodSecurity(prePostEnabled=true) // 특정 주소 접근시 권한 및 인증을 pre(미리) 체크하겠다. public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/auth/loginForm", "/auth/joinForm") .permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/auth/loginForm"); } }
테스트해보자!!!
SecurityConfig.java 수정
로그인 성공, 로그아웃 성공 URL 커스터마이징
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.config.auth.PrincipalDetailService; @Configuration // 빈 등록 (객체 생성) @EnableWebSecurity // 필터 체인에 등록 (스프링 시큐리티 활성화) @EnableGlobalMethodSecurity(prePostEnabled=true) // 특정 주소 접근시 권한 및 인증을 pre(미리) 체크하겠다. public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private PrincipalDetailService principalDetailService; // 1. Bean 어노테이션은 메서드에 붙여서 객체 생성시 사용 @Bean public BCryptPasswordEncoder encodePWD() { return new BCryptPasswordEncoder(); } // 2. 시큐리티가 로그인할 때 어떤 암호화로 인코딩해서 비번을 비교할지 알려줘야 함. @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD()); } // 3. 필터링 @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/", "/css/**", "/images/**", "/js/**", "/auth/**") .permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/auth/loginForm") .loginProcessingUrl("/auth/loginProc") .defaultSuccessUrl("/"); } // 참고 : .headers().frameOptions().disable() // 아이프레임 접근 막기 // 참고 : .csrf().disable() // csrf 토큰 비활성화 (테스트시 걸어주는 것이 좋음) }
loginForm.jsp 변경 (input type에 name 속성 넣기)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="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" class="form-control" placeholder="Enter username" id="username" name="username"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" placeholder="Enter password" id="password" name="password"> </div> <div class="form-group form-check"> <label class="form-check-label"> <input class="form-check-input" type="checkbox" name="remember"> Remember me </label> </div> <button class="btn btn-primary">로그인</button> </form> </div> <%@ include file="../layout/footer.jsp"%>
user.js 수정
let index = { init: function(){ $("#btn-save").on("click", ()=>{ // function(){} , ()=>{} this를 바인딩하기 위해서!! this.save(); }); }, save: function(){ //alert('user의 save함수 호출됨'); let data = { username: $("#username").val(), password: $("#password").val(), email: $("#email").val() }; //console.log(data); // ajax호출시 default가 비동기 호출 // ajax 통신을 이용해서 3개의 데이터를 json으로 변경하여 insert 요청!! // ajax가 통신을 성공하고 서버가 json을 리턴해주면 자동으로 자바 오브젝트로 변환해주네요. $.ajax({ type: "POST", url: "/auth/joinProc", data: JSON.stringify(data), // http body데이터 contentType: "application/json; charset=utf-8",// body데이터가 어떤 타입인지(MIME) dataType: "json" // 요청을 서버로해서 응답이 왔을 때 기본적으로 모든 것이 문자열 (생긴게 json이라면) => javascript오브젝트로 변경 }).done(function(resp){ alert("회원가입이 완료되었습니다."); //console.log(resp); location.href = "/"; }).fail(function(error){ alert(JSON.stringify(error)); }); }, } index.init();
com.cos.blog.config.auth 이하에 파일 생성
PrincipalDetail.java
package com.cos.blog.config.auth; import java.util.ArrayList; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.cos.blog.model.User; //principal (접근 주체) = 세션처럼 사용 = Spring Security Context 에 보관됨 public class PrincipalDetail implements UserDetails{ private static final long serialVersionUID = 7645618956884452156L; public PrincipalDetail(User user) { this.user = user; } private User user; public User getUser() { return user; } public void setUser(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<SimpleGrantedAuthority> collectors = new ArrayList<>(); collectors.add(new SimpleGrantedAuthority("ROLE_"+user.getRole())); return collectors; } }
UserRepository.java 수정
package com.cos.blog.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import com.cos.blog.model.User; // DAO // 자동으로 bean등록이 된다. // @Repository // 생략 가능하다. public interface UserRepository extends JpaRepository<User, Integer>{ // JPA Naming 쿼리 // SELECT * FROM user WHERE username = ?1 AND password = ?2; // User findByUsernameAndPassword(String username, String password); // @Query(value="SELECT * FROM user WHERE username = ?1 AND password = ?2", nativeQuery = true) // User login(String username, String password); Optional<User> findByUsername(String username); }
UserService.java 수정
package com.cos.blog.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.blog.model.User; import com.cos.blog.repository.UserRepository; // 스프링이 컴포넌트 스캔을 통해서 Bean에 등록을 해줌. IoC를 해준다. @Service public class UserService { @Autowired private UserRepository userRepository; @Transactional public void 회원가입(User user) { userRepository.save(user); } // @Transactional(readOnly = true) // Select할 때 트랜잭션 시작, 서비스 종료시에 트랜잭션 종료 (정합성) // public User 로그인(User user) { // return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword()); // } }
UserApiController.java 수정
package com.cos.blog.controller.api; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; 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; @PostMapping("/auth/joinProc") public ResponseDto<Integer> save(@RequestBody User user) { // username, password, email user.setRole(RoleType.USER); userService.회원가입(user); return new ResponseDto<Integer>(HttpStatus.OK.value(), 1); // 자바오브젝트를 JSON으로 변환해서 리턴 (Jackson) } }
PrincipalDetailService.java
package com.cos.blog.config.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 등록 (IoC) public class PrincipalDetailService implements UserDetailsService{ @Autowired private UserRepository userRepository; // LoginForm에서 action="/loginProc" 되면 // 스프링 필터 체인이 낚아채서 loadUserByUsername함수를 호출한다. @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.config.auth.PrincipalDetailService; @Configuration // 빈 등록 (객체 생성) @EnableWebSecurity // 필터 체인에 등록 (스프링 시큐리티 활성화) @EnableGlobalMethodSecurity(prePostEnabled=true) // 특정 주소 접근시 권한 및 인증을 pre(미리) 체크하겠다. public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private PrincipalDetailService principalDetailService; // 1. Bean 어노테이션은 메서드에 붙여서 객체 생성시 사용 @Bean public BCryptPasswordEncoder encodePWD() { return new BCryptPasswordEncoder(); } // 2. 시큐리티가 로그인할 때 어떤 암호화로 인코딩해서 비번을 비교할지 알려줘야 함. @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD()); } // 3. 필터링 @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/", "/css/**", "/images/**", "/js/**", "/auth/**") .permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/auth/loginForm") .loginProcessingUrl("/auth/loginProc") .defaultSuccessUrl("/"); } // 참고 : .headers().frameOptions().disable() // 아이프레임 접근 막기 // 참고 : .csrf().disable() // csrf 토큰 비활성화 (테스트시 걸어주는 것이 좋음) }
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.User; import com.cos.blog.repository.UserRepository; // 스프링이 컴포넌트 스캔을 통해서 Bean에 등록을 해줌. IoC를 해준다. @Service public class UserService { @Autowired private BCryptPasswordEncoder encoder; @Autowired private UserRepository userRepository; @Transactional public void 회원가입(User user) { // 회원가입시 패스워드를 인코딩 하지 않으면 시큐리티 로그인을 이용할 수 없다. String rawPassword = user.getPassword(); String encPassword = encoder.encode(rawPassword); user.setPassword(encPassword); userRepository.save(user); } // @Transactional(readOnly = true) // Select할 때 트랜잭션 시작, 서비스 종료시에 트랜잭션 종료 (정합성) // public User 로그인(User user) { // return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword()); // } }
테스트해보자!!!
이 오류가 난다면 /js 이하 폴더 접근이 막혀 있다는 뜻. SecurityConfig.java에서 풀어줘야 함.
회원가입 테스트
로그인 테스트
'스프링부트 with JPA 블로그' 카테고리의 다른 글
스프링부트 with JPA 블로그 14강 - 카카오 로그인 하기 (0) 2020.06.16 스프링부트 with JPA 블로그 13강 - 시큐리티 동작 원리 (2) 2020.06.10 스프링부트 with JPA 블로그 11강 - DB격리수준 (0) 2020.06.07 스프링부트 with JPA 블로그 10강 - 화면구현 (3) 2020.05.20 스프링부트 with JPA 블로그 9강 - Json 데이터로 통신하기 (1) 2020.05.10 댓글