[ISSUE-205|247] Support SSL + Login: Polish with testcase, doc, session over cookie

This commit is contained in:
walking98
2019-04-16 20:14:27 +08:00
parent 8edf026461
commit 1588f70b52
20 changed files with 294 additions and 297 deletions

View File

@@ -33,7 +33,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Configuration
@@ -64,7 +64,7 @@ public class AuthWebMVCConfigurerAdapter extends WebMvcConfigurerAdapter {
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
UserInfo userInfo = (UserInfo) WebUtil.getAttribute((ServletRequest) nativeWebRequest.getNativeRequest(),
UserInfo userInfo = (UserInfo) WebUtil.getValueFromSession((HttpServletRequest) nativeWebRequest.getNativeRequest(),
UserInfo.USER_INFO);
if (userInfo != null) {
return userInfo;

View File

@@ -17,15 +17,12 @@
package org.apache.rocketmq.console.config;
import java.io.File;
import java.io.UnsupportedEncodingException;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.console.util.CipherHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.apache.rocketmq.client.ClientConfig.SEND_MESSAGE_WITH_VIP_CHANNEL_PROPERTY;
@@ -47,19 +44,8 @@ public class RMQConfigure {
private String msgTrackTopicName;
private String ciperKey = "rocketmqrocketmq";
private boolean loginRequired = false;
public String getCiperKey() {
return ciperKey;
}
public void setCiperKey(String ciperKey) {
this.ciperKey = ciperKey;
}
public String getNamesrvAddr() {
return namesrvAddr;
}
@@ -112,12 +98,6 @@ public class RMQConfigure {
this.msgTrackTopicName = msgTrackTopicName;
}
@Bean
public CipherHelper cipherHelper() throws UnsupportedEncodingException {
CipherHelper cipherHelper = new CipherHelper(getCiperKey());
return cipherHelper;
}
public boolean isLoginRequired() {
return loginRequired;
}

View File

@@ -17,9 +17,10 @@
package org.apache.rocketmq.console.controller;
import org.apache.rocketmq.console.config.RMQConfigure;
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.CipherHelper;
import org.apache.rocketmq.console.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,29 +31,33 @@ 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;
import java.util.Map;
@Controller
@RequestMapping("/login")
public class LoginController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private RMQConfigure configure;
@Autowired
private UserService userService;
@Autowired
private CipherHelper cipherHelper;
@RequestMapping(value = "/index", method = RequestMethod.GET)
public String index(Map<String, Object> map) {
return "login";
@RequestMapping(value = "/check.query", method = RequestMethod.GET)
@ResponseBody
public Object check(HttpServletRequest request) {
WebUtil.setSessionValue(request, WebUtil.NEED_LOGIN, configure.isLoginRequired());
return new Boolean(configure.isLoginRequired());
}
@RequestMapping(value = "/check", method = RequestMethod.POST)
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
@ResponseBody
public Object check(@RequestParam("username") String username,
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);
@@ -60,16 +65,19 @@ public class LoginController {
if (user == null) {
throw new IllegalArgumentException("Bad username or password!");
} else {
WebUtil.setCookie(response, WebUtil.LOGIN_TOKEN, cipherHelper.encrypt(username), false);
WebUtil.setCookie(response, WebUtil.USER_NAME, username, false);
user.setPassword(null);
UserInfo userInfo = WebUtil.setLoginInfo(request, response, user);
WebUtil.setSessionValue(request, WebUtil.USER_INFO, userInfo);
WebUtil.setSessionValue(request, WebUtil.USER_NAME, username);
return Boolean.TRUE;
}
}
@RequestMapping(value = "/logout", method = RequestMethod.POST)
@RequestMapping(value = "/logout.do", method = RequestMethod.POST)
@ResponseBody
public void logout(HttpServletResponse response) {
WebUtil.setCookie(response, WebUtil.LOGIN_TOKEN,"", true);
WebUtil.setCookie(response, WebUtil.USER_NAME, "", true);
public Object logout(HttpServletRequest request) {
WebUtil.removeSession(request);
return Boolean.TRUE;
}
}

View File

@@ -17,11 +17,7 @@
package org.apache.rocketmq.console.interceptor;
import org.apache.rocketmq.console.model.UserInfo;
import org.apache.rocketmq.console.service.LoginService;
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.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -31,8 +27,6 @@ import javax.servlet.http.HttpServletResponse;
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private LoginService loginService;
@@ -43,14 +37,6 @@ public class AuthInterceptor extends HandlerInterceptorAdapter {
if (!ok) {
return false;
}
UserInfo userInfo = loginService.parse(request, response);
if (userInfo.getUser() == null) {
logger.warn("cannot parse userinfo, userInfo:{}", userInfo);
WebUtil.print(response, "User does not exist or wrong password");
return false;
}
WebUtil.setAttribute(request, UserInfo.USER_INFO, userInfo);
return true;
}
}

View File

@@ -35,6 +35,10 @@ public class User {
this.type = type;
}
public User cloneOne() {
return new User(this.name, this.password, this.type);
}
public long getId() {
return id;
}

View File

@@ -17,13 +17,9 @@
package org.apache.rocketmq.console.service;
import org.apache.rocketmq.console.model.UserInfo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface LoginService {
String getLoginId(HttpServletRequest request);
boolean login(HttpServletRequest request, HttpServletResponse response);
UserInfo parse(HttpServletRequest request, HttpServletResponse response);
}

View File

@@ -17,12 +17,9 @@
package org.apache.rocketmq.console.service.impl;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.console.config.RMQConfigure;
import org.apache.rocketmq.console.model.UserInfo;
import org.apache.rocketmq.console.service.LoginService;
import org.apache.rocketmq.console.service.UserService;
import org.apache.rocketmq.console.util.CipherHelper;
import org.apache.rocketmq.console.util.WebUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,7 +31,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
@Service
@@ -44,34 +40,13 @@ public class LoginServiceImpl implements LoginService {
@Resource
private RMQConfigure rmqConfigure;
@Resource
private CipherHelper cipherHelper;
@Autowired
private UserService userService;
@Override
public String getLoginId(HttpServletRequest request) {
String loginToken = WebUtil.getLoginCookieValue(request);
if (loginToken != null) {
String userName = cipherHelper.decrypt(loginToken);
if (StringUtils.isNotBlank(loginToken)) {
WebUtil.setAttribute(request, "username", userName);
return userName;
}
}
return null;
}
private String getLoginId(String ticket) {
// You can extend this func to support external ticket
return null;
}
@Override
public boolean login(HttpServletRequest request, HttpServletResponse response) {
String username = getLoginId(request);
if (username != null) {
if (WebUtil.getValueFromSession(request, WebUtil.USER_NAME) != null) {
return true;
}
@@ -79,40 +54,6 @@ public class LoginServiceImpl implements LoginService {
return false;
}
@Override
public UserInfo parse(HttpServletRequest request, HttpServletResponse response) {
String ip = WebUtil.getIp(request);
UserInfo userInfo = new UserInfo();
userInfo.setIp(ip);
userInfo.setLoginTime(System.currentTimeMillis());
Object username = WebUtil.getAttribute(request, "username");
if (username == null) {
userInfo.setUser(null);
} else {
userInfo.setUser(userService.queryByName(username.toString()));
}
return userInfo;
}
private String parseRedirect(HttpServletRequest request) throws Exception {
try {
String redirect = request.getParameter("redirect");
String url = null;
if (redirect != null) {
url = URLDecoder.decode(redirect, "UTF-8");
}
if (url != null) {
logger.info("redirect to:" + url);
return url;
}
} catch (Exception e) {
logger.error("", e);
}
return "";
}
protected void auth(HttpServletRequest request, HttpServletResponse response) {
try {
String url = WebUtil.getUrl(request);

View File

@@ -18,6 +18,7 @@
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;
@@ -59,7 +60,7 @@ public class UserServiceImpl implements UserService, InitializingBean {
}
}
private static class FileBasedUserInfoStore {
/*packaged*/ static class FileBasedUserInfoStore {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private static final String FILE_NAME = "users.properties";
@@ -68,9 +69,14 @@ public class UserServiceImpl implements UserService, InitializingBean {
public FileBasedUserInfoStore(RMQConfigure configure) {
log.info("XXXXXX " + configure);
filePath = configure.getRocketMqConsoleDataPath() + File.separator + FILE_NAME;
if (!new File(filePath).exists()) {
//Use the default path
filePath = this.getClass().getResource("/" + FILE_NAME).getPath();
}
log.info(String.format("Login Users configure file is %s", filePath));
load();
watch();
}
private void load() {
@@ -79,7 +85,7 @@ public class UserServiceImpl implements UserService, InitializingBean {
try {
prop.load(new FileReader(filePath));
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to load loginUserInfo property file: %s", filePath));
throw new ServiceException(0, String.format("Failed to load loginUserInfo property file: %s", filePath));
}
Map<String, User> loadUserMap = new HashMap<>();
@@ -131,7 +137,7 @@ public class UserServiceImpl implements UserService, InitializingBean {
public User queryByUsernameAndPassword(@NotNull String username, @NotNull String password) {
User user = queryByName(username);
if (user != null && password.equals(user.getPassword())) {
return user;
return user.cloneOne();
}
return null;

View File

@@ -1,72 +0,0 @@
/*
* 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.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
public class CipherHelper {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String ALGORITHM = "AES";
private SecretKeySpec secretKeySpec;
public CipherHelper(String ciperKey) throws UnsupportedEncodingException {
secretKeySpec = new SecretKeySpec(ciperKey.getBytes("UTF-8"), ALGORITHM);
}
/**
* encrypt
* @param toBeEncrypt
* @return
*/
public String encrypt(String toBeEncrypt) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
byte[] encrypted = cipher.doFinal(toBeEncrypt.getBytes());
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
logger.error("encrypt:{} err", toBeEncrypt, e);
}
return null;
}
/**
* decrypt
* @param encrypted
* @return
*/
public String decrypt(String encrypted) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.decodeBase64(encrypted));
return new String(decryptedBytes);
} catch (Exception e) {
logger.error("decrypt:{} err", encrypted, e);
}
return null;
}
}

View File

@@ -18,17 +18,20 @@
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.Cookie;
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 LOGIN_TOKEN = "TOKEN";
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
@@ -66,48 +69,6 @@ public class WebUtil {
return addr;
}
/**
* Obtain cookie
*
* @param request
* @param name
* @return
*/
public static Cookie getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie;
}
}
}
return null;
}
public static void setCookie(HttpServletResponse response, String key, String value, boolean removeFlag) {
Cookie cookie = new Cookie(key, value);
cookie.setPath("/");
if (removeFlag) {
cookie.setMaxAge(0);
}
response.addCookie(cookie);
}
/**
* Obtain login cookie info
*
* @param request
* @return
*/
public static String getLoginCookieValue(HttpServletRequest request) {
Cookie cookie = getCookie(request, LOGIN_TOKEN);
if (cookie != null) {
return cookie.getValue();
}
return null;
}
public static void redirect(HttpServletResponse response, HttpServletRequest request, String path) throws IOException {
response.sendRedirect(request.getContextPath() + path);
}
@@ -127,26 +88,6 @@ public class WebUtil {
return url;
}
/**
* Fetch attribute form request
*
* @param request
* @return
*/
public static void setAttribute(ServletRequest request, String name, Object obj) {
request.setAttribute(name, obj);
}
/**
* Set attribute to request
*
* @param request
* @return
*/
public static Object getAttribute(ServletRequest request, String name) {
return request.getAttribute(name);
}
/**
* Write content to front-page/response
*
@@ -161,4 +102,35 @@ public class WebUtil {
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);
}
}

View File

@@ -26,4 +26,4 @@ rocketmq.config.msgTrackTopicName=
rocketmq.config.ticketKey=ticket
#Must create userInfo file: ${rocketmq.config.dataPath}/users.properties if the login is required
rocketmq.config.loginRequired=true
rocketmq.config.loginRequired=false

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,43 +31,60 @@ var app = angular.module('app', [
'localytics.directives',
'pascalprecht.translate'
]).run(
['$rootScope','$location','$cookies','$http',
function ($rootScope,$location,$cookies,$http) {
// 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();
// }
//TODO: make the session timeout consistent with backend
// var sessionId = $cookies.get("JSESSIONID");
// console.log("sessionId "+ sessionId);
//
// if (sessionId === undefined || sessionId == null) {
// $window.sessionStorage.clear();
// }
$rootScope.username = $cookies.get("username");
if (!angular.isDefined($rootScope.username)) {
$rootScope.username = '';
var url = '/login/check.query';
var setting = {
type: "GET",
timeout:15000,
success:callback,
async:false
}
//sync invoke
$.ajax(url,setting)
}
console.log("username " + $rootScope.username);
$rootScope.globals = $cookies.get('TOKEN');
console.log('TOKEN ' + $rootScope.globals);
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
var restrictedPage = $.inArray($location.path(), ['/login']) === -1;
var loggedIn = $rootScope.globals;
if (restrictedPage && (!angular.isDefined(loggedIn) || !loggedIn)) {
var callback = $location.path();
$location.path('/login');
init(function(resp){
if (resp.status == 0) {
// console.log('resp.data==='+resp.data);
loginFlag = resp.data;
}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');
}
}
});
@@ -94,6 +113,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 = [];
@@ -142,6 +174,9 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
}
});
// check login status
$httpProvider.defaults.cache = false;
$routeProvider.when('/', {

View File

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

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
app.controller('loginController', ['$scope','$location','$http','Notification','$cookies', function ($scope,$location,$http,Notification,$cookies) {
app.controller('loginController', ['$scope','$location','$http','Notification','$cookies','$window', function ($scope,$location,$http,Notification,$cookies, $window) {
$scope.login = function () {
if(!$("#username").val()) {
alert("用户名不能为空");
@@ -28,12 +28,14 @@ app.controller('loginController', ['$scope','$location','$http','Notification','
$http({
method: "POST",
url: "login/check",
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());
window.location = "/";
initFlag = false;
} else{
Notification.error({message: resp.errMsg, delay: 2000});
}

View File

@@ -37,8 +37,6 @@
</div>
<script>
$(function(){
console.log('aaaaa111');
//$("#loginModal").modal("hide");
console.log('aaaaa');
})
</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