What to do?
인증기능 완성하기 #1
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 |