프로젝트 개요
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
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);
}
@Override
protected void configure (HttpSecurity http) throws Exception {
http.cors().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/login" ,"/signup" ,"/" ,"/notAccess" ).permitAll()
.antMatchers("/user" ,"/character" ).hasRole("USER" )
.antMatchers("/guest" ,"/success" ).authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint((request, response, authException) -> response.sendRedirect("/notAccess" ))
.and()
.logout()
.logoutSuccessUrl("/" )
.invalidateHttpSession(true )
.and()
.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public CustomAuthenticationFilter customAuthenticationFilter () throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
customAuthenticationFilter.setAuthenticationSuccessHandler(new CustomLoginSuccessHandler());
return customAuthenticationFilter;
}
@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 회원가입
asdf User가 캐릭터의 이름이 aa를 생성합니다.
qwer User가 asdf User가 만든 캐릭터의 이름을 수정해버렸습니다.
위의 문제점을 해결하기 위해서 인가에 대한 설정이 따로 필요합니다. Filter부분에 로직을 좀 더 추가할 수도 있지만 저희는 Interceptor를 만들어서 해당 부분을 처리하겠습니다. 감사합니다.
댓글을 사용할 수 없습니다.