Authentication manager added.
This commit is contained in:
parent
8e29051975
commit
4ffdbca835
|
@ -0,0 +1,40 @@
|
|||
package org.bench4q.master.api;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.bench4q.master.auth.AuthenticationManager;
|
||||
import org.bench4q.master.entity.User;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public abstract class BaseController {
|
||||
private HttpServletRequest request;
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
protected HttpServletRequest getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected void setRequest(HttpServletRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
protected AuthenticationManager getAuthenticationManager() {
|
||||
return authenticationManager;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
protected void setAuthenticationManager(
|
||||
AuthenticationManager authenticationManager) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
protected User getPrincipal() {
|
||||
return this.getAuthenticationManager().getPrincipal(this.getRequest());
|
||||
}
|
||||
|
||||
protected boolean checkScope(String scope) {
|
||||
return this.getAuthenticationManager().checkScope(this.getRequest(),
|
||||
scope);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,24 @@
|
|||
package org.bench4q.master.api;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class HomeController {
|
||||
@RequestMapping(value = { "/" }, method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
public String index() {
|
||||
return "It works!";
|
||||
}
|
||||
package org.bench4q.master.api;
|
||||
|
||||
import org.bench4q.master.entity.User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class HomeController extends BaseController {
|
||||
@RequestMapping(value = { "/" }, method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
public String index() {
|
||||
User principal = this.getPrincipal();
|
||||
String userName;
|
||||
if (principal == null) {
|
||||
userName = "Anonymous User";
|
||||
} else {
|
||||
userName = principal.getUserName();
|
||||
}
|
||||
return "Hello [" + userName + "]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.bench4q.master.api;
|
||||
|
||||
import org.bench4q.master.api.model.AuthorizeResponseModel;
|
||||
import org.bench4q.master.api.model.RegisterResponseModel;
|
||||
import org.bench4q.master.auth.AccessToken;
|
||||
import org.bench4q.master.auth.AuthenticationManager;
|
||||
import org.bench4q.master.entity.User;
|
||||
import org.bench4q.master.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
private UserService userService;
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
private UserService getUserService() {
|
||||
return userService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private void setUserService(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public AuthenticationManager getAuthenticationManager() {
|
||||
return authenticationManager;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setAuthenticationManager(
|
||||
AuthenticationManager authenticationManager) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/register", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
public RegisterResponseModel register(@RequestParam String userName,
|
||||
@RequestParam String password) {
|
||||
boolean success = this.getUserService().register(userName, password);
|
||||
RegisterResponseModel registerUserResponseModel = new RegisterResponseModel();
|
||||
registerUserResponseModel.setSuccess(success);
|
||||
return registerUserResponseModel;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/authorize", method = RequestMethod.GET)
|
||||
@ResponseBody
|
||||
public AuthorizeResponseModel authorize(@RequestParam String userName,
|
||||
@RequestParam String password) {
|
||||
boolean authorized = this.getUserService().validateUser(userName,
|
||||
password);
|
||||
if (!authorized) {
|
||||
AuthorizeResponseModel responseModel = new AuthorizeResponseModel();
|
||||
responseModel.setAccessToken(null);
|
||||
responseModel.setExpiresIn(-1);
|
||||
responseModel.setSuccess(false);
|
||||
return responseModel;
|
||||
}
|
||||
User user = this.getUserService().getUserByName(userName);
|
||||
AccessToken accessToken = this.getAuthenticationManager()
|
||||
.generateAccessToken(user);
|
||||
String credential = this.getAuthenticationManager().generateCredential(
|
||||
user);
|
||||
AuthorizeResponseModel responseModel = new AuthorizeResponseModel();
|
||||
responseModel.setAccessToken(credential);
|
||||
responseModel.setExpiresIn(accessToken.getExpiresIn());
|
||||
responseModel.setSuccess(true);
|
||||
return responseModel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package org.bench4q.master.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
import com.sun.xml.internal.txw2.annotation.XmlElement;
|
||||
|
||||
@XmlRootElement(name = "authorizeResponse")
|
||||
public class AuthorizeResponseModel {
|
||||
private boolean success;
|
||||
private String accessToken;
|
||||
private int expiresIn;
|
||||
|
||||
@XmlElement
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
@XmlElement
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
@XmlElement
|
||||
public int getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public void setExpiresIn(int expiresIn) {
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.bench4q.master.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
import com.sun.xml.internal.txw2.annotation.XmlElement;
|
||||
|
||||
@XmlRootElement(name = "registerResponse")
|
||||
public class RegisterResponseModel {
|
||||
private boolean success;
|
||||
|
||||
@XmlElement
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package org.bench4q.master.auth;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class AccessToken implements Serializable {
|
||||
private static final long serialVersionUID = 4134631390384650898L;
|
||||
private String userName;
|
||||
private String password;
|
||||
private Date generateTime;
|
||||
private int expiresIn;
|
||||
private List<String> scope;
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Date getGenerateTime() {
|
||||
return generateTime;
|
||||
}
|
||||
|
||||
public void setGenerateTime(Date generateTime) {
|
||||
this.generateTime = generateTime;
|
||||
}
|
||||
|
||||
public int getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public void setExpiresIn(int expiresIn) {
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
|
||||
public List<String> getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void setScope(List<String> scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package org.bench4q.master.auth;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.bench4q.master.entity.User;
|
||||
import org.bench4q.master.helper.StringHelper;
|
||||
import org.bench4q.master.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthenticationManager {
|
||||
private final int defaultExpiresTime = 3600;
|
||||
private CryptoManager cryptoManager;
|
||||
private UserService userService;
|
||||
private StringHelper stringHelper;
|
||||
|
||||
private CryptoManager getCryptoManager() {
|
||||
return cryptoManager;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private void setCryptoManager(CryptoManager cryptoManager) {
|
||||
this.cryptoManager = cryptoManager;
|
||||
}
|
||||
|
||||
private UserService getUserService() {
|
||||
return userService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private void setUserService(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private StringHelper getStringHelper() {
|
||||
return stringHelper;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private void setStringHelper(StringHelper stringHelper) {
|
||||
this.stringHelper = stringHelper;
|
||||
}
|
||||
|
||||
public User getPrincipal(HttpServletRequest request) {
|
||||
AccessToken accessToken = this.getAccessToken(request);
|
||||
if (accessToken == null) {
|
||||
return null;
|
||||
}
|
||||
return this.extractUser(accessToken);
|
||||
}
|
||||
|
||||
public boolean checkScope(HttpServletRequest request, String scope) {
|
||||
AccessToken accessToken = this.getAccessToken(request);
|
||||
if (accessToken == null) {
|
||||
return false;
|
||||
}
|
||||
return accessToken.getScope().contains(scope);
|
||||
}
|
||||
|
||||
public String generateCredential(User user) {
|
||||
AccessToken accessToken = this.generateAccessToken(user);
|
||||
return this.generateCredentialFromAccessToken(accessToken);
|
||||
}
|
||||
|
||||
private AccessToken getAccessToken(HttpServletRequest request) {
|
||||
if (request.getHeader("Authorization") == null) {
|
||||
return null;
|
||||
}
|
||||
if (!request.getHeader("Authorization").startsWith("Bearer ")) {
|
||||
return null;
|
||||
}
|
||||
String hex = request.getHeader("Authorization").substring(
|
||||
"Bearer ".length());
|
||||
byte[] bytes = this.getStringHelper().convertToBytes(hex);
|
||||
return this.extractAccessToken(bytes);
|
||||
}
|
||||
|
||||
private User extractUser(AccessToken accessToken) {
|
||||
if (accessToken == null) {
|
||||
return null;
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(accessToken.getGenerateTime());
|
||||
calendar.add(Calendar.SECOND, accessToken.getExpiresIn());
|
||||
Date expireTime = calendar.getTime();
|
||||
if (expireTime.before(new Date(System.currentTimeMillis()))) {
|
||||
return null;
|
||||
}
|
||||
return this.getUserService().getUserByName(accessToken.getUserName());
|
||||
}
|
||||
|
||||
public AccessToken generateAccessToken(User user) {
|
||||
AccessToken accessToken = new AccessToken();
|
||||
accessToken.setExpiresIn(this.defaultExpiresTime);
|
||||
accessToken.setGenerateTime(new Date(System.currentTimeMillis()));
|
||||
accessToken.setPassword(user.getPassword());
|
||||
accessToken.setUserName(user.getUserName());
|
||||
// TODO: scope
|
||||
accessToken.setScope(new ArrayList<String>());
|
||||
accessToken.getScope().add("all");
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
private AccessToken extractAccessToken(byte[] bytes) {
|
||||
try {
|
||||
byte[] decrypted = this.getCryptoManager().decrypt(bytes);
|
||||
ObjectInputStream inputStream = new ObjectInputStream(
|
||||
new ByteArrayInputStream(decrypted));
|
||||
return (AccessToken) inputStream.readObject();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String generateCredentialFromAccessToken(AccessToken accessToken) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ObjectOutputStream stream = new ObjectOutputStream(outputStream);
|
||||
stream.writeObject(accessToken);
|
||||
byte[] data = outputStream.toByteArray();
|
||||
byte[] encryptedData = this.getCryptoManager().encrypt(data);
|
||||
return this.getStringHelper().convertToHexString(encryptedData);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package org.bench4q.master.auth;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.KeyStore.PrivateKeyEntry;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class CryptoManager {
|
||||
private static final int MAX_ENCRYPT_BLOCK = 245;
|
||||
private static final int MAX_DECRYPT_BLOCK = 256;
|
||||
|
||||
private PublicKey publicKey;
|
||||
private PrivateKey privateKey;
|
||||
|
||||
private PublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
private void setPublicKey(PublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
private PrivateKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
private void setPrivateKey(PrivateKey privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public CryptoManager() {
|
||||
try {
|
||||
String pfxFileName = Thread.currentThread().getContextClassLoader()
|
||||
.getResource("key.pfx").getFile();
|
||||
String pfxPassword = "123456";
|
||||
File pfxFile = new File(pfxFileName);
|
||||
FileInputStream pfxFileInputStream = new FileInputStream(pfxFile);
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(pfxFileInputStream, pfxPassword.toCharArray());
|
||||
pfxFileInputStream.close();
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
if (aliases.hasMoreElements()) {
|
||||
String alias = aliases.nextElement();
|
||||
PrivateKeyEntry privateKeyEntry = (PrivateKeyEntry) keyStore
|
||||
.getEntry(alias, new KeyStore.PasswordProtection(
|
||||
pfxPassword.toCharArray()));
|
||||
this.setPrivateKey(privateKeyEntry.getPrivateKey());
|
||||
this.setPublicKey(privateKeyEntry.getCertificate()
|
||||
.getPublicKey());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] data) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, this.getPublicKey());
|
||||
int inputLen = data.length;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int offSet = 0;
|
||||
byte[] cache;
|
||||
int i = 0;
|
||||
while (inputLen - offSet > 0) {
|
||||
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
|
||||
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
|
||||
} else {
|
||||
cache = cipher.doFinal(data, offSet, inputLen - offSet);
|
||||
}
|
||||
out.write(cache, 0, cache.length);
|
||||
i++;
|
||||
offSet = i * MAX_ENCRYPT_BLOCK;
|
||||
}
|
||||
byte[] encryptedData = out.toByteArray();
|
||||
out.close();
|
||||
return encryptedData;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] encryptedData) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, this.getPrivateKey());
|
||||
int inputLen = encryptedData.length;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
int offSet = 0;
|
||||
byte[] cache;
|
||||
int i = 0;
|
||||
while (inputLen - offSet > 0) {
|
||||
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
|
||||
cache = cipher.doFinal(encryptedData, offSet,
|
||||
MAX_DECRYPT_BLOCK);
|
||||
} else {
|
||||
cache = cipher.doFinal(encryptedData, offSet, inputLen
|
||||
- offSet);
|
||||
}
|
||||
out.write(cache, 0, cache.length);
|
||||
i++;
|
||||
offSet = i * MAX_DECRYPT_BLOCK;
|
||||
}
|
||||
byte[] decryptedData = out.toByteArray();
|
||||
out.close();
|
||||
return decryptedData;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package org.bench4q.master.helper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class HttpHelper {
|
||||
|
||||
public String get(String url, String accessToken) {
|
||||
try {
|
||||
URL target = new URL(url);
|
||||
HttpURLConnection httpURLConnection = (HttpURLConnection) target
|
||||
.openConnection();
|
||||
httpURLConnection.setDoOutput(false);
|
||||
httpURLConnection.setDoInput(true);
|
||||
httpURLConnection.setUseCaches(false);
|
||||
httpURLConnection.setRequestMethod("GET");
|
||||
if (accessToken != null) {
|
||||
httpURLConnection.setRequestProperty("Authorization", "Bearer "
|
||||
+ accessToken);
|
||||
}
|
||||
BufferedReader bufferedReader = new BufferedReader(
|
||||
new InputStreamReader(httpURLConnection.getInputStream()));
|
||||
int temp = -1;
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
while ((temp = bufferedReader.read()) != -1) {
|
||||
stringBuffer.append((char) temp);
|
||||
}
|
||||
bufferedReader.close();
|
||||
return stringBuffer.toString();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,21 @@
|
|||
package org.bench4q.master.test;
|
||||
|
||||
import org.bench4q.master.helper.HttpHelper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class AuthenticationTest {
|
||||
@Test
|
||||
public void testAuthenticationUser() {
|
||||
String accessToken = "077e228e31ee315ed9fb03d45be559a162aecc8a8c614d7538459af807387cc3d04b065d015864d91627c8c73858d734ab0eb24b3d673efd2f5f3aa518cb48b511279a712c85cf8b84dabb32be6060fce1f077ff12818af870315fa03dafcb7618d8e87f98f1971abe569303a40d59aa3803d95c6b5a7dc42a22afee8a169b455e65dd56f8e035dd2549d028ddf814009ae600788c19817ade14321c65cc9f64dfc38000e6087e6423843a9af0cb2d3120f680a6f6e441f8b244d592b7390063a86587570180b5d4689eecf8480cfa37214adfe9f95a2b4deae6a96a3f4a3542e04f32c0493e8b8b23fe5220bb3c01a726b5274884631df61824c47d77d6d70d666aeb043da4f6e6697d7492b8965ac91ceeeb5fc324152a1c70a53ca74834672d526618f6cea2c4b3945b3117d95836e7d52c1756e6ed302d6c1d048ff9834e01ffb84b9ff3f99cc6d140b0dfc342820c291b7d0469581a598d214b14028684b92a131d322d979848bad864d93f5329b526f64ddb27be18f322f9837b33fefca5b7a2215c93faff5d09b21863f1f79780f78e35da6479e9484805508f1ee030b73a7d93a7ed14ed76d582faa20147a6c34a586905f712a0c1ffa25367e275887da4a7b2d6224c497a26de485d569ec67fefb907753276e234bca389496122b556d206cdf7c7a7976ee2780cdb6cf514ef71e5a8b64ab5015ef8c75b2e30fd44";
|
||||
String actual = new HttpHelper().get("http://localhost:8080/",
|
||||
accessToken);
|
||||
Assert.assertEquals("Hello [Test]", actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticationAnonymousUser() {
|
||||
String actual = new HttpHelper().get("http://localhost:8080/", null);
|
||||
Assert.assertEquals("Hello [Anonymous User]", actual);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue