본문 바로가기

Java/Spring

[Spring] 게시판 만들기 #12

What to do?

인증기능 완성하기 #1

Spring Security


구현내용

 

  • 회원가입 기능 및 화면 구현  

spring security 에서 제공하는 기본 로그인 화면

 

  • 게시글 작성
    • 게시글을 작성자가 현재 로그인한 유저명으로 잘 뜨는걸 확인할 수 있음

게시글 작성 페이지

 

게시판 페이지

 

  • 댓글 작성
    • 댓글작성 시에도 작성자 이름이 잘뜨는걸 확인할 수 있음

게시글 페이지

 

게시글 페이지

 

  • 데이터 베이스
    • MySQL에서 직접  쿼리를 쏴서 확인
    • created_by에 로그인한 유저명이 Jpa Auditing을 통해서 잘 박힘
    • 비밀번호도 Bcrypt Encoder로 Encoding된 비밀번호가 잘 박힘


Principal

 

MyPricipal.java

@Getter
public class MyPrincipal implements UserDetails {
    private String email;
    private String username;
    private String password;
    private String description;
    private Collection<? extends GrantedAuthority> authorities;

    private MyPrincipal(String email, String username, String password, String description, Collection<? extends GrantedAuthority> authorities) {
        this.email = email;
        this.username = username;
        this.password = password;
        this.description = description;
        this.authorities = authorities;
    }

    public static MyPrincipal of(String email, String username, String password, String description) {
        Set<RoleType> roleTypes = Set.of(RoleType.USER);    // TODO
        Collection<? extends GrantedAuthority> authorities = roleTypes
                .stream()
                .map(RoleType::getName)
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toUnmodifiableSet());
        return new MyPrincipal(email, username, password, description, authorities);
    }
    protected MyPrincipal(){}

    //  Principal → Entity
    public static UserAccount toEntity(MyPrincipal myPrincipal){
        return UserAccount.of(
                myPrincipal.getEmail(),
                myPrincipal.getUsername(),
                myPrincipal.getPassword(),
                myPrincipal.getDescription(),
                RoleType.USER
        );
    }

    //  Principal → Dto
    public static UserAccountDto toDto(MyPrincipal myPrincipal){
        return UserAccountDto.of(
                myPrincipal.getEmail(),
                myPrincipal.getUsername(),
                myPrincipal.getPassword(),
                myPrincipal.getDescription(),
                RoleType.USER
        );
    }

    // Entity → Principal
    public static MyPrincipal from(UserAccount userAccount){
        return MyPrincipal.of(
                userAccount.getEmail(),
                userAccount.getUsername(),
                userAccount.getPassword(),
                userAccount.getDescription()
        );
    }

    // Dto → Principal
    public static MyPrincipal from(UserAccountDto userAccountDto) {
        return MyPrincipal.of(
                userAccountDto.getEmail(),
                userAccountDto.getUsername(),
                userAccountDto.getPassword(),
                userAccountDto.getDescription());
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Config

 

Security Config

SecurityConfig.java

  • .csrf().disable()를 적용하지 않으면, /register 경로로 POST요청을 허용했음에도 불구하고, 에러가 발생
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http
    ) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                        // Static(html, css, js, favicon...) 허용
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                        .permitAll()
                        // GET 요청 허용
                        .mvcMatchers(
                            HttpMethod.GET,
                            "/","/register", "/articles"
                        )
                        .permitAll()
                        // POST 요청 허용
                        .mvcMatchers(
                                HttpMethod.POST,
                                "/register"
                        )
                        .permitAll()
                        // 이 외의 모든 기능은 인증 필요
                        .anyRequest()
                        .authenticated()
                )
                // 폼 로그인 사용 & 로그아웃 시 루트페이지로
                .formLogin(withDefaults())
                .logout(logout -> logout.logoutSuccessUrl("/"))
                // csrf 풀기
                .csrf().disable()
                .build();
    }

    @Bean
    public UserDetailsService userDetailsService(UserAccountService userAccountService) {
        return username -> MyPrincipal.from(userAccountService.findByUsername(username));
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

JPA Config

JpaConfig.java

  • Entity의 created_at, created_by, modifiied_at, modified_by와 같은 필드는 자동으로 JPA가 Auditing해주도록 하였다.
  • created_by, modfied_by 필드는 로그인한 유저명을 박아주도록 설정
@EnableJpaAuditing
@Configuration
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorAware() {
        return () -> Optional
                // ⅰ) Security Context 가져오기
                .ofNullable(SecurityContextHolder.getContext())
                // ⅱ) 인증정보 꺼내기
                .map(SecurityContext::getAuthentication)
                // ⅲ) 인증여부 확인
                .filter(Authentication::isAuthenticated)
                // ⅳ) Principal 꺼내기
                .map(Authentication::getPrincipal)
                // ⅴ) Type Casting
                .map(MyPrincipal.class::cast)
                // ⅵ) 유저명 꺼내기
                .map(MyPrincipal::getUsername);
    }
}

Service

 

UserAccountSerivce.java

  • 회원가입(register)시에 비밀번호는 encoding된 값을 DB에 저장
@Service
@RequiredArgsConstructor
public class UserAccountService {
    private final UserAccountRepository userAccountRepository;
    private final BCryptPasswordEncoder encoder;
    public UserAccount findByUsername(String username){
        return userAccountRepository
                .findByUsername(username)
                .orElseThrow(()->{throw new MyException(
                        ErrorCode.USER_NOT_FOUND,
                        String.format("Username [%s] is not founded", username));
        });
    }

    public void register(String email, String username, String password, String description){
        String encodedPassword = encoder.encode(password);
        UserAccount userAccount = UserAccount.of(email, username, encodedPassword, description, RoleType.USER);
        userAccountRepository.save(userAccount);
    }
}

Controller

 

RegisterRequest.java

@Data
public class RegisterRequest {
    private String email;
    private String username;
    private String password;
    private String description;
}

 

MainService.java

  • /register 경로로 GET요청 : resources/auth/register/index.html을 렌더링
  • /register 경로로 POST요청 : 회원가입 후, 게시판 페이지로 redirection
@Controller
@RequestMapping("/")
@RequiredArgsConstructor
public class MainController {

    private final UserAccountService userAccountService;

    @GetMapping("/")
    public String index(){
        return "redirect:/articles";
    }

    @GetMapping("/register")
    public String registerPage(){
        return "/auth/register/index";
    }

    @PostMapping("/register")
    public String register(RegisterRequest req){
        userAccountService.register(
                req.getEmail(),
                req.getUsername(),
                req.getPassword(),
                req.getDescription()
        );
        return "redirect:/articles";
    }
}

View

 

회원가입 페이지

 

 resources/auth/register/index.html

  • 회원가입 페이지 작성
    • form 태그의 method와 action은 controller에서 작성한 코드와 맞춰서 각각 post, /register
    • input tag의 name은 RegisterResponse.java의 필드명과 맞춤
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sign Up</title>
</head>
<body>
    <header/>
    <h1>Sign Up</h1>
    <form method="post" action="/register">
        <label for="email">Email</label>
        <input id="email" name="email" placeholder="Email..."/>

        <label for="username">Username</label>
        <input id="username" name="username" placeholder="Username..."/>

        <label for="password">Password</label>
        <input id="password" name="password" placeholder="Password..." type="password"/>

        <label for="description">Description</label>
        <input id="description" name="description" placeholder="Description..."/>

        <button type="submit">Submit</button>
    </form>
    <footer/>
 </body>
</html>

Thymeleaf Template

 

 resources/auth/register/index.th.xml

  • header와 footer만 가져오는 내용 외에 따로 작성하지 않음
<?xml version="1.0"?>
<thlogic xmlns:th="http://www.thymeleaf.org">
    <attr sel="#header" th:replace="header :: #header"/>
    <attr sel="#footer" th:replace="footer :: #footer"/>
</thlogic>

'Java > Spring' 카테고리의 다른 글

[Spring] 길찾기 서비스 #6  (0) 2022.12.11
[Spring] 길찾기 서비스 #5  (0) 2022.12.11
[Spring] 게시판 만들기 #11  (0) 2022.11.24
[Spring] 게시판 만들기 #10  (0) 2022.11.23
[Spring] 게시판 만들기 #9  (0) 2022.11.23