Browse Source

support spring security auth

lepdou 7 years ago
parent
commit
b1d0745059
27 changed files with 916 additions and 29 deletions
  1. 1 0
      apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/BizDBPropertySource.java
  2. 1 0
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/auth/WebSecurityConfig.java
  3. 1 1
      apollo-common/src/main/java/com/ctrip/framework/apollo/common/config/RefreshableConfig.java
  4. 1 3
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java
  5. 0 1
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AppController.java
  6. 20 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SignInController.java
  7. 25 1
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/UserInfoController.java
  8. 9 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java
  9. 68 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/UserPO.java
  10. 17 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/UserRepository.java
  11. 99 6
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java
  12. 0 5
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultUserService.java
  13. 18 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java
  14. 89 0
      apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserService.java
  15. 2 1
      apollo-portal/src/main/resources/static/index.html
  16. 336 0
      apollo-portal/src/main/resources/static/login.html
  17. 4 1
      apollo-portal/src/main/resources/static/scripts/app.js
  18. 16 0
      apollo-portal/src/main/resources/static/scripts/controller/LoginController.js
  19. 17 0
      apollo-portal/src/main/resources/static/scripts/controller/UserController.js
  20. 15 0
      apollo-portal/src/main/resources/static/scripts/services/UserService.js
  21. 95 0
      apollo-portal/src/main/resources/static/user-manage.html
  22. 5 6
      apollo-portal/src/main/resources/static/views/common/nav.html
  23. 2 1
      apollo-portal/src/main/scripts/startup.sh
  24. 5 0
      apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/FavoriteServiceTest.java
  25. 1 1
      scripts/build.sh
  26. 35 1
      scripts/sql-docker/apolloportaldb.sql
  27. 34 1
      scripts/sql/apolloportaldb.sql

+ 1 - 0
apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/BizDBPropertySource.java

@@ -85,6 +85,7 @@ public class BizDBPropertySource extends RefreshablePropertySource {
       this.source.put(key, value);
 
     }
+
   }
 
 }

+ 1 - 0
apollo-common/src/main/java/com/ctrip/framework/apollo/common/auth/WebSecurityConfig.java

@@ -25,4 +25,5 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
     auth.inMemoryAuthentication().withUser("user").password("").roles("USER").and()
         .withUser("apollo").password("").roles("USER", "ADMIN");
   }
+
 }

+ 1 - 1
apollo-common/src/main/java/com/ctrip/framework/apollo/common/config/RefreshableConfig.java

@@ -1,11 +1,11 @@
 package com.ctrip.framework.apollo.common.config;
 
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 
 import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
 import com.ctrip.framework.apollo.tracer.Tracer;
 
-import com.google.common.base.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;

+ 1 - 3
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java

@@ -29,11 +29,9 @@ public class PortalConfig extends RefreshableConfig {
   private static final Type ORGANIZATION = new TypeToken<List<Organization>>() {
   }.getType();
 
-
   @Autowired
   private PortalDBPropertySource portalDBPropertySource;
 
-
   @Override
   public List<RefreshablePropertySource> getRefreshablePropertySources() {
     return Collections.singletonList(portalDBPropertySource);
@@ -102,7 +100,7 @@ public class PortalConfig extends RefreshableConfig {
 
     String[] emergencyPublishSupportedEnvs = getArrayProperty("emergencyPublish.supported.envs", new String[0]);
 
-    for (String supportedEnv: emergencyPublishSupportedEnvs) {
+    for (String supportedEnv : emergencyPublishSupportedEnvs) {
       if (Objects.equals(targetEnv, supportedEnv.toUpperCase().trim())) {
         return true;
       }

+ 0 - 1
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/AppController.java

@@ -56,7 +56,6 @@ public class AppController {
   @Autowired
   private RolePermissionService rolePermissionService;
 
-
   @RequestMapping(value = "", method = RequestMethod.GET)
   public List<App> findApps(@RequestParam(value = "appIds", required = false) String appIds) {
     if (StringUtils.isEmpty(appIds)) {

+ 20 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/SignInController.java

@@ -0,0 +1,20 @@
+package com.ctrip.framework.apollo.portal.controller;
+
+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;
+
+/**
+ * @author lepdou 2017-08-30
+ */
+@Controller
+public class SignInController {
+
+  @RequestMapping(value = "/signin", method = RequestMethod.GET)
+  public String login(@RequestParam(value = "error", required = false) String error,
+                      @RequestParam(value = "logout", required = false) String logout) {
+    return "login.html";
+  }
+
+}

+ 25 - 1
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/UserInfoController.java

@@ -1,12 +1,18 @@
 package com.ctrip.framework.apollo.portal.controller;
 
+import com.ctrip.framework.apollo.common.exception.BadRequestException;
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
 import com.ctrip.framework.apollo.portal.spi.LogoutHandler;
 import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
-import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
 import com.ctrip.framework.apollo.portal.spi.UserService;
+import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.userdetails.User;
 import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
@@ -29,6 +35,22 @@ public class UserInfoController {
   @Autowired
   private UserService userService;
 
+
+  @PreAuthorize(value = "@permissionValidator.isSuperAdmin()")
+  @RequestMapping(value = "/users", method = RequestMethod.POST)
+  public void createOrUpdateUser(@RequestBody User user) {
+    if (StringUtils.isContainEmpty(user.getUsername(), user.getPassword())) {
+      throw new BadRequestException("Username and password can not be empty.");
+    }
+
+    if (userService instanceof SpringSecurityUserService) {
+      ((SpringSecurityUserService) userService).createOrUpdate(user);
+    } else {
+      throw new UnsupportedOperationException("Create or update user operation is unsupported");
+    }
+
+  }
+
   @RequestMapping(value = "/user", method = RequestMethod.GET)
   public UserInfo getCurrentUserName() {
     return userInfoHolder.getUser();
@@ -50,4 +72,6 @@ public class UserInfoController {
   public UserInfo getUserByUserId(@PathVariable String userId) {
     return userService.findByUserId(userId);
   }
+
+
 }

+ 9 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java

@@ -1,11 +1,20 @@
 package com.ctrip.framework.apollo.portal.entity.bo;
 
 public class UserInfo {
+  public static final UserInfo DEFAULT_USER = new UserInfo("apollo");
 
   private String userId;
   private String name;
   private String email;
 
+  public UserInfo() {
+
+  }
+
+  public UserInfo(String userId) {
+    this.userId = userId;
+  }
+
   public String getUserId() {
     return userId;
   }

+ 68 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/po/UserPO.java

@@ -0,0 +1,68 @@
+package com.ctrip.framework.apollo.portal.entity.po;
+
+import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * @author lepdou 2017-04-08
+ */
+@Entity
+@Table(name = "users")
+public class UserPO {
+
+  @Id
+  @GeneratedValue
+  @Column(name = "Id")
+  private long id;
+  @Column(name = "username", nullable = false)
+  private String username;
+  @Column(name = "password", nullable = false)
+  private String password;
+  @Column(name = "enabled", nullable = false)
+  private int enabled;
+
+  public long getId() {
+    return id;
+  }
+
+  public void setId(long id) {
+    this.id = id;
+  }
+
+  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 int getEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(int enabled) {
+    this.enabled = enabled;
+  }
+
+  public UserInfo toUserInfo() {
+    UserInfo userInfo = new UserInfo();
+    userInfo.setName(this.getUsername());
+    userInfo.setUserId(this.getUsername());
+    userInfo.setEmail("apollo@acme.com");
+    return userInfo;
+  }
+}

+ 17 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/repository/UserRepository.java

@@ -0,0 +1,17 @@
+package com.ctrip.framework.apollo.portal.repository;
+
+import com.ctrip.framework.apollo.portal.entity.po.UserPO;
+
+import org.springframework.data.repository.PagingAndSortingRepository;
+
+import java.util.List;
+
+/**
+ * @author lepdou 2017-04-08
+ */
+public interface UserRepository extends PagingAndSortingRepository<UserPO, Long> {
+
+  List<UserPO> findByUsernameLike(String username);
+
+  UserPO findByUsername(String username);
+}

+ 99 - 6
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java

@@ -1,5 +1,8 @@
 package com.ctrip.framework.apollo.portal.spi.configuration;
 
+import com.google.common.collect.Maps;
+
+import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile;
 import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
 import com.ctrip.framework.apollo.portal.spi.LogoutHandler;
 import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler;
@@ -13,7 +16,10 @@ import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultLogoutHandler;
 import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultSsoHeartbeatHandler;
 import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserInfoHolder;
 import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserService;
-import com.google.common.collect.Maps;
+import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
+import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.context.embedded.FilterRegistrationBean;
@@ -21,6 +27,16 @@ import org.springframework.boot.context.embedded.ServletListenerRegistrationBean
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.JdbcUserDetailsManager;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
 
 import javax.servlet.Filter;
 import java.util.EventListener;
@@ -170,10 +186,89 @@ public class AuthConfiguration {
 
 
   /**
-   * spring.profiles.active != ctrip
+   * spring.profiles.active = auth
    */
   @Configuration
-  @Profile({"!ctrip"})
+  @Profile("auth")
+  static class SpringSecurityAuthAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean(SsoHeartbeatHandler.class)
+    public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
+      return new DefaultSsoHeartbeatHandler();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(UserInfoHolder.class)
+    public UserInfoHolder springSecurityUserInfoHolder() {
+      return new SpringSecurityUserInfoHolder();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(LogoutHandler.class)
+    public LogoutHandler logoutHandler() {
+      return new DefaultLogoutHandler();
+    }
+
+    @Bean
+    public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource datasource) {
+      JdbcUserDetailsManager userDetailsService = new JdbcUserDetailsManager();
+      userDetailsService.setDataSource(datasource);
+
+      return userDetailsService;
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(UserService.class)
+    public UserService springSecurityUserService() {
+      return new SpringSecurityUserService();
+    }
+
+
+    @Order(99)
+    @Configuration
+    @Profile("auth")
+    @EnableWebSecurity
+    @EnableGlobalMethodSecurity(prePostEnabled = true)
+    static class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {
+
+      public static final String USER_ROLE = "user";
+
+      @Autowired
+      private DataSource datasource;
+      
+
+      @Override
+      protected void configure(HttpSecurity http) throws Exception {
+        http.csrf().disable();
+        http.headers().frameOptions().sameOrigin();
+        http.authorizeRequests()
+            .antMatchers("/openapi/*").permitAll()
+            .antMatchers("/*").hasAnyRole(USER_ROLE);
+        http.formLogin().loginPage("/signin").permitAll().failureUrl("/signin?#/error").and().httpBasic();
+        http.logout().invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/signin?#/logout");
+        http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin"));
+      }
+
+      @Autowired
+      public void configureGlobal(AuthenticationManagerBuilder auth, JdbcUserDetailsManager userDetailsService)
+          throws Exception {
+        PasswordEncoder encoder = new BCryptPasswordEncoder();
+
+        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
+        auth.jdbcAuthentication().dataSource(datasource).usersByUsernameQuery(
+            "select username,password, enabled from users where username=?");
+      }
+
+    }
+
+  }
+
+  /**
+   * default profile
+   */
+  @Configuration
+  @ConditionalOnMissingProfile({"ctrip", "auth"})
   static class DefaultAuthAutoConfiguration {
 
     @Bean
@@ -184,7 +279,7 @@ public class AuthConfiguration {
 
     @Bean
     @ConditionalOnMissingBean(UserInfoHolder.class)
-    public DefaultUserInfoHolder notCtripUserInfoHolder() {
+    public DefaultUserInfoHolder defaultUserInfoHolder() {
       return new DefaultUserInfoHolder();
     }
 
@@ -199,8 +294,6 @@ public class AuthConfiguration {
     public UserService defaultUserService() {
       return new DefaultUserService();
     }
-
   }
 
-
 }

+ 0 - 5
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultUserService.java

@@ -5,13 +5,8 @@ import com.google.common.collect.Lists;
 import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
 import com.ctrip.framework.apollo.portal.spi.UserService;
 
-import org.springframework.util.CollectionUtils;
-
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 /**
  * @author Jason Song(song_s@ctrip.com)

+ 18 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java

@@ -0,0 +1,18 @@
+package com.ctrip.framework.apollo.portal.spi.springsecurity;
+
+import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
+import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+
+public class SpringSecurityUserInfoHolder implements UserInfoHolder {
+
+  @Override
+  public UserInfo getUser() {
+    UserInfo userInfo = new UserInfo();
+    String userId = ((User) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername();
+    userInfo.setUserId(userId);
+    return userInfo;
+  }
+}

+ 89 - 0
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserService.java

@@ -0,0 +1,89 @@
+package com.ctrip.framework.apollo.portal.spi.springsecurity;
+
+import com.google.common.collect.Lists;
+
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+import com.ctrip.framework.apollo.portal.entity.bo.UserInfo;
+import com.ctrip.framework.apollo.portal.entity.po.UserPO;
+import com.ctrip.framework.apollo.portal.repository.UserRepository;
+import com.ctrip.framework.apollo.portal.spi.UserService;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.JdbcUserDetailsManager;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * @author lepdou 2017-03-10
+ */
+public class SpringSecurityUserService implements UserService {
+
+  private PasswordEncoder encoder = new BCryptPasswordEncoder();
+  private List<GrantedAuthority> authorities;
+
+  @Autowired
+  private JdbcUserDetailsManager userDetailsManager;
+  @Autowired
+  private UserRepository userRepository;
+
+  @PostConstruct
+  public void init() {
+    authorities = new ArrayList<>();
+    authorities.add(new SimpleGrantedAuthority("ROLE_user"));
+  }
+
+  public void createOrUpdate(User user) {
+    String username = user.getUsername();
+
+    User userDetails = new User(username, encoder.encode(user.getPassword()), authorities);
+
+    if (userDetailsManager.userExists(username)) {
+      userDetailsManager.updateUser(userDetails);
+    } else {
+      userDetailsManager.createUser(userDetails);
+    }
+
+  }
+
+  @Override
+  public List<UserInfo> searchUsers(String keyword, int offset, int limit) {
+    if (StringUtils.isEmpty(keyword)) {
+      return Collections.emptyList();
+    }
+
+    List<UserPO> userPOs = userRepository.findByUsernameLike("%" + keyword + "%");
+
+    List<UserInfo> result = Lists.newArrayList();
+    if (CollectionUtils.isEmpty(userPOs)) {
+      return result;
+    }
+
+    result.addAll(userPOs.stream().map(UserPO::toUserInfo).collect(Collectors.toList()));
+
+    return result;
+  }
+
+  @Override
+  public UserInfo findByUserId(String userId) {
+    UserPO userPO = userRepository.findByUsername(userId);
+    return userPO == null ? null : userPO.toUserInfo();
+  }
+
+  @Override
+  public List<UserInfo> findByUserIds(List<String> userIds) {
+    return null;
+  }
+
+
+}

+ 2 - 1
apollo-portal/src/main/resources/static/index.html

@@ -30,7 +30,8 @@
                     <h5>创建项目</h5>
                 </div>
             </div>
-            <div class="app-panel col-md-2 text-center" ng-repeat="app in createdApps" ng-click="goToAppHomePage(app.appId)">
+            <div class="app-panel col-md-2 text-center" ng-repeat="app in createdApps"
+                 ng-click="goToAppHomePage(app.appId)">
                 <div href="#" class="thumbnail hover cursor-pointer">
                     <h4 ng-bind="app.appId"></h4>
                     <h5 ng-bind="app.name"></h5>

+ 336 - 0
apollo-portal/src/main/resources/static/login.html

@@ -0,0 +1,336 @@
+<!DOCTYPE html>
+<html lang="en" ng-app="login">
+<head>
+    <meta charset="UTF-8">
+    <title>Apollo配置中心</title>
+    <link rel="icon" href="./img/config.png">
+    <link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" href="vendor/font-awesome.min.css">
+    <style type="text/css">
+        @import url(https://fonts.googleapis.com/css?family=Roboto:400,300,100,700,500);
+
+        body {
+            padding-top: 90px;
+            background: #F7F7F7;
+            color: #666666;
+            font-family: 'Roboto', sans-serif;
+            font-weight: 100;
+        }
+
+        body {
+            width: 100%;
+            background: -webkit-linear-gradient(left, #22d686, #24d3d3, #22d686, #24d3d3);
+            background: linear-gradient(to right, #22d686, #24d3d3, #22d686, #24d3d3);
+            background-size: 600% 100%;
+            -webkit-animation: HeroBG 20s ease infinite;
+            animation: HeroBG 20s ease infinite;
+        }
+
+        @-webkit-keyframes HeroBG {
+            0% {
+                background-position: 0 0;
+            }
+            50% {
+                background-position: 100% 0;
+            }
+            100% {
+                background-position: 0 0;
+            }
+        }
+
+        @keyframes HeroBG {
+            0% {
+                background-position: 0 0;
+            }
+            50% {
+                background-position: 100% 0;
+            }
+            100% {
+                background-position: 0 0;
+            }
+        }
+
+        .panel {
+            border-radius: 5px;
+        }
+
+        label {
+            font-weight: 300;
+        }
+
+        .panel-login {
+            border: none;
+            -webkit-box-shadow: 0px 0px 49px 14px rgba(188, 190, 194, 0.39);
+            -moz-box-shadow: 0px 0px 49px 14px rgba(188, 190, 194, 0.39);
+            box-shadow: 0px 0px 49px 14px rgba(188, 190, 194, 0.39);
+        }
+
+        .panel-login .checkbox input[type=checkbox] {
+            margin-left: 0px;
+        }
+
+        .panel-login .checkbox label {
+            padding-left: 25px;
+            font-weight: 300;
+            display: inline-block;
+            position: relative;
+        }
+
+        .panel-login .checkbox {
+            padding-left: 20px;
+        }
+
+        .panel-login .checkbox label::before {
+            content: "";
+            display: inline-block;
+            position: absolute;
+            width: 17px;
+            height: 17px;
+            left: 0;
+            margin-left: 0px;
+            border: 1px solid #cccccc;
+            border-radius: 3px;
+            background-color: #fff;
+            -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out;
+            -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out;
+            transition: border 0.15s ease-in-out, color 0.15s ease-in-out;
+        }
+
+        .panel-login .checkbox label::after {
+            display: inline-block;
+            position: absolute;
+            width: 16px;
+            height: 16px;
+            left: 0;
+            top: 0;
+            margin-left: 0px;
+            padding-left: 3px;
+            padding-top: 1px;
+            font-size: 11px;
+            color: #555555;
+        }
+
+        .panel-login .checkbox input[type="checkbox"] {
+            opacity: 0;
+        }
+
+        .panel-login .checkbox input[type="checkbox"]:focus + label::before {
+            outline: thin dotted;
+            outline: 5px auto -webkit-focus-ring-color;
+            outline-offset: -2px;
+        }
+
+        .panel-login .checkbox input[type="checkbox"]:checked + label::after {
+            font-family: 'FontAwesome';
+            content: "\f00c";
+        }
+
+        .panel-login > .panel-heading .tabs {
+            padding: 0;
+        }
+
+        .panel-login h2 {
+            font-size: 20px;
+            font-weight: 300;
+            margin: 30px;
+        }
+
+        .panel-login > .panel-heading {
+            color: #848c9d;
+            background-color: #e8e9ec;
+            border-color: #fff;
+            text-align: center;
+            border-bottom-left-radius: 5px;
+            border-bottom-right-radius: 5px;
+            border-top-left-radius: 0px;
+            border-top-right-radius: 0px;
+            border-bottom: 0px;
+            padding: 0px 15px;
+        }
+
+        .panel-login .form-group {
+            padding: 0 30px;
+        }
+
+        .panel-login > .panel-heading .login {
+            padding: 20px 30px;
+            border-bottom-leftt-radius: 5px;
+        }
+
+        .panel-login > .panel-heading .register {
+            padding: 20px 30px;
+            background: #2d3b55;
+            border-bottom-right-radius: 5px;
+        }
+
+        .panel-login > .panel-heading a {
+            text-decoration: none;
+            color: #666;
+            font-weight: 300;
+            font-size: 16px;
+            -webkit-transition: all 0.1s linear;
+            -moz-transition: all 0.1s linear;
+            transition: all 0.1s linear;
+        }
+
+        .panel-login > .panel-heading a#register-form-link {
+            color: #fff;
+            width: 100%;
+            text-align: right;
+        }
+
+        .panel-login > .panel-heading a#login-form-link {
+            width: 100%;
+            text-align: left;
+        }
+
+        .panel-login input[type="text"], .panel-login input[type="email"], .panel-login input[type="password"] {
+            height: 45px;
+            border: 0;
+            font-size: 16px;
+            -webkit-transition: all 0.1s linear;
+            -moz-transition: all 0.1s linear;
+            transition: all 0.1s linear;
+            -webkit-box-shadow: none;
+            box-shadow: none;
+            border-bottom: 1px solid #e7e7e7;
+            border-radius: 0px;
+            padding: 6px 0px;
+        }
+
+        .panel-login input:hover,
+        .panel-login input:focus {
+            outline: none;
+            -webkit-box-shadow: none;
+            -moz-box-shadow: none;
+            box-shadow: none;
+            border-color: #ccc;
+        }
+
+        .btn-login {
+            background-color: #E8E9EC;
+            outline: none;
+            color: #2D3B55;
+            font-size: 14px;
+            height: auto;
+            font-weight: normal;
+            padding: 14px 0;
+            text-transform: uppercase;
+            border: none;
+            border-radius: 0px;
+            box-shadow: none;
+        }
+
+        .btn-login:hover,
+        .btn-login:focus {
+            color: #fff;
+            background-color: #2D3B55;
+        }
+
+        .forgot-password {
+            text-decoration: underline;
+            color: #888;
+        }
+
+        .forgot-password:hover,
+        .forgot-password:focus {
+            text-decoration: underline;
+            color: #666;
+        }
+
+        .btn-register {
+            background-color: #E8E9EC;
+            outline: none;
+            color: #2D3B55;
+            font-size: 14px;
+            height: auto;
+            font-weight: normal;
+            padding: 14px 0;
+            text-transform: uppercase;
+            border: none;
+            border-radius: 0px;
+            box-shadow: none;
+        }
+
+        .btn-register:hover,
+        .btn-register:focus {
+            color: #fff;
+            background-color: #2D3B55;
+        }
+
+    </style>
+</head>
+<body>
+
+<div class="container" ng-controller="LoginController">
+    <div class="row">
+        <div class="col-md-6 col-md-offset-3">
+            <div class="panel panel-login">
+                <div class="panel-body">
+                    <div class="row">
+                        <div class="col-lg-12">
+                            <form id="login-form" action="/signin" method="post" role="form" style="display: block;">
+                                <h2 class="text-center">Apollo配置中心</h2>
+
+                                <div class="form-group">
+                                    <input type="text" name="username" tabindex="1" class="form-control"
+                                           placeholder="Username" value="">
+                                </div>
+                                <div class="form-group">
+                                    <input type="password" name="password" tabindex="2"
+                                           class="form-control" placeholder="Password">
+                                </div>
+                                <div class="form-group" style="color: red">
+                                    <small ng-bind="info"></small>
+                                </div>
+                                <div class="col-xs-12 form-group pull-right">
+                                    <input type="submit" name="login-submit" id="login-submit" tabindex="4"
+                                           class="form-control btn btn-login" value="登录">
+                                </div>
+                            </form>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- jquery.js -->
+<script src="vendor/jquery.min.js" type="text/javascript"></script>
+<!-- bootstrap.js -->
+<script src="vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
+
+<!--angular-->
+<script src="vendor/angular/angular.min.js"></script>
+<script src="vendor/angular/angular-resource.min.js"></script>
+<script src="vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
+<script src="vendor/angular/loading-bar.min.js"></script>
+
+<script type="application/javascript" src="scripts/app.js"></script>
+<script type="application/javascript" src="scripts/AppUtils.js"></script>
+<script type="application/javascript" src="scripts/directive/directive.js"></script>
+<script type="application/javascript" src="scripts/controller/LoginController.js"></script>
+
+
+<script type="application/javascript">
+    $(function () {
+        $('#login-form-link').click(function (e) {
+            $("#login-form").delay(100).fadeIn(100);
+            $("#register-form").fadeOut(100);
+            $('#register-form-link').removeClass('active');
+            $(this).addClass('active');
+            e.preventDefault();
+        });
+        $('#register-form-link').click(function (e) {
+            $("#register-form").delay(100).fadeIn(100);
+            $("#login-form").fadeOut(100);
+            $('#login-form-link').removeClass('active');
+            $(this).addClass('active');
+            e.preventDefault();
+        });
+
+    });
+
+</script>
+</body>
+</html>

+ 4 - 1
apollo-portal/src/main/resources/static/scripts/app.js

@@ -30,7 +30,10 @@ var cluster_module = angular.module('cluster', ['app.service', 'apollo.directive
 var release_history_module = angular.module('release_history', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
 //open manage
 var open_manage_module = angular.module('open_manage', ['app.service', 'apollo.directive', 'app.util', 'toastr', 'angular-loading-bar']);
-
+//user
+var user_module = angular.module('user', ['apollo.directive', 'toastr', 'app.service', 'app.util', 'angular-loading-bar', 'valdr']);
+//login
+var login_module = angular.module('login', ['toastr', 'app.util']);
 
 
 

+ 16 - 0
apollo-portal/src/main/resources/static/scripts/controller/LoginController.js

@@ -0,0 +1,16 @@
+login_module.controller('LoginController',
+                       ['$scope', '$window', '$location', 'toastr', 'AppUtil',
+                        LoginController]);
+
+function LoginController($scope, $window, $location, toastr, AppUtil) {
+    if ($location.$$url) {
+        var params = AppUtil.parseParams($location.$$url);
+        if (params.error) {
+            $scope.info = "用户名或密码错误";
+        }
+        if (params.logout) {
+            $scope.info = "登出成功";
+        }
+    }
+
+}

+ 17 - 0
apollo-portal/src/main/resources/static/scripts/controller/UserController.js

@@ -0,0 +1,17 @@
+user_module.controller('UserController',
+                      ['$scope', '$window', 'toastr', 'AppUtil', 'UserService',
+                       UserController]);
+
+function UserController($scope, $window, toastr, AppUtil, UserService) {
+
+    $scope.user = {};
+    
+    $scope.createOrUpdateUser = function () {
+        UserService.createOrUpdateUser($scope.user).then(function (result) {
+            toastr.success("创建用户成功");
+        }, function (result) {
+            AppUtil.showErrorMsg(result, "创建用户失败");
+        })
+
+    }
+}

+ 15 - 0
apollo-portal/src/main/resources/static/scripts/services/UserService.js

@@ -7,6 +7,10 @@ appService.service('UserService', ['$resource', '$q', function ($resource, $q) {
         find_users: {
             method: 'GET',
             url: '/users'
+        },
+        create_or_update_user: {
+            method: 'POST',
+            url: '/users'
         }
     });
     return {
@@ -36,6 +40,17 @@ appService.service('UserService', ['$resource', '$q', function ($resource, $q) {
                                          d.reject(result);
                                      });
             return d.promise;
+        },
+        createOrUpdateUser: function (user) {
+            var d = $q.defer();
+            user_resource.create_or_update_user({}, user,
+                                     function (result) {
+                                         d.resolve(result);
+                                     },
+                                     function (result) {
+                                         d.reject(result);
+                                     });
+            return d.promise;   
         }
     }
 }]);

+ 95 - 0
apollo-portal/src/main/resources/static/user-manage.html

@@ -0,0 +1,95 @@
+<!doctype html>
+<html ng-app="user">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <link rel="icon" href="./img/config.png">
+    <!-- styles -->
+    <link rel="stylesheet" type="text/css" href="vendor/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" type="text/css" href="vendor/angular/angular-toastr-1.4.1.min.css">
+    <link rel="stylesheet" type="text/css" href="vendor/select2/select2.min.css">
+    <link rel="stylesheet" type="text/css" media='all' href="vendor/angular/loading-bar.min.css">
+    <link rel="stylesheet" type="text/css" href="styles/common-style.css">
+
+    <title>用户管理</title>
+</head>
+
+<body>
+
+<apollonav></apollonav>
+<div class="container-fluid apollo-container">
+
+    <div class="row">
+        <div class="col-md-8 col-md-offset-2">
+            <div class="panel">
+                <header class="panel-heading">
+                    用户管理
+                </header>
+
+                <form class="form-horizontal panel-body" name="appForm" ng-controller="UserController"
+                      valdr-type="App"
+                      ng-submit="createOrUpdateUser()">
+                    <div class="form-group" valdr-form-group>
+                        <label class="col-sm-2 control-label">
+                            <apollorequiredfield></apollorequiredfield>
+                            用户名</label>
+                        <div class="col-sm-4">
+                            <input type="text" class="form-control" name="username" ng-model="user.username">
+                        </div>
+                    </div>
+                    <div class="form-group" valdr-form-group>
+                        <label class="col-sm-2 control-label">
+                            <apollorequiredfield></apollorequiredfield>
+                            密码</label>
+                        <div class="col-sm-4">
+                            <input type="text" class="form-control" name="password" ng-model="user.password">
+                        </div>
+                    </div>
+
+                    <div class="form-group">
+                        <div class="col-sm-offset-2 col-sm-9">
+
+                            <button type="submit" class="btn btn-primary"
+                                    ng-disabled="appForm.$invalid || submitBtnDisabled">提交
+                            </button>
+                        </div>
+                    </div>
+                </form>
+
+            </div>
+        </div>
+    </div>
+</div>
+
+<div ng-include="'views/common/footer.html'"></div>
+
+<!--angular-->
+<script src="vendor/angular/angular.min.js"></script>
+<script src="vendor/angular/angular-resource.min.js"></script>
+<script src="vendor/angular/angular-toastr-1.4.1.tpls.min.js"></script>
+<script src="vendor/angular/loading-bar.min.js"></script>
+
+<!-- jquery.js -->
+<script src="vendor/jquery.min.js" type="text/javascript"></script>
+<script src="vendor/select2/select2.min.js" type="text/javascript"></script>
+
+<!-- bootstrap.js -->
+<script src="vendor/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
+
+<!--valdr-->
+<script src="vendor/valdr/valdr.min.js" type="text/javascript"></script>
+<script src="vendor/valdr/valdr-message.min.js" type="text/javascript"></script>
+
+<script type="application/javascript" src="scripts/app.js"></script>
+<script type="application/javascript" src="scripts/services/AppService.js"></script>
+<script type="application/javascript" src="scripts/services/EnvService.js"></script>
+<script type="application/javascript" src="scripts/services/UserService.js"></script>
+<script type="application/javascript" src="scripts/services/CommonService.js"></script>
+<script type="application/javascript" src="scripts/AppUtils.js"></script>
+<script type="application/javascript" src="scripts/services/OrganizationService.js"></script>
+<script type="application/javascript" src="scripts/directive/directive.js"></script>
+
+<script type="application/javascript" src="scripts/controller/UserController.js"></script>
+
+<script src="scripts/valdr.js" type="text/javascript"></script>
+</body>
+</html>

+ 5 - 6
apollo-portal/src/main/resources/static/views/common/nav.html

@@ -25,16 +25,15 @@
                         <span class="glyphicon glyphicon-question-sign"></span> 帮助
                     </a>
                 </li>
-
                 <li class="dropdown">
-
-                    <a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
-                       aria-expanded="false"><span class="glyphicon glyphicon-user"></span> {{userName}} <span
-                            class="caret"></span></a>
+                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
+                        <span class="glyphicon glyphicon-user"></span>&nbsp;{{userName}}
+                        <span class="caret"></span></a>
                     <ul class="dropdown-menu">
-                        <li><a href="/user/logout">退出</a></li>
+                        <li><a href="/logout">退出</a></li>
                     </ul>
                 </li>
+
             </ul>
 
             <div class="navbar-form navbar-right form-inline" role="search">

+ 2 - 1
apollo-portal/src/main/scripts/startup.sh

@@ -18,9 +18,10 @@ import javax.annotation.PostConstruct;
 public abstract class AbstractIntegrationTest {
 
   RestTemplate restTemplate = new TestRestTemplate("apollo", "");
-
+ 
   @PostConstruct
   private void postConstruct() {
+    System.setProperty("spring.profiles.active", "test");
     restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
   }
 

+ 5 - 0
apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/FavoriteServiceTest.java

@@ -6,6 +6,7 @@ import com.ctrip.framework.apollo.portal.entity.po.Favorite;
 import com.ctrip.framework.apollo.portal.repository.FavoriteRepository;
 
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.PageRequest;
@@ -22,6 +23,10 @@ public class FavoriteServiceTest extends AbstractIntegrationTest {
 
   private String testUser = "apollo";
 
+  @Before
+  public void before() {
+
+  }
 
   @Test
   @Sql(scripts = "/sql/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)

+ 1 - 1
scripts/build.sh

@@ -30,7 +30,7 @@ echo "==== building config-service and admin-service finished ===="
 
 echo "==== starting to build portal ===="
 
-mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github -Dspring_datasource_url=$apollo_portal_db_url -Dspring_datasource_username=$apollo_portal_db_username -Dspring_datasource_password=$apollo_portal_db_password $META_SERVERS_OPTS
+mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github,auth -Dspring_datasource_url=$apollo_portal_db_url -Dspring_datasource_username=$apollo_portal_db_username -Dspring_datasource_password=$apollo_portal_db_password $META_SERVERS_OPTS
 
 echo "==== building portal finished ===="
 

+ 35 - 1
scripts/sql-docker/apolloportaldb.sql

@@ -275,16 +275,50 @@ CREATE TABLE `UserRole` (
   KEY `IX_UserId_RoleId` (`UserId`,`RoleId`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和role的绑定表';
 
+
+# Dump of table users
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `users`;
+
+CREATE TABLE `users` (
+  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
+  `username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名',
+  `password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
+  `enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效',
+  PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
+
+
+# Dump of table authorities
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `authorities`;
+
+CREATE TABLE `authorities` (
+  `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
+  `username` varchar(50) NOT NULL,
+  `authority` varchar(50) NOT NULL,
+  PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+
 # Config
 # ------------------------------------------------------------
 INSERT INTO `ServerConfig` (`Key`, `Value`, `Comment`)
 VALUES
     ('apollo.portal.envs', 'dev', '可支持的环境列表'),
     ('organizations', '[{\"orgId\":\"全辅导\",\"orgName\":\"全辅导\"},{\"orgId\":\"全课云\",\"orgName\":\"全课云\"}]', '部门列表'),
-    ('superAdmin', 'apollo', 'Portal超级管理员'),
+    ('superAdmin', 'admin', 'Portal超级管理员'),
     ('api.readTimeout', '10000', 'http接口read timeout'),
     ('consumer.token.salt', 'someSalt', 'consumer token salt');
 
+INSERT INTO `users` ( `username`, `password`, `enabled`)
+VALUES
+	('admin', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 1);
+
+INSERT INTO `authorities` (`username`, `authority`) VALUES ('admin', 'ROLE_user');
+
 /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;

+ 34 - 1
scripts/sql/apolloportaldb.sql

@@ -275,16 +275,49 @@ CREATE TABLE `UserRole` (
   KEY `IX_UserId_RoleId` (`UserId`,`RoleId`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和role的绑定表';
 
+# Dump of table users
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `users`;
+
+CREATE TABLE `users` (
+  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
+  `username` varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名',
+  `password` varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码',
+  `enabled` tinyint(4) DEFAULT NULL COMMENT '是否有效',
+  PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
+
+
+# Dump of table authorities
+# ------------------------------------------------------------
+
+DROP TABLE IF EXISTS `authorities`;
+
+CREATE TABLE `authorities` (
+  `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id',
+  `username` varchar(50) NOT NULL,
+  `authority` varchar(50) NOT NULL,
+  PRIMARY KEY (`Id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+
 # Config
 # ------------------------------------------------------------
 INSERT INTO `ServerConfig` (`Key`, `Value`, `Comment`)
 VALUES
     ('apollo.portal.envs', 'dev', '可支持的环境列表'),
     ('organizations', '[{\"orgId\":\"TEST1\",\"orgName\":\"样例部门1\"},{\"orgId\":\"TEST2\",\"orgName\":\"样例部门2\"}]', '部门列表'),
-    ('superAdmin', 'apollo', 'Portal超级管理员'),
+    ('superAdmin', 'admin', 'Portal超级管理员'),
     ('api.readTimeout', '10000', 'http接口read timeout'),
     ('consumer.token.salt', 'someSalt', 'consumer token salt');
 
+INSERT INTO `users` ( `username`, `password`, `enabled`)
+VALUES
+	('admin', '$2a$10$7r20uS.BQ9uBpf3Baj3uQOZvMVvB1RN3PYoKE94gtz2.WAOuiiwXS', 1);
+
+INSERT INTO `authorities` (`username`, `authority`) VALUES ('admin', 'ROLE_user');
+
 /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;