SpringBoot [스프링부트] Spring Security Basic(2)
프로젝트 개요
- User는 회원 가입을 하고 로그인을 할 수 있다.
- User의 권한을 가진 사람만 캐릭터를 만들 수 있다.
- 캐릭터는 여러 개 생성 가능하다.
- Guest의 권한을 가진 사람은 /user과 /character에 접근이 불가능하다.
위의 요구사항을 토대로 만들겠습니다.
기본 개념들에 대한 설명은 생략하고, Security에 관한 내용만 설명하겠습니다.
1. User
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@OneToMany(mappedBy = "user")
public List<Character> characters = new ArrayList<>();
@Builder
public User(String email, String password){
this.email=email;
this.password=password;
this.role=Role.ROLE_USER;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role.name()));
return simpleGrantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
- getAuthoriteies(): User가 가진 권한을 반환하는 역할을 합니다. Security에서 권한을 확인할 때 사용됩니다.
- Security에서 UserDetails를 활용하여 인증을 진행하므로 상속받습니다.
2. Role
@RequiredArgsConstructor
public enum Role {
ROLE_USER("유저"),
ROLE_GUEST("게스트");
private final String key;
}
3. Character
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Character {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "character_name")
private String name;
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY)
private User user;
private void setUser(User user){
this.user=user;
}
private void addCharacter(){
user.getCharacters().add(this);
}
public void update(String name){
this.name=name;
}
public static Character createCharacter(String name, User user){
Character character = new Character();
character.setUser(user);
character.addCharacter();
character.name=name;
return character;
}
}
4. UserRepository
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from User u where u.email= :email")
Optional<User> findByEmail(@Param("email")String email);
}
5. CharacterRepository
public interface CharacterRepository extends JpaRepository<Character, Long> {
}
6. UserServiceImpl (예제의 간단화를 위해 CharacterService 로직을 UserServiceImpl에 넣었습니다.)
@Service
@RequiredArgsConstructor
@Transactional
public class UserServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final CharacterRepository characterRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email).orElseThrow(
() -> {
throw new UserNotFound("없는 유저입니다.");
}
);
}
public User signup(String email, String password){
userRepository.findByEmail(email).ifPresent(
a-> {
throw new IllegalStateException("이미 가입된 이메일입니다.");
}
);
User user = (User)User.builder()
.email(email)
.password(passwordEncoder.encode(password))
.build();
return userRepository.save(user);
}
public Character createCharacter(String name, String email){
User user = (User) loadUserByUsername(email);
Character character = Character.createCharacter(name, user);
return characterRepository.save(character);
}
public void updateCharacter(Long id, String name){
Character character = characterRepository.findById(id).get();
character.update(name);
}
}
- Security에서는 UserDetailsService를 이용하여 인증을 진행하므로 상속받아 loadUserByUsername을 구현합니다. 여기서 username은 저희가 만든 User에서 email이라고 생각하면 되겠습니다.
7. SecuritybasicApplication 수정
@SpringBootApplication
public class SecuritybasicApplication {
public static void main(String[] args) {
SpringApplication.run(SecuritybasicApplication.class, args);
}
// 비밀번호를 암호화 하는 곳에 쓰일 객체를 bean로 등록합니다.
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
8. CustomAuthenticationFilter
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(request.getParameter("username"), request.getParameter("password"));
setDetails(request,token);
return this.getAuthenticationManager().authenticate(token);
}
}
- UsernamePasswordAuthenticationFilter를 상속받아 구현합니다. HttpServletRequest로 들어온 파라미터를 기반으로 UsernamePasswordAuthenticationToken을 만듭니다.
9. CustomAuthenticationProvider
@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final PasswordEncoder passwordEncoder;
private final UserServiceImpl userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String email = (String) token.getName();
String password = (String) token.getCredentials();
User user = (User) userService.loadUserByUsername(email);
if(!passwordEncoder.matches(password,user.getPassword())){
throw new BadCredentialsException("비밀 번호가 틀렸습니다.");
}
return new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
- AuthenticationProvider를 상속받아 구현합니다. 해당 객체에서 User인증을 진행합니다. 여기서 인증이란 DB에 저장된 email과 password과 로그인 시도를 한 것을 비교합니다.
10. CustomLoginSuccessHandler
public class CustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(authentication);
response.sendRedirect("/success");
}
}
- SavedRequestAwareAuthenticationSuccessHandler를 상속받아 인증을 성공했다면 Authentication에 인증된 결과를 저장합니다. 그 후 /success로 redirect를 진행합니다.
11. CustomAccessDeniedHandler
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/notAccess");
}
}
- 사용하는 접근자의 권한이 접근하고자 하는 자원이 요구하는 권한과 다를 때 /notAccess로 보내 자원에게 접근하는 것을 차단합니다.
12. SecurityConfig (주석으로 설명을 넣어놓겠습니다.)
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomAuthenticationProvider customAuthenticationProvider;
// 정적 컨텐츠가 있다면 작성합니다.
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
/* ex
web.ignoring().antMatchers("/~~"); 경로를 적어줍니다.
*/
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/login","/signup","/","/notAccess").permitAll() //해당 url에는 모두 접근가능
.antMatchers("/user","/character").hasRole("USER") // ROLE_USER를 가진 사용자만 접근 가능
.antMatchers("/guest","/success").authenticated() // 어떠한 권한을 한 가지만 가지고 있다면 접근 가능
.and()
.exceptionHandling() // 예외를 다루겠다는 뜻
.accessDeniedHandler(new CustomAccessDeniedHandler()) //위에서 만든 handler를 넣어줍니다.
.authenticationEntryPoint((request, response, authException) -> response.sendRedirect("/notAccess"))// 람다로 표현한 것입니다. 이것은 권한을 아무것도 가지지 않은채
.and() // 접근했을 경우 /notAccess로 접근을 차단합니다.
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.and()
.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);// 만든 Filter를 넣어줍니다.
}
// AuthenticationManager, CustomLoginSuccessHandler를 주입하고 bean으로 등록해줍니다.
@Bean
public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
customAuthenticationFilter.setAuthenticationSuccessHandler(new CustomLoginSuccessHandler());
return customAuthenticationFilter;
}
// 저희가 만든 Provider를 사용하도록 넣어줍니다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
}
13. UserController
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserServiceImpl userService;
@GetMapping("/")
public String index(){
return "Welcome Home";
}
@PostMapping("/signup")
public String signup(@RequestBody UserDto userDto){
User success = userService.signup(userDto.getEmail(), userDto.getPassword());
return "signup Success";
}
@PostMapping("/character")
public String create(@RequestParam(name = "email") String email,
@RequestParam(name = "name") String name){
Character character = userService.createCharacter(name, email);
return "ok";
}
@PatchMapping("/character")
public String update(@RequestParam(name = "id") Long id,
@RequestParam(name = "name") String name){
userService.updateCharacter(id,name);
return "ok";
}
@GetMapping("/user")
public String user(){
return "user";
}
@GetMapping("/notAccess")
public String access(){
return "notAccess";
}
@GetMapping("/guest")
public String guest(){
return "guest";
}
@GetMapping("/success")
public String success(){
return "success";
}
}
- Sesstion을 따로 사용하지 않았기 때문에 @RequestParam을 이용해서 사용자의 email을 받아야 합니다.
14. Postman test (1)
- 로그인을 하지 않은 채 authenticated()와 USER권한이 있어야 되는 곳을 갔을 때 /notAccess로 이동하고 결과를 받는 것을 볼 수 있습니다.
- 회원가입과 로그인을 했을 때 /success로 redirect 한 것을 볼 수 있습니다. (login 할 때는 parameter로 email과 password를 넘겨줘야 합니다.)
- User를 생성할 때 권한을 ROLE_USER로 만들었으므로 /user에 접근할 수 있는 것을 볼 수 있습니다.
15. Postman test (2)
- User를 생성할 때 권한을 ROLE_USER를 줬던 것을 ROLE_GUEST로 변경해보고 위에서 했던 것을 그대로 진행해보면 권한이 GUEST이므로 /user에서는 권한이 옳지 않아 접근이 불가능한 것을 알 수 있습니다.
- GUEST의 권한을 가진 유저가 로그인을 했습니다.
- 권한이 USER가 아닌 GUEST이므로 접근이 거부된 것을 볼 수 있습니다.
지금까지 권한에 대한 Security는 모두 만들었습니다. 하지만 여기서 문제 접은 인가입니다. 현재까지 만든 것은 User의 권한만 있다면 다른 사람의 Character를 수정할 수 도 있습니다. 예시를 보여드리겠습니다. User를 생성할 때 권한은 다시 ROLE_USER로 변경합니다.
16. Postman test (3)
- asdf의 user와 qwer의 유저가 있습니다. asdf의 유저가 Character aa를 만들었습니다. 그렇다면 asdf유저 만이 aa에 접근할 수 있을까요?? 정답은 아닙니다. 현재 인증을 진행하고 권한을 확인하는 로직만 만들었습니다. 밑에 예시를 보여드리겠습니다.
- asdf User 회원가입
- qwer User 회원가입
- asdf User 로그인
- asdf User가 캐릭터의 이름이 aa를 생성합니다.
- qwer User가 로그인합니다.
- qwer User가 asdf User가 만든 캐릭터의 이름을 수정해버렸습니다.
위의 문제점을 해결하기 위해서 인가에 대한 설정이 따로 필요합니다. Filter부분에 로직을 좀 더 추가할 수도 있지만 저희는 Interceptor를 만들어서 해당 부분을 처리하겠습니다. 감사합니다.
'SpringBoot > spring security' 카테고리의 다른 글
Springboot [스프링부트] Spring Sercurity Basic(4) (0) | 2022.02.12 |
---|---|
SpringBoot [스프링부트] Spring Security Baisc (3) (0) | 2022.01.27 |
SpringBoot [스프링 부트] Spring Security Basic (1) 프로젝트 생성 (0) | 2022.01.25 |
SpringBoot [스프링부트] Spring Security Basic (0) (0) | 2022.01.25 |
SpringBoot [스프링부트] Spring Security JWT 만들기 (1) 끝 (0) | 2022.01.22 |
댓글
이 글 공유하기
다른 글
-
Springboot [스프링부트] Spring Sercurity Basic(4)
Springboot [스프링부트] Spring Sercurity Basic(4)
2022.02.12 -
SpringBoot [스프링부트] Spring Security Baisc (3)
SpringBoot [스프링부트] Spring Security Baisc (3)
2022.01.27 -
SpringBoot [스프링 부트] Spring Security Basic (1) 프로젝트 생성
SpringBoot [스프링 부트] Spring Security Basic (1) 프로젝트 생성
2022.01.25 -
SpringBoot [스프링부트] Spring Security Basic (0)
SpringBoot [스프링부트] Spring Security Basic (0)
2022.01.25