반응형

*현재 validation 미통과시 js로 오류메시지를 alert하도록 설정했는데
추후엔 오류메시지를 타임리프를 활용하여 변경할 예정이다.

SignupDto
- 회원가입 관련 DTO로, 회원가입 폼에서 전달한 데이터를 담는 역할을한다.
- @Valid의 대상이된다.
- toEntity() 메소드는DTO로 받은 회원데이터를 User객체로 변환해준다.

@Data
public class SignupDto {
    @Size(min=2, max = 20)
    @NotBlank
    private  String username;

    @NotBlank
    private  String password;

    @NotBlank
    private  String email;

    @NotBlank
    private  String name;

    public User toEntity(){
        return User.builder()
                .username(username)
                .password(password)
                .email(email)
                .name(name)
                .build();
    }
}

AuthController
- 사실 프론트 단에서 유효성 검사하는것으로도 끝낼 수 있지만 데이터를 프론트가 아닌 다른 방법
(ex postman)으로 전송할 수 있으니까 백엔드 단도 구성했다.
- SignupDto에서의 유효성 검사가 하나라도 실패하면 실패한 것들이 AuthController의 signup 메소드의 bindingResult에 담긴다.
- bindingResult에 에러가 하나라도 있으면 errorMap 이라는 이름에 Map에다 에러 정보를 담는다.
- 다 담기면 CustomValidationException을 throw한다. 이때 메시지("유효성 검사 실패함")와 errorMap을 파라미터로 넘긴다.
- 오류가 없다면 회원가입 진행 후 "auth/signin"페이질 이동

@PostMapping("/auth/signup")
public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) {

    //프론트 단에서 유효성 검사해도 postman 전송할 수 있으니까
    if (bindingResult.hasErrors()) {
        Map<String, String> errorMap = new HashMap<>();

        for (FieldError error : bindingResult.getFieldErrors()) {
            errorMap.put(error.getField(), error.getDefaultMessage());
        }
        throw new CustomValidationException("유효성 검사 실패함", errorMap);
    } else {
        //User < - SingupDto
        User user = signupDto.toEntity();
        User userEntity = authService.register(user);
        return "auth/signin";
    }
}

 

CustomValidationException.class
- CustomValidationException 은 RuntimeException을 상속받는다.
- 생성자는 String 타입의 메시지와 Map 타입의 errorMap을 매개변수로 갖는다.
- getErrorMap() 메소드는 errorMap을 리턴한다.

public class CustomValidationException extends RuntimeException{

    //객체를 구분할때
    private static final long serialVersionUID = 1L;

    private Map<String, String> errorMap;

    public CustomValidationException(String message, Map<String, String> errorMap){
        super(message);
        this.errorMap = errorMap;
    }

    public Map<String, String > getErrorMap(){
        return errorMap;
    }
}

 

ControllerExceptionHandler.class
- @ControllerAdvice 는 모든 Exception들을 낚아챈다.
- @ExceptionHandler(CustomValidationException.class)를 선언해줌으로써 모든 CustomValidationException을 이 함수가 가로챈다.
- 리턴값은 back이라는 정적메소드에 파라미터로 errorMap을 String으로 변환시켜 전달해준다.

@RestController
@ControllerAdvice
public class ControllerExceptionHandler {

    @ExceptionHandler(CustomValidationException.class)
    public String validationException(CustomValidationException e){
        return Script.back(e.getErrorMap().toString());
    }
}


Script.java
- validationException의 리턴값을 설정해준다.
- 자바스크립트 코드를 문자열화해서 리턴해준다. 

public class Script {

    public static String back(String msg){
        StringBuffer sb = new StringBuffer();
        sb.append("<script>");
        sb.append("alert('"+msg+"');");
        sb.append("history.back();");
        sb.append("</script>");
        return sb.toString();
    }
}
반응형

다이어그램에서 보듯 한 유저는 여러 board를 가질 수 있고 여러 board는 한 user에 속한다.

외래키 제약조건을 건다 - user_id 컬럼을 사용자 테이블의 id값과 연결되도록 저장

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class Board {
   ...

    @ManyToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user;

}

게시글 입장에선 many to one 관계
- @JoinColumn: 어떤 컬럼과 user 테이블이 연결될지를 설정한다. 
- referencedColumnName = "id" : user 테이블의 id와 조인된다. 생략가능

== 글 작성시 사용자 정보도 넣기 ==

BoardController.java

   @PostMapping("/form")
    public String postForm(@Valid Board board, BindingResult bindingResult, Authentication authentication){
        boardValidator.validate(board, bindingResult);
        if(bindingResult.hasErrors()) {
            return "board/form";
        }

        String username = authentication.getName();
        boardService.save(username, board);
        return "redirect:/board/list";
    }

-Authentication 을 파라미터로 선언해주면 인증정보가 알아서 담긴다. 
- getName() 으로 구한 유저 이름을 board 정보와 함께 boardService에 전달

@Service
public class BoardService {

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private UserRepository userRepository;

    public Board save(String username, Board board){
        //1. 넘겨받은 username을 가지고 user의 id 조회

        User user = userRepository.findByUsername(username);
        board.setUser(user);

        return boardRepository.save(board);

    }
}

BoardService에서는 컨트롤러에서 넘겨받은 username을 가지고 user 정보를 조회한다.
-> 그러기위해선  UserRepository 에 관련 메소드를 만들어줘야한다. 

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username); //컬럼이름에 일치하는 사용자 데이터를 가져온다.
}

 

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class User {

    ...
    @OneToMany(mappedBy = "user")
    private List<Board> boards = new ArrayList<>();

}

- mappedBy 로 Board 클래스의 user를 적어준다.
- onetomany, manytoone 관계에서 보통 many쪽  에서 소유하는쪽을 @JoinColumn을 써서 적어준다

반응형

Spring Security 사용하기 위해 다음의 의존성 추가가 필요하다. (gradle 기준)

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'

 

이후 WebSecurityConfigurerAdapter을 상속받은 WebSecurityConfig클래스를 만들어준다.
*@Configuration 을 선언한 클래스에선 @Bean을 관리할 수 있다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //application.properties의 dataSource가져옴
    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/", "/account/register", "/css/**").permitAll() //누구나 접근할수 있는 url
                    .anyRequest().authenticated() //어떤 요청이라도 authenticated 돼야한다.
                    .and()
                .formLogin() //authenticated안된 상태에서 다른 페이지 가려하면
                    .loginPage("/account/login") //login페이지로 redirect된다.
                    .permitAll() //login 페이지는 누구라도 접근가능
                    .and()
                .logout()
                    .permitAll();
    }

}

  

테이블 ERD는 다음과 같다.
user 테이블에는 id, username, password, enabled(활성화여부)
role 테이블은 id, name(권한명)
user와 role은 many-to-many 관계이므로 
user_role 테이블을 만들어서 관계형 테이블을 만든다.

user_id 와 role_id는 각각 pk이다.
외래키 설정을 통해 무결성을 보장하도록 한다. 

 

JDBC Authentication 설정을 위해
https://www.baeldung.com/spring-security-jdbc-authentication 을 참고했다.
-PasswordEncoder: 추후 회원가입시 암호화시켜주는 bean

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //application.properties의 dataSource가져옴
    @Autowired
    private DataSource dataSource;

  ...

    //테이블에 쿼리 날림
    //스프링 datasource 가져와서 알아서 내부에서 인증처리
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(passwordEncoder()) //알아서 비번 암호화
                .usersByUsernameQuery ("select username,password,enabled "  //인증처리
                        + "from user "
                        + "where username = ?")
                .authoritiesByUsernameQuery("select u.username, r.name " //권한처리
                        + "from user_role ur inner join user u on ur.user_id =u.id "
                        +  "inner join role r on ur.role_id = r.id "
                        + "where u.username = ?");
    }
    
    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

- authoritiesByUsernameQuery: 권한정보 가져오는 쿼리, user_role에는 user와 role에 대한 정보가 없다. 따라서 user테이블과 role 테이블을 조인해서 정보를 가져오도록 한다.  

select u.username, r.name 
from user_role ur 
inner join user u on ur.user_id =u.id 
inner join role r on ur.role_id = r.id 
where u.username = ?

 

==로그인 및 회원가입 컨트롤러 설정==

@Controller
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public String login(){
        return "account/login";
    }

    @GetMapping("/register")
    public String register(){
        return "account/register";
    }

    @PostMapping("/register")
    public String register(User user){ //modelattribute 생략해도 괜춘
        userService.save(user);
        return "redirect:/";
    }
}

==로그인시 에러처리==

login.html

<form class="form-signin" th:action="@{/account/login}" method="post">
   
   <h1 class="h3 mb-3 font-weight-normal"><a th:href="@{/}">Please sign in</a></h1>
        
        <div th:if="${param.error}" class="alert alert-danger" role="alert">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}" class="alert alert-primary" role="alert">
            You have been logged out.
        </div>
        
    <label for="username" class="sr-only">Email address</label>
    <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
    
    <label for="inputPassword" class="sr-only" name="password">Password</label>
    <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
   
    ...
</form>

- form 태그 안에서 login으로 post 요청 보내는데, 만약 에러가 발생하면(파라미터에 error발생하면) 해당 에러 메시지가 나온다(로그아웃 시에도 동일) 

 

==User, Role 모델을 만들어 many-to-many 관계설정-

참조: https://www.baeldung.com/jpa-many-to-many

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //Autoincrement 설정, IDENTITY가 많이 사용
    private Long id;

    private String username;
    private String password;
    private Boolean enabled;

    //user에 해당하는 권한이 알아서 조회돼서, roles에 담긴다.
    @ManyToMany
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name="user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roles = new ArrayList<>();
}

 

@Entity //db연동을 위한 모델클래스임을 명시
@Data
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //Autoincrement 설정, IDENTITY가 많이 사용
    private Long id;

    private String name; //권한명

    @ManyToMany(mappedBy = "roles") //mappedBy: User클래스에 있는 컬럼명이 된다.
    private List<User> users;

}

 

==UserRepository 생성==

public interface UserRepository extends JpaRepository<User, Long> {
}

==UserService 생성==

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    //유저 저장
    public User save(User user){
        // 1.비밀번호 인코딩
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);

        // 2.회원 활성화 여부 - 기본적으로 enabled로 설정
        user.setEnabled(true);

        // 3. role정보 추가
        Role role = new Role();
        role.setId(1L);
        user.getRoles().add(role);
        return userRepository.save(user);
    }
}

- DI를 통해 UserRepository 객체를 받아온다.
- WebSecurityConfig에 Bean으로 있는 PasswordEncoder를 DI로 가져와서 사용자가 전달한 비밀번호 인코딩한다.

== 로그인 여부에 따른 메뉴 구성 ==
https://www.thymeleaf.org/doc/articles/springsecurity.html
참고

common.html

<a class="btn btn-secondary my-2 mr-2 my-sm-0" th:href="@{/account/login}"
   sec:authorize="!isAuthenticated()">로그인</a>
<a class="btn btn-secondary my-2 my-sm-0" th:href="@{/account/register}"
   sec:authorize="!isAuthenticated()">회원가입</a>

<form class="form-inline my-2 my-lg-0"  th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()">
    <span class="text-white" sec:authentication="name">사용자</span>
    <span class="text-white mx-2" sec:authentication="principal.authorities">권한</span>
    <button class="btn btn-secondary my-2 my-sm-0" type="submit">로그아웃</button>
</form>

sec:authorize="!isAuthenticated() : isAuthenticated()은 로그인 되었을때니까 ! 붙여준다.
sec:authentication="name" : 로그인 한 사용자의 name 조회
sec:authentication="principal.authorities" :로그인 한 사용자의 권한 조회

이 기능을 추가하기 위해선 다음 의존성을 먼저 추가해주고, 

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

네임스페이스도 추가해준다.

반응형

강의를 1개라도 수강하는 학생에게는 수강 강의를 보여주고
수강하는 강의가 없다면 '현재 수강중인 강의가 없습니다.' 문장을 노출시키려한다.

<div class="container">
  <h5 class="card-title">수강중인 강의</h5>
  
    <div th:if="${courses_exist.isEmpty()}">
   	 <h4>현재 수강중인 강의가 없습니다.</h4>
    </div>
    
    <div th:unless="${courses_exist.isEmpty()}" id="my-div" style="position:relative; ">
    	<iframe target="parent"th:src="${login_key}" id="my-iframe"> </iframe>
    </div>

</div>

- 강의 정보(courses_exist)는 list형식으로 되어있기때문에 `.isEmpty()` 메소드를 활용하여 조건문을 분기한다.
- th:if : 타임리프틔 조건문
- th:unless: th:if와 쌍을 이루며, if문의 반대상황에서의 실행을 정의한다. 여기서는 강의정보가 있다면, iframe을 통해 외부 페이지를 가져오도록 했다. 

 

결과:

*수강중인 강의가 없을 때 

*수강중인 강의가 있을 때

반응형

Exception processing template "/main": Error resolving template [/main], template might not exist or might not be accessible by any of the configured Template Resolvers

이란 오류가 발생했다. 
분명 로컬에선 잘 돌아갔는데 호스팅했을땐 안돼서 몇시간을 삽질했는데,

다음의 구글링을 통해 그 원인을 알게됐다.
https://myserena.tistory.com/155

 

template might not exist or might not be accessible by any of the configured Template Resolvers

문제발생상황 회원가입 버튼을 누르면 500에러가 발생. 로그를 확인해보니 Error resolving template “/fragments/footer”, template might not exist or might not be accessible by any of the configured Tem..

myserena.tistory.com

에 따르면 view에 대한 path를 다음과 같이 /main으로 했었기 때문에 //main 으로 되어 path해석이 불가능했던것이다.
그러면 왜 내 local, Intellj에선 실행이 됐지?(헷갈리게 말이다..)

When you’re running in your IDE the resource is available straight off the filesystem and the double slash doesn’t cause a problem. When you’re running from a jar file the resource is nested within that jar and the double slash prevents it from being found.

-> //에대한 처리는 IDE에서는 처리를 해 주고, jar 배포시에는 처리를 못한다.

그래서 return "main" 으로 수정해줬더니 해당 path로 잘 return된것을 확인할 수 있었다.

 

 

+ Recent posts