유저권한 기능 구현 ( 시큐리티 )
추가하는 디펜던시
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 |