Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] move code from AccountController to AccountService #2373

Merged
merged 9 commits into from
Jul 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,19 @@

import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_LOGIN_FAILED_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import com.usthe.sureness.provider.SurenessAccount;
import com.usthe.sureness.provider.SurenessAccountProvider;
import com.usthe.sureness.provider.ducument.DocumentAccountProvider;
import com.usthe.sureness.util.JsonWebTokenUtil;
import com.usthe.sureness.util.Md5Util;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.naming.AuthenticationException;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
import org.apache.hertzbeat.manager.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -50,49 +44,21 @@
* Authentication registration TOKEN management API
*/
@Tag(name = "Auth Manage API")
@RestController()
@RestController
@RequestMapping(value = "/api/account/auth", produces = {APPLICATION_JSON_VALUE})
@Slf4j
public class AccountController {
/**
* Token validity time in seconds
*/
private static final long PERIOD_TIME = 3600L;
/**
* account data provider
*/
private SurenessAccountProvider accountProvider = new DocumentAccountProvider();
@Autowired
private AccountService accountService;

@PostMapping("/form")
@Operation(summary = "Account password login to obtain associated user information", description = "Account password login to obtain associated user information")
public ResponseEntity<Message<Map<String, String>>> authGetToken(@Valid @RequestBody LoginDto loginDto) {
SurenessAccount account = accountProvider.loadAccount(loginDto.getIdentifier());
if (account == null || account.getPassword() == null) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Incorrect Account or Password"));
} else {
String password = loginDto.getCredential();
if (account.getSalt() != null) {
password = Md5Util.md5(password + account.getSalt());
}
if (!account.getPassword().equals(password)) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Incorrect Account or Password"));
}
if (account.isDisabledAccount() || account.isExcessiveAttempts()) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Expired or Illegal Account"));
}
try {
return ResponseEntity.ok(Message.success(accountService.authGetToken(loginDto)));
} catch (AuthenticationException e) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, e.getMessage()));
}
// Get the roles the user has - rbac
List<String> roles = account.getOwnRoles();
// Issue TOKEN
String issueToken = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME, roles);
Map<String, Object> customClaimMap = new HashMap<>(1);
customClaimMap.put("refresh", true);
String issueRefresh = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME << 5, customClaimMap);
Map<String, String> resp = new HashMap<>(2);
resp.put("token", issueToken);
resp.put("refreshToken", issueRefresh);
resp.put("role", JsonUtil.toJson(roles));
return ResponseEntity.ok(Message.success(resp));
}

@GetMapping("/refresh/{refreshToken}")
Expand All @@ -101,30 +67,12 @@ public ResponseEntity<Message<RefreshTokenResponse>> refreshToken(
@Parameter(description = "Refresh TOKEN", example = "xxx")
@PathVariable("refreshToken") @NotNull final String refreshToken) {
try {
Claims claims = JsonWebTokenUtil.parseJwt(refreshToken);
String userId = String.valueOf(claims.getSubject());
boolean isRefresh = claims.get("refresh", Boolean.class);
if (userId == null || !isRefresh) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Illegal Refresh Token"));
}
SurenessAccount account = accountProvider.loadAccount(userId);
if (account == null) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Not Exists This Token Mapping Account"));
}
List<String> roles = account.getOwnRoles();
String issueToken = issueToken(userId, roles, PERIOD_TIME);
String issueRefresh = issueToken(userId, roles, PERIOD_TIME << 5);
RefreshTokenResponse response = new RefreshTokenResponse(issueToken, issueRefresh);
return ResponseEntity.ok(Message.success(response));
return ResponseEntity.ok(Message.success(accountService.refreshToken(refreshToken)));
} catch (AuthenticationException e) {
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, e.getMessage()));
} catch (Exception e) {
log.error("Exception occurred during token refresh: {}", e.getClass().getName(), e);
return ResponseEntity.ok(Message.fail(MONITOR_LOGIN_FAILED_CODE, "Refresh Token Expired or Error"));
}
}

private String issueToken(String userId, List<String> roles, long expirationMillis) {
Map<String, Object> customClaimMap = new HashMap<>(1);
customClaimMap.put("refresh", true);
return JsonWebTokenUtil.issueJwt(userId, expirationMillis, roles, customClaimMap);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hertzbeat.manager.service;

import java.util.Map;
import javax.naming.AuthenticationException;
import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;

/**
* Account service
*/
public interface AccountService {
/**
* Account password login to obtain associated user information
* @param loginDto loginDto
* @return token info
* @throws AuthenticationException when authentication is failed
*/
Map<String, String> authGetToken(LoginDto loginDto) throws AuthenticationException;

/**
* Use refresh TOKEN to re-acquire TOKEN
* @param refreshToken refreshToken
* @return token and refresh token
* @throws AuthenticationException failed to refresh
*/
RefreshTokenResponse refreshToken(String refreshToken) throws AuthenticationException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.hertzbeat.manager.service.impl;

import com.usthe.sureness.provider.SurenessAccount;
import com.usthe.sureness.provider.SurenessAccountProvider;
import com.usthe.sureness.provider.ducument.DocumentAccountProvider;
import com.usthe.sureness.util.JsonWebTokenUtil;
import com.usthe.sureness.util.Md5Util;
import io.jsonwebtoken.Claims;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.naming.AuthenticationException;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
import org.apache.hertzbeat.manager.service.AccountService;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;

/**
* Implementation of Account service
*/
@Service
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class AccountServiceImpl implements AccountService {
/**
* Token validity time in seconds
*/
private static final long PERIOD_TIME = 3600L;
/**
* account data provider
*/
private final SurenessAccountProvider accountProvider = new DocumentAccountProvider();

@Override
public Map<String, String> authGetToken(LoginDto loginDto) throws AuthenticationException {
SurenessAccount account = accountProvider.loadAccount(loginDto.getIdentifier());
if (account == null || account.getPassword() == null) {
throw new AuthenticationException("Incorrect Account or Password");
} else {
String password = loginDto.getCredential();
if (account.getSalt() != null) {
password = Md5Util.md5(password + account.getSalt());
}
if (!account.getPassword().equals(password)) {
throw new AuthenticationException("Incorrect Account or Password");
}
if (account.isDisabledAccount() || account.isExcessiveAttempts()) {
throw new AuthenticationException("Expired or Illegal Account");
}
}
// Get the roles the user has - rbac
List<String> roles = account.getOwnRoles();
// Issue TOKEN
String issueToken = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME, roles);
Map<String, Object> customClaimMap = new HashMap<>(1);
customClaimMap.put("refresh", true);
String issueRefresh = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME << 5, customClaimMap);
Map<String, String> resp = new HashMap<>(2);
resp.put("token", issueToken);
resp.put("refreshToken", issueRefresh);
resp.put("role", JsonUtil.toJson(roles));

return resp;
}

@Override
public RefreshTokenResponse refreshToken(String refreshToken) throws AuthenticationException {
Claims claims = JsonWebTokenUtil.parseJwt(refreshToken);
String userId = String.valueOf(claims.getSubject());
boolean isRefresh = claims.get("refresh", Boolean.class);
if (userId == null || !isRefresh) {
throw new AuthenticationException("Illegal Refresh Token");
}
SurenessAccount account = accountProvider.loadAccount(userId);
if (account == null) {
throw new AuthenticationException("Not Exists This Token Mapping Account");
}
List<String> roles = account.getOwnRoles();
String issueToken = issueToken(userId, roles, PERIOD_TIME);
String issueRefresh = issueToken(userId, roles, PERIOD_TIME << 5);
return new RefreshTokenResponse(issueToken, issueRefresh);
}

private String issueToken(String userId, List<String> roles, long expirationMillis) {
Map<String, Object> customClaimMap = new HashMap<>(1);
customClaimMap.put("refresh", true);
return JsonWebTokenUtil.issueJwt(userId, expirationMillis, roles, customClaimMap);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,19 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.usthe.sureness.util.JsonWebTokenUtil;
import java.util.HashMap;
import java.util.Map;
import javax.naming.AuthenticationException;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
import org.apache.hertzbeat.manager.service.AccountService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
Expand All @@ -43,6 +49,8 @@ class AccountControllerTest {

@InjectMocks
private AccountController accountController;
@Mock
private AccountService accountService;

@BeforeEach
void setUp() {
Expand All @@ -59,6 +67,12 @@ void authGetToken() throws Exception {
.identifier("admin")
.credential("hertzbeat")
.build();
Map<String, String> resp = new HashMap<>(2);
resp.put("token", "token");
resp.put("refreshToken", "refreshToken");
resp.put("role", "roles");
Mockito.when(accountService.authGetToken(loginDto)).thenReturn(resp);

this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/auth/form")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(loginDto)))
Expand All @@ -67,6 +81,7 @@ void authGetToken() throws Exception {
.andExpect(jsonPath("$.data.token").exists())
.andReturn();
loginDto.setCredential("wrong_credential");
Mockito.when(accountService.authGetToken(loginDto)).thenThrow(new AuthenticationException());
this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/auth/form")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(loginDto)))
Expand All @@ -76,8 +91,10 @@ void authGetToken() throws Exception {

@Test
void refreshToken() throws Exception {
String refreshToken = "123456";
Mockito.when(accountService.refreshToken(refreshToken)).thenThrow(new AuthenticationException());
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/auth/refresh/{refreshToken}",
"123456"))
refreshToken))
.andExpect(jsonPath("$.code").value((int) CommonConstants.MONITOR_LOGIN_FAILED_CODE))
.andReturn();
}
Expand Down
Loading