우선 회원가입 기능부터 시작해 보자.
회원가입시 권한은 USER이고 ID가 sol이라면 ADMIN 권한을 준다.
나중에 ADMIN이 USER의 권한을 변경하는 로직을 추가 할 예정이다.
우선 Controller이다.
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
@Api(tags = {"User API"})
public class UserRestController {
private final UserService userService;
@ApiOperation(value = "회원가입 기능",notes = "userName, password 입력해서 회원가입")
@PostMapping("/join")
public Response<UserJoinResponse> join(@RequestBody UserJoinRequest userJoinRequest) {
log.info("\nusername = {}, password = {}\n",userJoinRequest.getUserName(), userJoinRequest.getPassword());
//id에 sol라는 단어가 들어가면 ADMIN 권한이 주어진다.
return Response.success(userService.join(userJoinRequest));
}
}
User Entity이다.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
@Table(name = "Users")
public class User extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userName;
private String password;
@OneToMany(mappedBy = "user") //post의 user필드와 연결
@Builder.Default
private List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
@Builder.Default
private List<Comment> comments = new ArrayList<>();
@OneToMany(mappedBy = "user")
@Builder.Default
private List<Like> likes = new ArrayList<>();
@OneToMany(mappedBy = "user")
@Builder.Default
private List<Alarm> alarms = new ArrayList<>();
@Enumerated(EnumType.STRING)
private UserRole role;
public UserJoinResponse fromEntity() {
return new UserJoinResponse(this.id, this.userName);
}
}
Dto를 통해 값을 입력받고 있다.
Request dto와 Response dto이다.
@Getter
@AllArgsConstructor
@Slf4j
public class UserJoinRequest {
private String userName;
private String password;
public static User toEntity(UserJoinRequest dto, BCryptPasswordEncoder encoder) {
UserRole userRole = UserRole.ROLE_USER;
//초기 ADMIN회원 1명 (id가 sol이라면)
if(dto.userName.equals("sol")) userRole = UserRole.ROLE_ADMIN;
User user = User.builder()
.userName(dto.userName)
.password(encoder.encode(dto.password))
.role(userRole)
.build();
return user;
}
}
@Getter
@AllArgsConstructor
public class UserJoinResponse {
private Long userId;
private String userName;
}
Service에서 비지니스 로직을 처리한다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder encoder;
public UserJoinResponse join(UserJoinRequest dto) {
// 중복처리
userRepository.findByUserName(dto.getUserName())
.ifPresent((user -> {
throw new AppException(ErrorCode.DUPLICATED_USER_NAME, dto.getUserName() + "는 이미 존재합니다.");
}));
User savedUser = userRepository.save(UserJoinRequest.toEntity(dto, encoder));
//Entity -> dto
UserJoinResponse userJoinResponse = savedUser.fromEntity();
return userJoinResponse;
}
}
DB에 접근하기위해 JpaRepository를 사용했다.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUserName(String username);
}
권한을 부여할때 Enum으로 사용할 수 있게 처리했다.
public enum UserRole {
ROLE_USER, ROLE_ADMIN
}
우선 Dependency를 추가해주자
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
}
@EnableWebSecurity를 사용해야 Spring Security를 활성화 시킬 수 있다.
Security가 활성화 되어야 위의 BCryptPasswordEncoder를 사용할 수 있다.
Spring Security적용을 하려면 Security Filter의 구조를 알아야 한다를 참고하면 Spring Security를 이해하는데 큰 도움이 될것이다.
.antMatchers()를 통해 접근제한을 걸수있다.
authenticated() : 인증이 필요함
permitAll() : 인증,인가 필요없음
access() : 인가 필요함
아래에 보면 addFilterBefore()을 사용했다.
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)
UserNamePasswordAuthenticationFilter적용하기 전에 JWTFilter를 적용 하라는 뜻이다.
이렇게 되면 설정한 UsernamePasswordAuthenticationToken을 SecurityContextHolder에서 꺼내 사용할 수 있다.
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final UserService userService;
@Value("${jwt.secret}")
private String secretKey;
public SecurityConfig(UserService userService) {
this.userService = userService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
//Post는 전부 막아논다.
// .antMatchers(HttpMethod.POST, "/api/**").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.POST, "/api/v1/posts/**").access("hasRole('ROLE_USER')")
.antMatchers(HttpMethod.POST, "/api/v1/users/{id}/role/change").access("hasRole('ROLE_ADMIN')")
// .antMatchers(HttpMethod.POST, "/api/**").authenticated()
.antMatchers(HttpMethod.PUT, "/api/**").authenticated()
.antMatchers(HttpMethod.DELETE, "/api/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS = jwt사용하는 경우 씀 : 매번 토큰을 사용하는 개념?
.and()
.addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
.build();
}
}
Service에서 BCryptPasswordEncoder를 사용해 비밀번호를 암호화 해야한다.
BCryptPasswordEncoder를 만들어보자.
@Configuration
public class EncoderConfig {
//암호화를 위해 Bean 등록
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
이제 Password가 암호화되어서 잘 저장되는것을 확인 할 수 있다.
