본문 바로가기

boot_JPA 수업 정리

Boot_JPA 4일차

유저권한 기능 구현 ( 시큐리티 ) 

추가하는 디펜던시 

	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

 

 

SecurityConfig 생성/ 설정

 

- 인코딩

    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

 

- 권한별 페이지 설정, 로그인, 로그아웃 설정

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/js/**","/dist/**","/upload/**","/index","/user/join","/user/login","/board/list","board/detail","/comment/list/**").permitAll()
                        .requestMatchers("/user/list").hasAnyRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(login -> login
                                .usernameParameter("email")
                                .passwordParameter("pwd")
                                .loginPage("/user/login")
                                .successHandler(authenticationSuccessHandler())
                                .failureHandler(authenticationFailureHandler())
                                .permitAll()
                        )
                .logout(logout -> logout
                        .logoutUrl("/user/logout")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .logoutSuccessUrl("/")
                )
                .build();
    }

 

-  UserDetailsService ( DB에서 아이디가 일치하는 정보를 가져오게하는 역할 )

    @Bean
    UserDetailsService customUserDetailService(){
        return new CustomUserService();
    }

 

- AuthenticationManager(생성필요없음 알아서 구현돼있음)

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

 

- 로그인 성공 핸들러

    @Bean
    AuthenticationSuccessHandler authenticationSuccessHandler(){
        return new LoginSuccessHandler();
    }

 

- 로그인 실패 핸들러

    @Bean
    AuthenticationFailureHandler authenticationFailureHandler(){
        return new LoginFailureHandler();
    }

 

  User, AuthUser  Entity, DTO 각자 생성해주고 서비스부터 레파지토리까지 쭉 연결생성

 

●  헤더에 회원가입, 로그인 추가

          <li class="nav-item">
            <a class="nav-link" href="/user/join">SignUp</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/user/login">Login</a>
          </li>

 

●  회원가입구현

 

- 화면구성

  <div class="container-md">
    <h1>User Join Page</h1>
    <hr>
    <form action="/user/join" method="post">
      <div class="mb-3">
        <label for="e" class="form-label">email</label>
        <input type="text"
               class="form-control" id="e" name="email" placeholder="email...">
      </div>
      <div class="mb-3">
        <label for="p" class="form-label">password</label>
        <input type="text"
               class="form-control" id="p" name="pwd" placeholder="password...">
      </div>
      <div class="mb-3">
        <label for="n" class="form-label">nickName</label>
        <input type="text"
               class="form-control" id="n" name="nickName" placeholder="nickName...">
      </div>
      <button type="submit" class="btn btn-primary">JOIN</button>
    </form>
  </div>

 

- 컨트롤러에 GetMapping으로 회원가입 화면으로 이동할 수있게 해주고 인코딩을위해 생성자 선언.

PostMapping으로 화면에서 userDTO정보를 가져와서 비밀번호를 암호화해준다음 서비스에 전달 

email을 리턴받아와 null이아니면 성공해서 index 화면으로 이동.

private final PasswordEncoder passwordEncoder;
    @GetMapping("/join")
    public void join() {

    }
    @PostMapping("/join")
    public String join(UserDTO userDTO){
        log.info(" userDTO >>> {} ", userDTO);

        userDTO.setPwd(passwordEncoder.encode(userDTO.getPwd()));

        String email = userService.join(userDTO);

        log.info("email >> {} " , email);

        return (email == null) ? "/user/join" : "/index";
    }

 

- 서비스에서 컨버트 구현 

    default User convertUserDTOToUser(UserDTO userDTO){
        return User.builder()
                .email(userDTO.getEmail())
                .pwd(userDTO.getPwd())
                .nickName(userDTO.getNickName())
                .lastLogin(userDTO.getLastLogin())
                .build();
    }

    default AuthUser converAuthUserDTOToAuthUser(UserDTO userDTO){
        return AuthUser.builder()
                .email(userDTO.getEmail())
                .auth("USER_ROLE")
                .build();
    }

    default AuthUserDTO convertAuthUserToAuthUserDTO(AuthUser authUser){
        return AuthUserDTO.builder()
                .email(authUser.getEmail())
                .auth(authUser.getAuth())
                .build();
    }

    default UserDTO convertUserToUserDTO(User user, List<AuthUserDTO> authUserDTOList){
        return  UserDTO.builder()
                .email((user.getEmail()))
                .pwd((user.getPwd()))
                .nickName(user.getNickName())
                .lastLogin((user.getLastLogin()))
                .regAt((user.getRegAt()))
                .modAt((user.getModAt()))
                .authList(authUserDTOList)
                .build();
    }

 

- ServiceImpl에서 구현한 컨버트를 가지고 받아온 userDTO를 save로 insert해주고 이메일을 리턴

email 빈값이 아니라면 권한도 insert해주도록함.

   @Override
    public String join(UserDTO userDTO) {

        String email = userRepository.save(convertUserDTOToUser(userDTO)).getEmail();
        if( email != null ){
            authUserRepository.save(converAuthUserDTOToAuthUser(userDTO));
        }
        return email;
    }

 

● 로그인 구현

 

- 로그인 화면 구성

  <div class="container-md">
    <h1>User Join Page</h1>
    <hr>
    <form action="/user/login" method="post">
      <div class="mb-3">
        <label for="e" class="form-label">email</label>
        <input type="text"
               class="form-control" id="e" name="email" placeholder="email...">
      </div>
      <div class="mb-3">
        <label for="p" class="form-label">password</label>
        <input type="text"
               class="form-control" id="p" name="pwd" placeholder="password...">
      </div>
      <button type="submit" class="btn btn-primary">login</button>
    </form>
  </div>

 

- UserDetailsService 구현체 구현  ( username을 받아서 DB에 username에맞는 유저와 권한을 가져옴 )

@Slf4j
public class CustomUserService implements UserDetailsService {

    @Autowired
    public UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username 이 DB에 있는지 확인 user 테이블에 email 객체를 가져오기
        UserDTO userDTO = userService.selectEmail(username);
        log.info(">>> login User >> {} ", userDTO);
        if(userDTO == null){
            throw new UsernameNotFoundException(username);
        }
        return new AuthMember(userDTO);
    }
}

 

- AuthMember 구현

@Getter
@Slf4j
public class AuthMember extends User {

    private UserDTO userDTO;

    public AuthMember(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public AuthMember(UserDTO userDTO) {
        super(userDTO.getEmail(),userDTO.getPwd(),
                userDTO.getAuthList().stream()
                        .map(auth -> new SimpleGrantedAuthority(auth.getAuth()))
                        .collect(Collectors.toList()));
        this.userDTO = userDTO;
    }
}

 

- 로그인 성공 핸들러 구현

@Component
@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Getter
    @Setter
    private String authUrl;

    @Getter
    @Setter
    private String authEmail;

    // 성공 후 가야하는 경로 설정 (객체)
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    // request 객체의 저장공간 : 직전 갔던 경로 저장
    private RequestCache requestCache = new HttpSessionRequestCache();

    @Autowired
    public UserService userService;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        setAuthEmail(authentication.getName());
        setAuthUrl("/board/list");

        boolean isOk = userService.updateLastLogin(getAuthEmail());

        HttpSession ses = request.getSession();
        if(!isOk || ses == null){
            return;
        } else{
            ses.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
        }
        SavedRequest savedRequest = requestCache.getRequest(request,response);
        log.info(">>> saveRequest >> {}" , savedRequest);
        redirectStrategy.sendRedirect(request,response,
                (savedRequest != null ? savedRequest.getRedirectUrl() : getAuthUrl()));


    }
}

 

로그인이 성공하면 이동하는 경로설정, 로그인 전에 이동했던 경로가있다면 로그인이 성공 했을 때 그 경로로 이동하도록 설정.

 

- 로그인 실패 핸들러 구현 

@Slf4j
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errorMessage;
        if(exception instanceof BadCredentialsException){
            errorMessage = "아이디 또는 비밀번호가 맞지않습니다. 다시 확인해주세요.";
        } else if (exception instanceof InternalAuthenticationServiceException){
            errorMessage = "내부 시스템 문제로 로그인 처리를 할 수 없습니다. 관리자에게 문의해주세요.";
        } else if (exception instanceof UsernameNotFoundException){
            errorMessage = "존재하지않는 아이디입니다.";
        } else if (exception instanceof AuthenticationCredentialsNotFoundException){
            errorMessage = "인증요청이 거부됐습니다. 관리자에게 문의해주세요.";
        } else {
            errorMessage = "관리자에게 문의해주세요.";
        }

        errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
        setDefaultFailureUrl("/user/login?error=true&exception="+errorMessage);
        super.onAuthenticationFailure(request, response, exception);

    }
}

 

상황에맞는 에러메세지를 띄울 수 있도록 구현. 인코딩해주어야함.

 

- 컨트롤러에서 에러존재여부와 에러메세지를 받아서 화면에 전달할 수 있도록함.

    @GetMapping("/login")
    public void login(@RequestParam(value = "error", required = false) String error,
                      @RequestParam(value = "exception", required = false) String exception,
                      Model model) {
        /* 에러와 예외값을 담아 화면으로 전달*/
        model.addAttribute("error",error);
        model.addAttribute("exception", exception);
    }

 

ADMIN 계정에 유저리스트를 볼 수 있도록 구현

 

- 앞으로 권한을 다루는 내용에는 아래 코드가 삽입되어 존재

xmlns:sec="http://www.thymeleaf.org/extras/spring-security"

 

-  1111 계정에만 어드민 권한을 따로 부여

insert into auth_user (email, auth) values('1111@naver.com','ROLE_ADMIN');

 

- header에 ADMIN 권한에 유저리스트를 볼 수 있도록 추가하고 로그인 , 로그아웃, 글쓰기 등 권한이 있을때 없을때 띄우는 메뉴가 다르도록 수정 / 추가

       <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/board/register">BoardRegister</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/board/list">BoardList</a>
          </li>
          <li class="nav-item" sec:authorize="isAnonymous()">
            <a class="nav-link" href="/user/join">SignUp</a>
          </li>
          <li class="nav-item" sec:authorize="isAnonymous()">
            <a class="nav-link" href="/user/login">Login</a>
          </li>
          <li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/user/modify">[[${#authentication.getPrincipal().userDTO.nickName}]]</a>
          </li>
          <li class="nav-item" sec:authorize="hasRole('ROLE_ADMIN')">
            <a  class="nav-link text-danger" href="/user/list">UserList</a>
          </li>
          <li class="nav-item" sec:authorize="isAuthenticated()">
            <a class="nav-link" href="/user/logout">Logout</a>
          </li>
        </ul>

 

- 유저 리스트 화면 구성 

  <div class="row row-cols-1 row-cols-md-3 g-4">
    <th:block th:each="uvo:${userList}">
      <div class="col">
        <div class="card">
          <img th:src="@{/image/사람2.png}" class="card-img-top" alt="...">
          <div class="card-body">
            <h4 class="card-title">[[${uvo.email }]]</h4>
            <span style="margin-right : 3px"  th:each="avo:${uvo.authList }" class="badge text-bg-info">[[${avo.auth }]]</span>
            <br><br> <p class="card-text">가입일 : [[${uvo.regAt }]]</p>
            <p>마지막 로그인 : [[${uvo.lastLogin }]]</p>

          </div>
        </div>
      </div>
    </th:block>
  </div>

 

- 컨트롤러에서 서비스에 연결해서 가져와서 화면에 모델로 뿌릴건데 findAll 할거라 파라미터가 필요하지않음.

   @GetMapping("/list")
    public void list(Model model){

        List<UserDTO> userList = userService.getList();

        model.addAttribute("userList", userList);
    }

 

-  유저객체의 리스트를 전부 찾아와서 for문을 돌려 user객체의 Auth객체 리스트를 가져올 수 있도록함.

AuthUser의 리스트를 AuthUserDTO로 컨버트해서 리스트로 받고 User객체의 리스트를 UserDTO 객체의 리스트로 컨버트해서 리턴할 수 있도록함. 

   @Override
    public List<UserDTO> getList() {
        List<User> userList = userRepository.findAll();
        for( User user : userList){
            if(user.getEmail() != null){
              List<AuthUser> authUserList = authUserRepository.findByEmail(user.getEmail());
              List<AuthUserDTO> authUserDTOList = authUserList.stream().map(a -> convertAuthUserToAuthUserDTO(a)).toList();
              List<UserDTO> userDTOList = userList.stream().map(u -> convertUserToUserDTO(u,authUserDTOList)).toList();
              return userDTOList;
            }
        }
        return null;
    }

 

● 본인 정보 수정기능 구현

 

- 정보 수정 화면 구현

  <th:block th:with="uvo=${#authentication.getPrincipal().userDTO}" >
    <form action="/user/modify" method="post" >
      <input type="hidden" name="email" th:value="${uvo.email }">
      <div class="card mb-3" style="max-width: 540px; margin: 0 auto;">
        <div class="row g-0">
          <div class="col-md-4">
            <img style="margin-top : 35px; " src="/image/사람2.png" class="img-fluid rounded-start"
                 alt="...">
          </div>
          <div class="col-md-8">
            <div class="card-body">
              <h5 class="card-title">
                <input style="margin-bottom : 3px;" type="text" id="e" th:value="${uvo.email }" readonly="readonly">
                <input style="margin-bottom : 3px;" type="text" id="n" name="nickName" th:value="${uvo.nickName }">
                <input type="text" id="p" name="pwd" placeholder="password...">
              </h5>
              <span style="margin-right : 3px"  th:each="avo:${uvo.authList }" class="badge text-bg-info">[[${avo.auth }]]</span>
              <p class="card-text">가입일 : [[${uvo.regAt }]]</p>
              <p class="card-text">
                <small class="text-body-secondary">마지막 로그인 :
                  [[${uvo.lastLogin }]]</small>
              </p>
              <button type="submit" class = "btn btn-primary btn-sm">modify</button>
              <a href="/user/delete"><button type="button" class = "btn btn-danger btn-sm">delete</button></a>
            </div>
          </div>
        </div>
      </div>
    </form>
  </th:block>

 

- 컨트롤러에서 GetMapping으로 화면으로 갈 수 있도록하고 PostMapping으로 userDTO정보를 가져와서 

비밀번호를 입력하지않았을 때와, 비밀번호까지 수정했을 때 두가지로 나눠서 서비스에 연결해 update 진행

    @GetMapping("/modify")
    public void modify() {}

    @PostMapping("/modify")
    public String modfiy(UserDTO userDTO){
        log.info(">>> modify UserDTO >> {}" ,userDTO);

        String email;
        if(userDTO.getPwd() == null || userDTO.getPwd().isEmpty()) {
            // 비번 없이 업데이트 진행
            email = userService.modifyNoPwd(userDTO);
        }else {
            // 비번 암호화 후 업데이트 진행
            userDTO.setPwd(passwordEncoder.encode(userDTO.getPwd()));
            email = userService.modify(userDTO);
        }

        return "redirect:/user/logout";
    }

 

- 비밀번호가 없을 때 User객체를 optional로 가져와서 optional이 존재한다면  user객체에 가져온 userDTO의 닉네임을 set해줄 수 있도록하고 save로 db에 insert해준다음 이메일리턴

    @Override
    public String modifyNoPwd(UserDTO userDTO) {
        log.info(">>> userDTO Email >> {}" , userDTO.getEmail());
        Optional<User> optional = userRepository.findById(userDTO.getEmail());
        log.info( " >>> optional >> {} ", optional.isPresent());
        if(optional.isPresent()){
            User user = optional.get();
            user.setNickName(userDTO.getNickName());
            return userRepository.save(user).getEmail();
        }
        return null;
    }

 

- 비밀번호가 있을때는 비밀번호 set 까지 추가해주면 끝.

    @Override
    public String modify(UserDTO userDTO) {
        Optional<User> optional = userRepository.findById(userDTO.getEmail());
        if(optional.isPresent()){
            User user = optional.get();
            user.setNickName(userDTO.getNickName());
            user.setPwd(userDTO.getPwd());
            return userRepository.save(user).getEmail();
        }
        return null;
    }

 

  삭제구현

 

- Principal로 유저정보를 가져와서 email을 따로 저장해주고 email을 받아서 서비스에서 delete를 진행할 수 있도록함.

    @GetMapping("/delete")
    public String delete(Principal principal){
        String email = principal.getName();

        userService.delete(email);

        return "redirect:/user/logout";
    }

 

- optinal로 User 객체를 가져와서 AuthUser의 리스트를 findByEmail로 가져올 수 있도록하고 

for문을 돌려서 authUser 객체의 id를 가져와서 권한을 delete 할 수 있도록함.

user는 그냥 email을 받아서 delete 하면 끝.

   @Override
    public void delete(String email) {
        Optional<User> optional = userRepository.findById(email);
        if(optional.isPresent()){
            User user = optional.get();
            List<AuthUser> authUserList = authUserRepository.findByEmail(user.getEmail());
            for(AuthUser authUser : authUserList){
                authUserRepository.deleteById(authUser.getId());
            }
        }
        userRepository.deleteById(email);
    }

'boot_JPA 수업 정리' 카테고리의 다른 글

Boot_JPA 5일차  (0) 2024.11.26
Boot_JPA 3일차  (0) 2024.11.22
Boot_JPA 2일차  (3) 2024.11.21
Boot_JPA 1일  (1) 2024.11.20