Spring

[Keycloak] MSA에서 키클록 Spring Security 연동 (2) 토큰 발급, 토큰값 확인

Karla Ko 2022. 12. 27. 22:35
728x90
 

[Keycloak] MSA에서 키클록 Spring Security 연동 (1) dependency 추가, gateway/auth 서비스 설정

개발환경 macOS Spring Boot 2.7.5 RELEASE JAVA 11 1. MSA 서비스 구조 서비스 디스커버리 패턴으로 gateway 서비스(api gateway)를 통해 각 서비스의 api를 호출하는 형태입니다. auth 서비스를 통해 회원가입/로그

karla.tistory.com

1. auth 서비스 - 회원가입

AuthController

authService를 통해 keycloack DB에 사용자 생성 후, userService를 통해 부가적인 사용자 데이터를 user DB에 저장

  // 회원가입
@PostMapping("/signup")
public ResponseEntity<String> registerUser(UserDTO userDto) {
    if(authService.existsByUsername(userDto.getUserId())) {
        return new ResponseEntity<>("유저가 존재합니다.", HttpStatus.CONFLICT);
    }
    try {

        UserDTO userDTO = authService.createUser(userDto); // keycloak 사용자 생성

        userService.registerUser(userDTO);
    } catch (Exception e) {
        log.error(e.getMessage());
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }
    return new ResponseEntity<>("회원가입에 성공했습니다.", HttpStatus.CREATED);
}

 

UserDto

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private String userId;
    private String name;
    private String job;
    private int age;
    private String gender;
    private String phone;
    private String mailAddr;
    private UserRole userRole;
    private MultipartFile userImage;
    private String imageUrl;
    private String userPwd;
    private int status;
    private String statusInfo;
}

 

AuthService

application.yml의 값을 사용

@Value("${keycloak.auth-server-url}")
private String authServerUrl;

@Value("${keycloak.realm}")
private String realm;

@Value("${keycloak.resource}")
private String clientId;

@Value("${keycloak.credentials.secret}")
private String clientSecret;

private final Keycloak keycloak;

 

keycloak API로 사용자 생성

public UserDTO createUser(UserDTO userDto) {

    // 유저정보 세팅
    UserRepresentation user = new UserRepresentation();
    user.setEnabled(true);
    user.setUsername(userDto.getUserId());

    // Get realm
    RealmResource realmResource = keycloak.realm(realm);
    UsersResource usersResource = realmResource.users();

    Response response = usersResource.create(user);
    if(response.getStatus() == 201) {

        String userId = CreatedResponseUtil.getCreatedId(response);

        // create password credential
        CredentialRepresentation passwordCred = new CredentialRepresentation();
        passwordCred.setTemporary(false);
        passwordCred.setType(CredentialRepresentation.PASSWORD);
        passwordCred.setValue(userDto.getUserPwd());
        log.info("Created userId {}", userId);
        UserResource userResource = usersResource.get(userId);

        // Set password credential
        userResource.resetPassword(passwordCred);

        // role 세팅
        ClientRepresentation clientRep = realmResource.clients().findByClientId(clientId).get(0);
        RoleRepresentation clientRoleRep = realmResource.clients().get(clientRep.getId()).roles().get(userDto.getUserRole().getCode()).toRepresentation();
        userResource.roles().clientLevel(clientRep.getId()).add(Arrays.asList(clientRoleRep));

	}

    userDto.setStatus(response.getStatus());
   // userDto.setStatusInfo(response.getStatusInfo().toString());

    return userDto;
}

 

2. auth 서비스 - 로그인(토큰 발급)

키클록 API

 

AuthController

아이디와 비밀번호를 파라미터로 받고, 키클록 토큰 발급에 성공하면 아이디에 따른 사용자 정보를 함께 반환

  // 로그인
    @PostMapping(path = "/signin")
    public ResponseEntity authenticateUser(@RequestBody HashMap<String, String> map) {
      
        AccessTokenResponse tokenRes = authService.setAuth(map); // Token 발급
        
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("token", tokenRes);
        if(tokenRes != null){
            data.put("info", userService.getUserDetailInfo(map.get("username")));
        }
        Map<String, Object> resultmap = new HashMap<String, Object>();
        resultmap.put("data", data);
        return ResponseEntity.ok(resultmap);
    }

 

AuthService

keycloak API로 토큰 발급

 public AccessTokenResponse setAuth(HashMap<String, String> map) {
        Map<String, Object> clientCredentials = new HashMap<>();
        clientCredentials.put("secret", clientSecret);
        clientCredentials.put("grant_type", "password");

        Configuration configuration =new Configuration(authServerUrl, realm, clientId, clientCredentials, null);
        AuthzClient authzClient = AuthzClient.create(configuration);

        AccessTokenResponse response = authzClient.obtainAccessToken(map.get("username"), map.get("password"));
        // authzClient.obtainAccessToken(아이디, 비밀번호);
        
        return response;
    }

 

3. auth 서비스 - 토큰 리프레시

키클록 API

 

AuthController

리프레시 토큰을 파라미터로 받고, 키클록 토큰 발급에 성공하면 아이디에 따른 사용자 정보를 함께 반환

// refresh token
    @PostMapping(path = "/refresh_token")
    public ResponseEntity refreshToken(@RequestBody  HashMap<String, String> map) {
        String refreshToken = map.get("refresh_token");
        Map<String, Object> tokenRes = authService.refreshToken(refreshToken);

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("token", tokenRes.get("token"));
        if(tokenRes != null){
            data.put("info", userService.getUserDetailInfo((String) tokenRes.get("username")));
        }

        return ResponseEntity.ok(data);
    }

 

AuthService

grant_type을 refresh_token으로 토큰 발급, 발급에 성공하면 토큰과 사용자 아이디를 반환

  public   Map<String, Object> refreshToken(String refreshToken) {
        String url = authServerUrl + "realms/" + realm + "/protocol/openid-connect/token";
        String url2 = authServerUrl + "realms/" + realm + "/protocol/openid-connect/userinfo";


        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("refresh_token", refreshToken);
        map.add("grant_type", "refresh_token");
        map.add("client_id", clientId);
        map.add("client_secret", clientSecret);

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        HttpEntity<?> entity = new HttpEntity<>(map, headers);

        Map<String, Object> data = new HashMap<String, Object>();

        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
        data.put("token", response.getBody());

        if (response.getStatusCodeValue() == 200 ) {
            headers.add( "Authorization","Bearer " + response.getBody().get("access_token"));

            ResponseEntity<Map> response2 = restTemplate.exchange( url2, HttpMethod.POST, entity, Map.class );
            data.put("username", response2.getBody().get("preferred_username"));
        }
        return data;
    }

 

더보기
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {

    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.resource}")
    private String clientId;

    @Value("${keycloak.credentials.secret}")
    private String clientSecret;

    private final Keycloak keycloak;

    public UserDTO createUser(UserDTO userDto) {

        // 유저정보 세팅
        UserRepresentation user = new UserRepresentation();
        user.setEnabled(true);
        user.setUsername(userDto.getUserId());

        // Get realm
        RealmResource realmResource = keycloak.realm(realm);
        UsersResource usersResource = realmResource.users();

        Response response = usersResource.create(user);
        if(response.getStatus() == 201) {

            String userId = CreatedResponseUtil.getCreatedId(response);

            // create password credential
            CredentialRepresentation passwordCred = new CredentialRepresentation();
            passwordCred.setTemporary(false);
            passwordCred.setType(CredentialRepresentation.PASSWORD);
            passwordCred.setValue(userDto.getUserPwd());
            log.info("Created userId {}", userId);
            UserResource userResource = usersResource.get(userId);

            // Set password credential
            userResource.resetPassword(passwordCred);

            // role 세팅
            ClientRepresentation clientRep = realmResource.clients().findByClientId(clientId).get(0);
            RoleRepresentation clientRoleRep = realmResource.clients().get(clientRep.getId()).roles().get(userDto.getUserRole().getCode()).toRepresentation();
            userResource.roles().clientLevel(clientRep.getId()).add(Arrays.asList(clientRoleRep));

        }

        userDto.setStatus(response.getStatus());

        return userDto;
    }

    public AccessTokenResponse setAuth(HashMap<String, String> map) {
        Map<String, Object> clientCredentials = new HashMap<>();
        clientCredentials.put("secret", clientSecret);
        clientCredentials.put("grant_type", "password");

        Configuration configuration =new Configuration(authServerUrl, realm, clientId, clientCredentials, null);
        AuthzClient authzClient = AuthzClient.create(configuration);

        AccessTokenResponse response = authzClient.obtainAccessToken(map.get("username"), map.get("password"));

        return response;
    }

    public   Map<String, Object> refreshToken(String refreshToken) {
        String url = authServerUrl + "realms/" + realm + "/protocol/openid-connect/token";
        String url2 = authServerUrl + "realms/" + realm + "/protocol/openid-connect/userinfo";


        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("refresh_token", refreshToken);
        map.add("grant_type", "refresh_token");
        map.add("client_id", clientId);
        map.add("client_secret", clientSecret);

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        HttpEntity<?> entity = new HttpEntity<>(map, headers);

        Map<String, Object> data = new HashMap<String, Object>();

        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
        data.put("token", response.getBody());

        if (response.getStatusCodeValue() == 200 ) {
            headers.add( "Authorization","Bearer " + response.getBody().get("access_token"));

            ResponseEntity<Map> response2 = restTemplate.exchange( url2, HttpMethod.POST, entity, Map.class );
            data.put("username", response2.getBody().get("preferred_username"));
        }
        return data;
    }

    // 사용자 존재하는지 체크
    public boolean existsByUsername(String userName) {

        List<UserRepresentation> search = keycloak.realm(realm).users().search(userName);
        if(search.size() > 0){
            log.debug("search : {}", search.get(0).getUsername());
            return true;
        }
        return false;
    }
}

 

4. 토큰 체크

controller단에서 Principal 객체를 사용해 JwtAuthenticationToken 사용자 권한 및 아이디 조회

@PostMapping("/list")
public ResponseEntity<PageResultDTO<GroupDTO, Group>> getGroupList(Principal principal, PageRequestDTO pageRequestDTO) {
    JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
    //사용자 아이디 조회
    String userId = token.getTokenAttributes().get("preferred_username").toString();

    return new ResponseEntity<>( groupService.getGroupList(userId, pageRequestDTO), HttpStatus.OK);
}
@GetMapping("/menu")
public List<MenuDTO> getUserMenuDTO(Principal principal) {

        if (principal != null) { // 토큰이 있는 경우
            JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
            Map<String,Object> resource_access = (Map<String,Object>) token.getTokenAttributes().get("resource_access");
            Map<String, Object> team_cloud_client = (Map<String, Object>) resource_access.get("team_cloud_client");
            // 사용자 권한 조회
            ArrayList<String> roles = (ArrayList) team_cloud_client.get("roles");
            
        if (roles.get(0).equals("USER")) { // USER 권한인 경우
            return commonService.getUserMenuDTOList();
        } else if (roles.get(0).equals("ADMIN")) { // ADMIN 권한인 경우
            return commonService.getAdminMenuDTOList();
        }
    }
    return commonService.getAllMenuDTOList();
}

 

각 서비스에 생성한 security config 파일로 @PreAuthorize를 키클록의 권한으로 설정

@PreAuthorize("hasRole('ROLE_USER')") : USER 권한을 가진 사용자만 접근

// 설문 생성 리스트 조회
@PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping(value = "/make_list", method = RequestMethod.GET)
public ResponseEntity<Page<SurveyDTO>> getMakeList(
                                        @RequestParam (value = "category", required = false) Integer[] categoryId,
                                        @RequestParam (value = "status", required = false) SurveyStatus status,
                                        @RequestParam (value = "title", required = false) String title,
                                        Principal principal, PageRequestDTO pageRequestDTO) {

    JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
    String userId = token.getTokenAttributes().get("preferred_username").toString();

    Page<SurveyDTO> list =  surveyService.getSurveyMakeList(title, userId, categoryId, status, pageRequestDTO);

    return new ResponseEntity<>(list, HttpStatus.OK);
}

 

 

 

 

 

728x90