CREATE TABLE `game_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(10) NOT NULL,
`name` varchar(20) NOT NULL,
`pw` varchar(150) NOT NULL,
PRIMARY KEY (`id`)
)
game users 테이블을 먼저 생성해주었다. 물론 로컬 테스트는 jpa가 자동으로 생성해주지만 까먹지 않게 미리미리!
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "game_users")
public class GameUserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@Column(name = "user_id", nullable = false, unique = true, length = 10)
private String userId;
@Column(nullable = false, length = 20)
private String name;
@Column(nullable = false, length = 150)
private String pw;
@CreationTimestamp
private LocalDateTime createdAt = LocalDateTime.now();
@Builder
public GameUserEntity(@NonNull String userId,@NonNull String name,@NonNull String pw) {
this.userId = userId;
this.name = name;
this.pw = pw;
}
}
entity는 다음과 같이 추가해주고 로직을 만들어보자!
@Repository
public interface GameUserRepository extends CrudRepository<GameUserEntity, Long> {
}
@Getter
@AllArgsConstructor
public class ResponseGameUser {
String userId;
}
Response Vo를 만들어주고
public interface GameUserService {
ResponseGameUser join();
}
interface만 우선 선언해주자.
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class RequestGameUser {
@NotEmpty(message = "아이디는 비어있을 수 없습니다.")
@Length(min = 4, max = 12, message = "아이디는 4~12 글자입니다.")
private String userId;
@NotEmpty(message = "비밀번호는 비어있을 수 없습니다.")
@Length(min = 6, max = 12, message = "비밀번호는 6~12 글자입니다.")
private String pw;
@NotEmpty(message = "비밀번호 재입력은 비어있을 수 없습니다.")
@Length(min = 6, max = 12, message = "비밀번호는 6~12 글자입니다.")
private String rePw;
@NotEmpty(message = "이름은 비어있을 수 없습니다.")
private String name;
}
매개변수로 받을 객체를 먼저 생성해주고
public interface GameUserService {
ResponseGameUser join(RequestGameUser requestGameUser);
}
GameUserService도 수정해준다.
@RestController
@RequestMapping("/game")
@RequiredArgsConstructor
@Slf4j
public class GameUserController {
private final GameUserService gameUserService;
@PostMapping("/join")
public ResponseEntity<CommonApi> join(@RequestBody RequestGameUser requestGameuser, BindingResult bindingResult){
if(bindingResult.hasErrors()){
log.error("game user 회원가입 잘못된 요청");
throw new UserException(UserCode.BAD_REQUEST, bindingResult);
}
ResponseGameUser user = gameUserService.join(requestGameuser);
CommonApi<Object> response = new CommonApi(CommonEnum.OK, user);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
controller의 경우 다음과 같이 작성해주고 bindingResult의 경우 service로 넘겨서 해결해도 되지만 service에서는 서비스 로직만 신경쓰고 싶어서 다음과 같이 작성했다.
@Service
@RequiredArgsConstructor
public class GameUserServiceImpl implements GameUserService{
private final BCryptPasswordEncoder passwordEncoder;
private final GameUserRepository gameUserRepository;
@Override
public ResponseGameUser join(RequestGameUser requestGameUser) {
GameUserEntity gameUserEntity = GameUserEntity.builder()
.userId(requestGameUser.getUserId())
.pw(passwordEncoder.encode(requestGameUser.getPw()))
.name(requestGameUser.getName())
.build();
GameUserEntity save = gameUserRepository.save(gameUserEntity);
ResponseGameUser responseGameUser = new ResponseGameUser(save.getUserId());
return responseGameUser;
}
}
우선은 예외처리 없이 작성해보았다.
포스트맨으로 요청시 잘 응답이 온다.
db에도 잘 저장되었다.
여기서 요청하면서 알았다. rePw을 빼먹고 보냈는데 bindingResult에 걸리지 않았다. 왜지? 당연히 @Valid를 추가하지 않아서지
@PostMapping("/join")
public ResponseEntity<CommonApi> join(@RequestBody @Valid RequestGameUser requestGameuser, BindingResult bindingResult){
...
}
@RequestBody 옆에 @Valid를 추가해주자
다시 그대로 요청하면
이전에 만들었던 Exception 대로 응답이 온다.
회원가입까진 상관 없지만 로그인 했을 경우 jwt를 반환해주어야한다. api-key로 요청했을 경우 해당 api-key가 어떤 서비스에서 요청되는건지 확인하여 해당 서비스에 맞는 회원의 정보를 jwt로 반환해주도록 해보자.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final GameUserService gameUserService;
private final BCryptPasswordEncoder passwordEncoder;
private final Environment env;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/join").permitAll();
http.authorizeRequests().antMatchers("/login").permitAll();
/*
game user 회원 로그인 요청 및 jwt 발급
*/
http.authorizeRequests().antMatchers("/game").permitAll()
.and().addFilter(getAuthenticationGameFilter());
http.authorizeRequests().antMatchers("/**").permitAll()
.and().addFilter(getAuthenticationFilter()) //filter 추가
;
http.headers().frameOptions().disable(); //h2 error
}
private GameAuthFilter getAuthenticationGameFilter() throws Exception {
GameAuthFilter auth = new GameAuthFilter(gameUserService, env);
auth.setFilterProcessesUrl("/game/login");
auth.setAuthenticationManager(authenticationManager());
return auth;
}
//인증 service 등록
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
auth.userDetailsService(gameUserService).passwordEncoder(passwordEncoder);
}
}
다음과 같이 Seurity config 파일을 수정해주면 엄청난 오류를 뿜기 시작할건데 하나씩 수정해보자.
@Slf4j
@RequiredArgsConstructor
public class GameAuthFilter extends UsernamePasswordAuthenticationFilter {
private final GameUserService gameUserService;
private final Environment env;
//로그인 요청
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
RequestLogin login = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
//인증정보 생성
return getAuthenticationManager()
.authenticate(
new UsernamePasswordAuthenticationToken(
login.getUserId(),
login.getPw(),
new ArrayList<>() //권한
)
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//로그인 성공
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
GameUserVo user = gameUserService.getUserDetailByUserId(((User) authResult.getPrincipal()).getUsername());
log.info("로그인 성공 = {}",user.toString());
String token = Jwts.builder()
.setSubject(user.getUserId())
.setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(env.getProperty("token.expiration_time")))) //파기일
.signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret")) //암호화 알고리즘과 암호화 키값
.compact();
response.setHeader("token", token);
}
}
filter로 추가하고 service를 수정해주자.
@Repository
public interface GameUserRepository extends CrudRepository<GameUserEntity, Long> {
GameUserEntity findByUserId(String userId);
}
repository에 우선 추가해주고
public interface GameUserService extends UserDetailsService {
ResponseGameUser join(RequestGameUser requestGameUser);
GameUserVo getUserDetailByUserId(String userId);
}
service에는 getUserDetailByUserId()
메서드를 추가해준 뒤 extends UserDetailsService
를 security에서 사용할 수 있기 위해 추가해주자.
@Service
@RequiredArgsConstructor
public class GameUserServiceImpl implements GameUserService{
private final BCryptPasswordEncoder passwordEncoder;
private final GameUserRepository gameUserRepository;
@Override
public ResponseGameUser join(RequestGameUser requestGameUser) {
GameUserEntity gameUserEntity = GameUserEntity.builder()
.userId(requestGameUser.getUserId())
.pw(passwordEncoder.encode(requestGameUser.getPw()))
.name(requestGameUser.getName())
.build();
GameUserEntity save = gameUserRepository.save(gameUserEntity);
ResponseGameUser responseGameUser = new ResponseGameUser(save.getUserId());
return responseGameUser;
}
@Override
public GameUserVo getUserDetailByUserId(String userId) {
GameUserEntity userEntity = gameUserRepository.findByUserId(userId);
GameUserVo gameUserVo = GameUserVo.builder()
.userId(userEntity.getUserId())
.name(userEntity.getName())
.pw(userEntity.getPw())
.createdAt(userEntity.getCreatedAt())
.build();
return gameUserVo;
}
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
GameUserEntity user = gameUserRepository.findByUserId(userId);
if(user == null) throw new UsernameNotFoundException("user가 존재하지 않습니다.");
return new User(user.getUserId(), user.getPw(), true, true, true, true, new ArrayList<>());
}
}
다음과 같이 모두 구현해주고
포스트맨으로 테스트해보자
game의 경우 /game/login
으로 요청해야만 작동한다.
api key 발급을 위한 로그인 서비스도 정상적으로 반환하는 것을 확인할 수 있다!
진행하면서
.setFilterProcessesUrl()
를 모르는 상태로 진행해서 모든 요청이 새로운 필터로 요청되는 에러가 있었다. 모든 소스를 다 롤백해야되는 줄 알고 힘들 뻔 했는데 다행히 해당 메서드로 login 요청 url과 작동 filter를 구분할 수 있어서 잘 해결할 수 있었던거 같다!