[ISSUE-#205|#247] Support SSL + UserName & Password Login

[ISSUE-#205|#247] Support SSL + UserName & Password Login
This commit is contained in:
dinglei
2019-04-25 19:05:26 +08:00
committed by GitHub
30 changed files with 1234 additions and 36 deletions

View File

@@ -61,4 +61,55 @@
* 根据Topic和Key进行查询
* 最多只会展示64条
* 根据消息主题和消息Id进行消息的查询
* 消息详情可以展示这条消息的详细信息,查看消息对应到具体消费组的消费情况(如果异常,可以查看具体的异常信息)。可以向指定的消费组重发消息。
* 消息详情可以展示这条消息的详细信息,查看消息对应到具体消费组的消费情况(如果异常,可以查看具体的异常信息)。可以向指定的消费组重发消息。
## HTTPS 方式访问Console
* HTTPS功能实际上是使用SpringBoot提供的配置功能即可完成首先需要有一个SSL KeyStore来存放服务端证书可以使用本工程所提供的测试密钥库:
resources/rmqcngkeystore.jks, 它可以通过如下keytool命令生成
```
#生成库并以rmqcngKey别名添加秘钥
keytool -genkeypair -alias rmqcngKey -keyalg RSA -validity 3650 -keystore rmqcngkeystore.jks
#查看keystore内容
keytool -list -v -keystore rmqcngkeystore.jks
#转换库格式
keytool -importkeystore -srckeystore rmqcngkeystore.jks -destkeystore rmqcngkeystore.jks -deststoretype pkcs12
```
* 配置resources/application.properties, 打开SSL的相关选项, 启动console后即开启了HTTPS.
```
#设置https端口
server.port=8443
### SSL setting
#server.ssl.key-store=classpath:rmqcngkeystore.jks
#server.ssl.key-store-password=rocketmq
#server.ssl.keyStoreType=PKCS12
#server.ssl.keyAlias=rmqcngkey
```
## 登录访问Console
在访问Console时支持按用户名和密码登录控制台在操作完成后登出。需要做如下的设置:
* 1.在Spring配置文件resources/application.properties中修改 开启登录功能
```$xslt
# 开启登录功能
rocketmq.config.loginRequired=true
# Dashboard文件目录登录用户配置文件所在目录
rocketmq.config.dataPath=/tmp/rocketmq-console/data
```
* 2.确保${rocketmq.config.dataPath}定义的目录存在,并且该目录下创建登录配置文件"users.properties", 如果该目录下不存在此文件则默认使用resources/users.properties文件。
users.properties文件格式为:
```$xslt
# 该文件支持热修改即添加和修改用户时不需要重新启动console
# 格式, 每行定义一个用户, username=password[,N] #N是可选项可以为0 (普通用户) 1 (管理员)
#定义管理员
admin=admin,1
#定义普通用户
user1=user1
user2=user2
```
* 3. 启动控制台则开启了登录功能

View File

@@ -62,4 +62,57 @@
* Only Return 64 Messages
* Query By Topic And MessageId
* look over this message's detail info.you can see the message's consume state(each group has one line),show the exception message if has exception.
you can send this message to the group you selected
you can send this message to the group you selected
## Access Console with HTTPS
* SpringBoot itself has provided the SSL configuration. You can use the project test Keystore:resources/rmqcngkeystore.jks. The store is generated with the following unix keytool commands:
```
#Generate Keystore and add alias rmqcngKey
keytool -genkeypair -alias rmqcngKey -keyalg RSA -validity 3650 -keystore rmqcngkeystore.jks
#View keystore content
keytool -list -v -keystore rmqcngkeystore.jks
#Transfer type as official
keytool -importkeystore -srckeystore rmqcngkeystore.jks -destkeystore rmqcngkeystore.jks -deststoretype pkcs12
```
* Uncomment the following SSL properties in resources/application.properties. restart Console then access with HTTPS.
```
#Set https port
server.port=8443
### SSL setting
server.ssl.key-store=classpath:rmqcngkeystore.jks
server.ssl.key-store-password=rocketmq
server.ssl.keyStoreType=PKCS12
server.ssl.keyAlias=rmqcngkey
```
## Login/Logout on Console
Access Console with username and password and logout to leave the consoleTo stage the function on, we need the steps below:
* 1.Turn on the property in resources/application.properties.
```$xslt
# open the login func
rocketmq.config.loginRequired=true
# Directory of ashboard & login user configure file
rocketmq.config.dataPath=/tmp/rocketmq-console/data
```
* 2.Make sure the directory defined in property ${rocketmq.config.dataPath} exists and the file "users.properties" is created under it.
The console system will use the resources/users.properties by default if a customized file is not found。
The format in the content of users.properties:
```$xslt
# This file supports hot change, any change will be auto-reloaded without Console restarting.
# Format: a user per line, username=password[,N] #N is optional, 0 (Normal User); 1 (Admin)
# Define Admin
admin=admin,1
# Define Normal users
user1=user1
user2=user2
```
* 3. Restart Console Application after above configuration setting well.

View File

@@ -11,7 +11,7 @@
<groupId>org.apache</groupId>
<artifactId>rocketmq-console-ng</artifactId>
<packaging>jar</packaging>
<version>1.0.0</version>
<version>1.0.1</version>
<name>rocketmq-console-ng</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -0,0 +1,78 @@
/*
* 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.rocketmq.console.config;
import org.apache.rocketmq.console.interceptor.AuthInterceptor;
import org.apache.rocketmq.console.model.UserInfo;
import org.apache.rocketmq.console.util.WebUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Configuration
public class AuthWebMVCConfigurerAdapter extends WebMvcConfigurerAdapter {
@Autowired
@Qualifier("authInterceptor")
private AuthInterceptor authInterceptor;
@Resource
RMQConfigure configure;
@Override
public void addInterceptors(InterceptorRegistry registry) {
if (configure.isLoginRequired()) {
registry.addInterceptor(authInterceptor).excludePathPatterns("/error", "/user/guide/**", "/login/**");
}
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().isAssignableFrom(UserInfo.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
UserInfo userInfo = (UserInfo) WebUtil.getValueFromSession((HttpServletRequest) nativeWebRequest.getNativeRequest(),
UserInfo.USER_INFO);
if (userInfo != null) {
return userInfo;
}
throw new MissingServletRequestPartException(UserInfo.USER_INFO);
}
});
super.addArgumentResolvers(argumentResolvers); //REVIEW ME
}
}

View File

@@ -17,12 +17,18 @@
package org.apache.rocketmq.console.config;
import java.io.File;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.common.MixAll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ErrorPageRegistrar;
import org.springframework.boot.web.servlet.ErrorPageRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import static org.apache.rocketmq.client.ClientConfig.SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY;
@@ -37,12 +43,14 @@ public class RMQConfigure {
private volatile String isVIPChannel = System.getProperty(SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY, "true");
private String dataPath;
private String dataPath = "/tmp/rocketmq-console/data";
private boolean enableDashBoardCollect;
private String msgTrackTopicName;
private boolean loginRequired = false;
public String getNamesrvAddr() {
return namesrvAddr;
}
@@ -94,4 +102,27 @@ public class RMQConfigure {
public void setMsgTrackTopicName(String msgTrackTopicName) {
this.msgTrackTopicName = msgTrackTopicName;
}
public boolean isLoginRequired() {
return loginRequired;
}
public void setLoginRequired(boolean loginRequired) {
this.loginRequired = loginRequired;
}
// Error Page process logic, move to a central configure later
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
return new MyErrorPageRegistrar();
}
private static class MyErrorPageRegistrar implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404"));
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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.rocketmq.console.controller;
import org.apache.rocketmq.console.config.RMQConfigure;
import org.apache.rocketmq.console.model.LoginInfo;
import org.apache.rocketmq.console.model.User;
import org.apache.rocketmq.console.model.UserInfo;
import org.apache.rocketmq.console.service.UserService;
import org.apache.rocketmq.console.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
@RequestMapping("/login")
public class LoginController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private RMQConfigure configure;
@Autowired
private UserService userService;
@RequestMapping(value = "/check.query", method = RequestMethod.GET)
@ResponseBody
public Object check(HttpServletRequest request) {
LoginInfo loginInfo = new LoginInfo();
loginInfo.setLogined(WebUtil.getValueFromSession(request, WebUtil.USER_NAME) != null);
loginInfo.setLoginRequired(configure.isLoginRequired());
return loginInfo;
}
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody
public Object login(@RequestParam("username") String username,
@RequestParam(value = "password") String password,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
logger.info("user:{} login", username);
User user = userService.queryByUsernameAndPassword(username, password);
if (user == null) {
throw new IllegalArgumentException("Bad username or password!");
} else {
user.setPassword(null);
UserInfo userInfo = WebUtil.setLoginInfo(request, response, user);
WebUtil.setSessionValue(request, WebUtil.USER_INFO, userInfo);
WebUtil.setSessionValue(request, WebUtil.USER_NAME, username);
userInfo.setSessionId(WebUtil.getSessionId(request));
return userInfo;
}
}
@RequestMapping(value = "/logout.do", method = RequestMethod.POST)
@ResponseBody
public Object logout(HttpServletRequest request) {
WebUtil.removeSession(request);
return Boolean.TRUE;
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.rocketmq.console.interceptor;
import org.apache.rocketmq.console.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
boolean ok = loginService.login(request, response);
if (!ok) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.rocketmq.console.model;
public class LoginInfo {
private boolean loginRequired;
private boolean logined;
public boolean isLoginRequired() {
return loginRequired;
}
public void setLoginRequired(boolean loginRequired) {
this.loginRequired = loginRequired;
}
public boolean isLogined() {
return logined;
}
public void setLogined(boolean logined) {
this.logined = logined;
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.rocketmq.console.model;
import org.hibernate.validator.constraints.Range;
public class User {
public static final int ORDINARY = 0;
public static final int ADMIN = 1;
private long id;
private String name;
private String password;
@Range(min = 0, max = 1)
private int type = 0;
public User(String name, String password, int type) {
this.name = name;
this.password = password;
this.type = type;
}
public User cloneOne() {
return new User(this.name, this.password, this.type);
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
", type=" + type +
'}';
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.rocketmq.console.model;
public class UserInfo {
public static final String USER_INFO = "userInfo";
private User user;
private long loginTime;
private String ip;
private String sessionId;
public long getLoginTime() {
return loginTime;
}
public void setLoginTime(long loginTime) {
this.loginTime = loginTime;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
@Override
public String toString() {
return "UserInfo{" +
"user=" + user +
", loginTime=" + loginTime +
", ip='" + ip + '\'' +
", sessionId='" + sessionId + '\'' +
'}';
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.rocketmq.console.service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface LoginService {
boolean login(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -0,0 +1,25 @@
/*
* 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.rocketmq.console.service;
import org.apache.rocketmq.console.model.User;
public interface UserService {
User queryByName(String name);
User queryByUsernameAndPassword(String username, String password);
}

View File

@@ -147,7 +147,9 @@ public class DashboardCollectServiceImpl implements DashboardCollectService {
String dataLocationPath = rmqConfigure.getConsoleCollectData();
File file = new File(dataLocationPath + date + "_topic" + ".json");
if (!file.exists()) {
throw Throwables.propagate(new ServiceException(1, "This date have't data!"));
log.info(String.format("No dashboard data for data: %s", date));
//throw Throwables.propagate(new ServiceException(1, "This date have't data!"));
return Maps.newHashMap();
}
return jsonDataFile2map(file);
}

View File

@@ -0,0 +1,70 @@
/*
* 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.rocketmq.console.service.impl;
import org.apache.rocketmq.console.config.RMQConfigure;
import org.apache.rocketmq.console.service.LoginService;
import org.apache.rocketmq.console.service.UserService;
import org.apache.rocketmq.console.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Service
public class LoginServiceImpl implements LoginService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private RMQConfigure rmqConfigure;
@Autowired
private UserService userService;
@Override
public boolean login(HttpServletRequest request, HttpServletResponse response) {
if (WebUtil.getValueFromSession(request, WebUtil.USER_NAME) != null) {
return true;
}
auth(request, response);
return false;
}
protected void auth(HttpServletRequest request, HttpServletResponse response) {
try {
String url = WebUtil.getUrl(request);
try {
url = URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("url encode:{}", url, e);
}
WebUtil.redirect(response, request, "/#/login?redirect=" + url);
} catch (IOException e) {
logger.error("redirect err", e);
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.rocketmq.console.service.impl;
import org.apache.rocketmq.console.config.RMQConfigure;
import org.apache.rocketmq.console.exception.ServiceException;
import org.apache.rocketmq.console.model.User;
import org.apache.rocketmq.console.service.UserService;
import org.apache.rocketmq.srvutil.FileWatchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class UserServiceImpl implements UserService, InitializingBean {
@Resource
RMQConfigure configure;
FileBasedUserInfoStore fileBasedUserInfoStore;
@Override
public User queryByName(String name) {
return fileBasedUserInfoStore.queryByName(name);
}
@Override
public User queryByUsernameAndPassword(String username, String password) {
return fileBasedUserInfoStore.queryByUsernameAndPassword(username, password);
}
@Override
public void afterPropertiesSet() throws Exception {
if (configure.isEnableDashBoardCollect()) {
fileBasedUserInfoStore = new FileBasedUserInfoStore(configure);
}
}
/*packaged*/ static class FileBasedUserInfoStore {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private static final String FILE_NAME = "users.properties";
private String filePath;
private final Map<String, User> userMap = new ConcurrentHashMap<>();
public FileBasedUserInfoStore(RMQConfigure configure) {
filePath = configure.getRocketMqConsoleDataPath() + File.separator + FILE_NAME;
if (!new File(filePath).exists()) {
//Use the default path
InputStream inputStream = getClass().getResourceAsStream("/" + FILE_NAME);
if (inputStream == null) {
log.error(String.format("Can not found the file %s in Spring Boot jar", FILE_NAME));
System.out.printf(String.format("Can not found file %s in Spring Boot jar or %s, stop the console starting",
FILE_NAME, configure.getRocketMqConsoleDataPath()));
System.exit(1);
} else {
load(inputStream);
}
} else {
log.info(String.format("Login Users configure file is %s", filePath));
load();
watch();
}
}
private void load() {
load(null);
}
private void load(InputStream inputStream) {
Properties prop = new Properties();
try {
if (inputStream == null) {
prop.load(new FileReader(filePath));
} else {
prop.load(inputStream);
}
} catch (Exception e) {
log.error("load user.properties failed", e);
throw new ServiceException(0, String.format("Failed to load loginUserInfo property file: %s", filePath));
}
Map<String, User> loadUserMap = new HashMap<>();
String[] arrs;
int role;
for (String key : prop.stringPropertyNames()) {
String v = prop.getProperty(key);
if (v == null) continue;
arrs = v.split(",", 2);
if (arrs.length == 0) {
continue;
} else if (arrs.length == 1) {
role = 0;
} else {
role = Integer.parseInt(arrs[1].trim());
}
loadUserMap.put(key, new User(key, arrs[0].trim(), role));
}
userMap.clear();
userMap.putAll(loadUserMap);
}
private boolean watch() {
try {
FileWatchService fileWatchService = new FileWatchService(new String[]{filePath}, new FileWatchService.Listener() {
@Override
public void onChanged(String path) {
log.info("The loginUserInfo property file changed, reload the context");
load();
}
});
fileWatchService.start();
log.info("Succeed to start LoginUserWatcherService");
return true;
} catch (Exception e) {
log.error("Failed to start LoginUserWatcherService", e);
}
return false;
}
public User queryByName(String name) {
return userMap.get(name);
}
public User queryByUsernameAndPassword(@NotNull String username, @NotNull String password) {
User user = queryByName(username);
if (user != null && password.equals(user.getPassword())) {
return user.cloneOne();
}
return null;
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.rocketmq.console.util;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.console.model.User;
import org.apache.rocketmq.console.model.UserInfo;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
public class WebUtil {
public static final String USER_INFO = "userInfo";
public static final String USER_NAME = "username";
public static final String NEED_LOGIN = "needLogin";
/**
* Obtain ServletRequest header value
*
* @param request
* @param name
* @return
*/
public static String getHeaderValue(HttpServletRequest request, String name) {
String v = request.getHeader(name);
if (v == null) {
return null;
}
return v.trim();
}
/**
* Fetch request ip address
*
* @param request
* @return
*/
public static String getIp(ServletRequest request) {
HttpServletRequest req = (HttpServletRequest) request;
String addr = getHeaderValue(req, "X-Forwarded-For");
if (StringUtils.isNotEmpty(addr) && addr.contains(",")) {
addr = addr.split(",")[0];
}
if (StringUtils.isEmpty(addr)) {
addr = getHeaderValue(req, "X-Real-IP");
}
if (StringUtils.isEmpty(addr)) {
addr = req.getRemoteAddr();
}
return addr;
}
public static void redirect(HttpServletResponse response, HttpServletRequest request, String path) throws IOException {
response.sendRedirect(request.getContextPath() + path);
}
/**
* Obtain the full url path
*
* @param request
* @return
*/
public static String getUrl(HttpServletRequest request) {
String url = request.getRequestURL().toString();
String queryString = request.getQueryString();
if (queryString != null) {
url += "?" + request.getQueryString();
}
return url;
}
/**
* Write content to front-page/response
*
* @param response
* @param result
* @throws IOException
*/
public static void print(HttpServletResponse response, String result) throws IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(result);
out.flush();
out.close();
}
public static Object getValueFromSession(HttpServletRequest request, String key) {
HttpSession session = request.getSession(false);
if (session != null) {
return session.getAttribute(key);
}
return null;
}
public static UserInfo setLoginInfo(HttpServletRequest request, HttpServletResponse response, User user) {
String ip = WebUtil.getIp(request);
UserInfo userInfo = new UserInfo();
userInfo.setIp(ip);
userInfo.setLoginTime(System.currentTimeMillis());
userInfo.setUser(user);
return userInfo;
}
public static void removeSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.invalidate();
}
public static void setSessionValue(HttpServletRequest request, String key, Object value) {
HttpSession session = request.getSession();
session.setAttribute(key, value);
}
public static String getSessionId(HttpServletRequest request) {
return request.getSession().getId();
}
}

View File

@@ -1,5 +1,12 @@
server.contextPath=
server.port=8080
### SSL setting
#server.ssl.key-store=classpath:rmqcngkeystore.jks
#server.ssl.key-store-password=rocketmq
#server.ssl.keyStoreType=PKCS12
#server.ssl.keyAlias=rmqcngkey
#spring.application.index=true
spring.application.name=rocketmq-console
spring.http.encoding.charset=UTF-8
@@ -15,4 +22,8 @@ rocketmq.config.dataPath=/tmp/rocketmq-console/data
#set it false if you don't want use dashboard.default true
rocketmq.config.enableDashBoardCollect=true
#set the message track trace topic if you don't want use the default one
rocketmq.config.msgTrackTopicName=
rocketmq.config.msgTrackTopicName=
rocketmq.config.ticketKey=ticket
#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required
rocketmq.config.loginRequired=false

Binary file not shown.

View File

@@ -113,6 +113,6 @@
<script type="text/javascript" src="src/ops.js?timestamp=7"></script>
<script type="text/javascript" src="src/remoteApi/remoteApi.js"></script>
<script type="text/javascript" src="vendor/preLoading/main.js"></script>
<script type="text/javascript" src="src/login.js"></script>
</body>
</html>

View File

@@ -15,6 +15,8 @@
* limitations under the License.
*/
'use strict';
var initFlag = false;
var loginFlag = false;
var app = angular.module('app', [
'ngAnimate',
'ngCookies',
@@ -29,27 +31,57 @@ var app = angular.module('app', [
'localytics.directives',
'pascalprecht.translate'
]).run(
['$rootScope','$location','$cookies',
function ($rootScope,$location,$cookies) {
// var filter = function(url){
// var outFilterArrs = []
// outFilterArrs.push("/login");
// outFilterArrs.push("/reg");
// outFilterArrs.push("/logout");
// outFilterArrs.push("/404");
// var flag = false;
// $.each(outFilterArrs,function(i,value){
// if(url.indexOf(value) > -1){
// flag = true;
// return false;
// }
// });
// return flag;
// }
['$rootScope','$location','$cookies','$http', '$window','Notification',
function ($rootScope,$location,$cookies,$http, $window, Notification) {
var init = function(callback){
if (initFlag) return;
initFlag = true;
// if(angular.isDefined($cookies.get("isLogin")) && $cookies.get("isLogin") == 'true'){
// chatApi.login();
// }
var url = '/login/check.query';
var setting = {
type: "GET",
timeout:15000,
success:callback,
async:false
}
//sync invoke
$.ajax(url,setting)
}
console.log('initFlag0='+ initFlag + ' loginFlag0==='+loginFlag);
$rootScope.$on('$locationChangeStart', function (event, next, current) {
// redirect to login page if not logged in and trying to access a restricted page
init(function(resp){
if (resp.status == 0) {
// console.log('resp.data==='+resp.data);
var loginInfo = resp.data;
loginFlag = loginInfo.loginRequired;
if (!loginInfo.logined) {
$window.sessionStorage.clear();
}
}else {
Notification.error({message: "" + resp.errMsg, delay: 2000});
}
});
console.log('initFlag='+ initFlag + ' loginFlag==='+loginFlag);
$rootScope.username = '';
if (loginFlag || loginFlag == "true") {
var username = $window.sessionStorage.getItem("username");
if (username != null) {
$rootScope.username = username;
}
// console.log("username " + $rootScope.username);
var restrictedPage = $.inArray($location.path(), ['/login']) === -1;
if (restrictedPage && !username) {
var callback = $location.path();
$location.path('/login');
}
}
});
$rootScope.$on('$routeChangeSuccess', function() {
@@ -77,6 +109,19 @@ var app = angular.module('app', [
}
});
app.factory('abc', function ($http, $window) {
console.log('xxxxxxx');
$http({
method: "GET",
url: "/login/check.query"
}).success(function (resp) {
if (resp.status == 0) {
alert(resp.data)
}
});
return 1;
});
app.provider('getDictName', function () {
var dictList = [];
@@ -125,11 +170,17 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
}
});
// check login status
$httpProvider.defaults.cache = false;
$routeProvider.when('/', {
templateUrl: 'view/pages/index.html',
controller:'dashboardCtrl'
}).when('/login', {
templateUrl: 'view/pages/login.html',
controller:'loginController'
}).when('/cluster', {
templateUrl: 'view/pages/cluster.html',
controller:'clusterController'
@@ -152,8 +203,8 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
templateUrl: 'view/pages/ops.html',
controller:'opsController'
}).when('/404', {
templateUrl: '404'
}).otherwise('404');
templateUrl: 'view/pages/404.html'
}).otherwise('/404');
$translateProvider.translations('en',en);
$translateProvider.translations('zh',zh);

View File

@@ -14,10 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
app.controller('AppCtrl', ['$scope','$rootScope','$cookies','$location','$translate', function ($scope,$rootScope,$cookies,$location,$translate) {
app.controller('AppCtrl', ['$scope','$window','$translate','$http','Notification', function ($scope,$window,$translate, $http, Notification) {
$scope.changeTranslate = function(langKey){
$translate.use(langKey);
}
$scope.logout = function(){
$http({
method: "POST",
url: "login/logout.do"
}).success(function (resp) {
window.location = "/";
$window.sessionStorage.clear();
});
}
}]);
app.controller('dashboardCtrl', ['$scope','$rootScope','$translate','$filter','Notification','remoteApi','tools', function ($scope,$rootScope,$translate,$filter,Notification,remoteApi,tools) {

View File

@@ -1,5 +1,5 @@
var en = {
"TITLE": "RocketMq-Console-Ng",
"TITLE": "RocketMQ-Console",
"CLOSE": "Close",
"NO": "NO.",
"ADDRESS": "Address",
@@ -77,5 +77,10 @@ var en = {
"CLUSTER_NAME":"clusterName",
"OPS":"OPS",
"AUTO_REFRESH":"AUTO_REFRESH",
"REFRESH":"REFRESH"
"REFRESH":"REFRESH",
"LOGOUT":"Logout",
"LOGIN":"Login",
"USER_NAME":"Username",
"PASSWORD":"Password",
"WELCOME":"Hi, welcome using RocketMQ Console"
}

View File

@@ -1,5 +1,5 @@
var zh = {
"TITLE": "RocketMq控制台",
"TITLE": "RocketMQ控制台",
"CLOSE": "关闭",
"NO": "编号",
"ADDRESS": "地址",
@@ -77,5 +77,10 @@ var zh = {
"CLUSTER_NAME":"集群名",
"OPS":"运维",
"AUTO_REFRESH":"自动刷新",
"REFRESH":"刷新"
"REFRESH":"刷新",
"LOGOUT":"退出",
"LOGIN":"登录",
"USER_NAME":"用户名",
"PASSWORD":"密码",
"WELCOME":"您好欢迎使用RocketMQ控制台"
}

View File

@@ -0,0 +1,46 @@
/*
* 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.
*/
app.controller('loginController', ['$scope','$location','$http','Notification','$cookies','$window', function ($scope,$location,$http,Notification,$cookies, $window) {
$scope.login = function () {
if(!$("#username").val()) {
alert("用户名不能为空");
return;
}
if(!$("#password").val()) {
alert("密码不能为空");
return;
}
$http({
method: "POST",
url: "login/login.do",
params:{username:$("#username").val(), password:$("#password").val()}
}).success(function (resp) {
if (resp.status == 0) {
Notification.info({message: 'Login successful, redirect now', delay: 2000});
$window.sessionStorage.setItem("username", $("#username").val());
//alert("XXXXX resp.data="+resp.data.sessionId);
//$window.sessionStorage.setItem("sessionId", resp.data.sessionId);
window.location = "/";
initFlag = false;
} else{
Notification.error({message: resp.errMsg, delay: 2000});
}
});
};
}]);

View File

@@ -28,7 +28,27 @@
<li><a href="javascript:void(0)" ng-click="changeTranslate('zh')">Simplified Chinese</a></li>
</ul>
</li>
<li class="dropdown" ng-show="username != ''">
<a href="bootstrap-elements.html" data-target="#" class="dropdown-toggle" data-toggle="dropdown">{{username}}
<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="javascript:void(0)" ng-click="logout()">{{'LOGOUT' | translate}}</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var app = angular.module('DemoApp',[]);
app.controller('DemoController',function($scope){
$scope.IsVisible = false;
$scope.ShowHide = function(){
$scope.IsVisible = $scope.IsVisible = true;
}
});
</script>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,42 @@
<div id="loginModal" class="page-content" id="deployHistoryList" data-width="400" data-backdrop="static" role="main" >
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{'WELCOME' | translate}}</h4>
</div>
<form class="form-horizontal form-bordered form-row-stripped" id="loginForm">
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<div class="form-body">
<div class="form-group">
<label class="control-label col-md-4">{{'USER_NAME' | translate}}: </label>
<div class="col-md-5">
<input type="text" id="username" name="username" placeholder="{{'USER_NAME' | translate}}" class="form-control" ng-model="filterStr"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-4"> {{'PASSWORD' | translate}}: </label>
<div class="col-md-5">
<input type="password" name="password" id="password"
value="" placeholder="{{'USER_NAME' | translate}}"
class="form-control" />
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="modal-footer">
<button class="btn btn-raised btn-sm btn-primary" type="button" ng-click="login()">{{'LOGIN' | translate}}</button>
</div>
</div>
</div>
</div>
<script>
$(function(){
//$("#loginModal").modal("hide");
})
</script>

View File

@@ -0,0 +1,9 @@
# This file supports hot change, any change will be auto-reloaded without Console restarting.
# Format: a user per line, username=password[,N] #N is optional, 0 (Normal User); 1 (Admin)
# Define Admin
admin=admin,1
# Define Users
user1=user1
user2=user2

View File

@@ -0,0 +1,26 @@
package org.apache.rocketmq.console.service.impl;
import org.apache.rocketmq.console.config.RMQConfigure;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class LoginFileTest {
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testLoad() throws Exception {
RMQConfigure configure = new RMQConfigure();
configure.setDataPath(this.getClass().getResource("/").getPath());
UserServiceImpl.FileBasedUserInfoStore fileBasedUserInfoStore = new UserServiceImpl.FileBasedUserInfoStore(configure);
Assert.assertTrue("No exception raise for FileBasedUserInfoStore", true);
}
}

View File

@@ -0,0 +1,2 @@
admin=admin
test=test