Browse Source

1.添加SpringSecurity及其相关配置;
2.添加用户注册登录页面;
3.添加拦截器;
4.添加处理事件;
5.添加用户角色实体类及其服务类;
6.添加header和footer模板及layout模板。

Yumin 6 years ago
parent
commit
63d8b23c30
33 changed files with 1093 additions and 65 deletions
  1. 19 0
      pom.xml
  2. 36 0
      src/main/java/cn/minbb/edu/config/AppInterceptor.java
  3. 38 0
      src/main/java/cn/minbb/edu/config/WebMvcConfig.java
  4. 98 0
      src/main/java/cn/minbb/edu/config/WebSecurityConfig.java
  5. 9 4
      src/main/java/cn/minbb/edu/controller/web/MainController.java
  6. 8 0
      src/main/java/cn/minbb/edu/data/ResponseResult.java
  7. 24 0
      src/main/java/cn/minbb/edu/handler/AccessDenied.java
  8. 34 0
      src/main/java/cn/minbb/edu/handler/AppExceptionHandler.java
  9. 48 0
      src/main/java/cn/minbb/edu/handler/LoginAuthenticationFailureHandler.java
  10. 60 0
      src/main/java/cn/minbb/edu/handler/LoginAuthenticationSuccessHandler.java
  11. 19 0
      src/main/java/cn/minbb/edu/handler/LoginUrlEntryPoint.java
  12. 47 0
      src/main/java/cn/minbb/edu/handler/LogoutSuccessHandler.java
  13. 63 14
      src/main/java/cn/minbb/edu/model/User.java
  14. 56 0
      src/main/java/cn/minbb/edu/model/UserRole.java
  15. 14 0
      src/main/java/cn/minbb/edu/model/repository/UserRoleRepository.java
  16. 15 0
      src/main/java/cn/minbb/edu/service/UserRoleService.java
  17. 2 1
      src/main/java/cn/minbb/edu/service/UserService.java
  18. 5 4
      src/main/java/cn/minbb/edu/service/impl/CourseServiceImpl.java
  19. 5 4
      src/main/java/cn/minbb/edu/service/impl/HomeworkServiceImpl.java
  20. 42 0
      src/main/java/cn/minbb/edu/service/impl/UserRoleServiceImpl.java
  21. 39 0
      src/main/java/cn/minbb/edu/service/impl/UserServiceImpl.java
  22. 74 0
      src/main/java/cn/minbb/edu/system/UserSession.java
  23. 35 0
      src/main/java/cn/minbb/edu/task/InitDataRunner.java
  24. 16 0
      src/main/java/cn/minbb/edu/util/RequestUtil.java
  25. 3 1
      src/main/resources/application.properties
  26. 60 0
      src/main/resources/static/css/user-sign.css
  27. BIN
      src/main/resources/static/favicon.ico
  28. 17 0
      src/main/resources/templates/fragments/footer.html
  29. 61 0
      src/main/resources/templates/fragments/header.html
  30. 8 3
      src/main/resources/templates/index.html
  31. 44 0
      src/main/resources/templates/layouts/layout.html
  32. 47 34
      src/main/resources/templates/sign-in.html
  33. 47 0
      src/main/resources/templates/sign-up.html

+ 19 - 0
pom.xml

@@ -50,6 +50,25 @@
             <artifactId>spring-boot-starter-thymeleaf</artifactId>
         </dependency>
 
+        <!-- 避免 layout:fragment 失效 -->
+        <dependency>
+            <groupId>nz.net.ultraq.thymeleaf</groupId>
+            <artifactId>thymeleaf-layout-dialect</artifactId>
+            <version>2.4.1</version>
+        </dependency>
+
+        <!-- Spring Security -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <!-- MySQL 数据库依赖 -->
         <dependency>
             <groupId>mysql</groupId>

+ 36 - 0
src/main/java/cn/minbb/edu/config/AppInterceptor.java

@@ -0,0 +1,36 @@
+package cn.minbb.edu.config;
+
+import cn.minbb.edu.model.User;
+import cn.minbb.edu.system.UserSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+// 自定义拦截器
+public class AppInterceptor extends HandlerInterceptorAdapter {
+
+    private Logger logger = LoggerFactory.getLogger(AppInterceptor.class);
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        return super.preHandle(request, response, handler);
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+        super.postHandle(request, response, handler, modelAndView);
+        if (null != modelAndView) {
+            User user = UserSession.getUserAuthentication();
+            if (null != user) modelAndView.addObject("user", user);
+        }
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        super.afterCompletion(request, response, handler, ex);
+    }
+}

+ 38 - 0
src/main/java/cn/minbb/edu/config/WebMvcConfig.java

@@ -0,0 +1,38 @@
+package cn.minbb.edu.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 多个拦截器组成一个拦截器链
+        registry.addInterceptor(appInterceptor());
+    }
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry) {
+        // 和页面有关的静态目录都放在项目的 static 目录下
+        registry.addResourceHandler("/**")
+                .addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/")
+                .setCachePeriod(0);
+    }
+
+    @Override
+    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
+    }
+
+    // 自定义拦截器
+    @Bean
+    public AppInterceptor appInterceptor() {
+        return new AppInterceptor();
+    }
+}

+ 98 - 0
src/main/java/cn/minbb/edu/config/WebSecurityConfig.java

@@ -0,0 +1,98 @@
+package cn.minbb.edu.config;
+
+import cn.minbb.edu.handler.*;
+import cn.minbb.edu.model.repository.UserRepository;
+import cn.minbb.edu.service.impl.UserServiceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+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.core.session.SessionRegistryImpl;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
+
+    private UserRepository userRepository;
+    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;
+    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
+    private LogoutSuccessHandler logoutSuccessHandler;
+
+    @Autowired
+    public WebSecurityConfig(
+            UserRepository userRepository,
+            LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler,
+            LoginAuthenticationFailureHandler loginAuthenticationFailureHandler,
+            LogoutSuccessHandler logoutSuccessHandler) {
+        this.userRepository = userRepository;
+        this.loginAuthenticationSuccessHandler = loginAuthenticationSuccessHandler;
+        this.loginAuthenticationFailureHandler = loginAuthenticationFailureHandler;
+        this.logoutSuccessHandler = logoutSuccessHandler;
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http.exceptionHandling()
+                .authenticationEntryPoint(new LoginUrlEntryPoint())
+                .accessDeniedHandler(new AccessDenied())
+                .and().csrf().disable().authorizeRequests()
+                .antMatchers("/user/**").access("hasRole('USER')")
+                .antMatchers("/admin/**").access("hasRole('USER') and  hasRole('ADMIN')")
+                .antMatchers("/swagger-ui.html").access("hasRole('ADMIN')")
+                .antMatchers("/css/**", "/js/**", "/images/**", "/fonts/*").permitAll()
+                .and().formLogin().loginPage("/sign-in").loginProcessingUrl("/sign-in")
+                // 用户名字段和密码字段
+                .usernameParameter("username").passwordParameter("password")
+                // 登陆成功后跳转的请求和验证失败后跳转的请求
+                .defaultSuccessUrl("/admin").failureUrl("/sign-in?error=true").permitAll()
+                .successHandler(loginAuthenticationSuccessHandler)
+                .failureHandler(loginAuthenticationFailureHandler)
+                .and().logout().logoutUrl("/sign-out").logoutSuccessUrl("").logoutSuccessHandler(logoutSuccessHandler)
+                // 使 Session 失效
+                .invalidateHttpSession(true)
+                // 清除认证信息
+                .clearAuthentication(true).permitAll()
+                // 开启 cookie 存储用户信息
+                .and().rememberMe().rememberMeParameter("remember").rememberMeCookieName("remember")
+                // cookie 有效期为两个星期(秒)
+                .tokenValiditySeconds(1209600)
+                .and().authorizeRequests()
+                .anyRequest().permitAll()
+                // Session 管理
+                .and().sessionManagement()
+                // 系统中同一个账号的登陆数量限制
+                .maximumSessions(1).sessionRegistry(new SessionRegistryImpl())
+                .and().and().headers().frameOptions().disable()
+                .and().authorizeRequests();
+    }
+
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth.userDetailsService(new UserServiceImpl(userRepository)).passwordEncoder(new BCryptPasswordEncoder() {
+            @Override
+            public String encode(CharSequence rawPassword) {
+                logger.info("加密时待加密的密码 1 = {}", rawPassword);
+                logger.info("加密原始密码 2 = {}", new BCryptPasswordEncoder().encode(rawPassword));
+                return new BCryptPasswordEncoder().encode(rawPassword);
+            }
+
+            @Override
+            public boolean matches(CharSequence rawPassword, String encodedPassword) {
+                logger.info("用户输入密码 = {}", rawPassword);
+                logger.info("数据库密码 = {}", encodedPassword);
+                logger.info("用户输入密码加密 = {}", new BCryptPasswordEncoder().encode(rawPassword));
+                if (!encodedPassword.contentEquals(rawPassword))
+                    throw new BadCredentialsException("密码错误!");
+                return true;
+            }
+        });
+    }
+}

+ 9 - 4
src/main/java/cn/minbb/edu/controller/web/MainController.java

@@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.servlet.ModelAndView;
 
 import javax.servlet.http.HttpServletRequest;
@@ -22,9 +21,15 @@ public class MainController {
     }
 
     @GetMapping(value = "")
-    @ResponseBody
-    public String main() {
-        return "EDUServer Running...";
+    public ModelAndView indexPage(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("index");
+        return modelAndView;
+    }
+
+    @GetMapping(value = {"register", "sign-up"})
+    public ModelAndView signUp(ModelAndView modelAndView, HttpServletRequest request) {
+        modelAndView.setViewName("sign-up");
+        return modelAndView;
     }
 
     @GetMapping(value = {"login", "sign-in"})

+ 8 - 0
src/main/java/cn/minbb/edu/data/ResponseResult.java

@@ -1,11 +1,13 @@
 package cn.minbb.edu.data;
 
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 import java.util.Collection;
 
 @Data
+@NoArgsConstructor
 public class ResponseResult<T extends Serializable> implements Serializable {
 
     private Integer code = -1;
@@ -17,4 +19,10 @@ public class ResponseResult<T extends Serializable> implements Serializable {
     private T data;
 
     private Collection<T> dataset;
+
+    public ResponseResult(Integer code, Boolean success, String message) {
+        this.code = code;
+        this.success = success;
+        this.message = message;
+    }
 }

+ 24 - 0
src/main/java/cn/minbb/edu/handler/AccessDenied.java

@@ -0,0 +1,24 @@
+package cn.minbb.edu.handler;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 当一个已授权(或已登陆)的用户请求访问他权限之外的资源时, handle 方法将会被调用
+ */
+public class AccessDenied implements AccessDeniedHandler {
+
+    @Override
+    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
+        httpServletRequest.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, new Exception("访问权限不足"));
+        httpServletResponse.setStatus(HttpStatus.OK.value());
+        httpServletResponse.sendRedirect("/sign-in?error=true");
+    }
+}

+ 34 - 0
src/main/java/cn/minbb/edu/handler/AppExceptionHandler.java

@@ -0,0 +1,34 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.util.RequestUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RestControllerAdvice
+public class AppExceptionHandler {
+
+    private Logger logger = LoggerFactory.getLogger(AppExceptionHandler.class);
+
+    @ExceptionHandler(value = Exception.class)
+    public Object errorHandler(HttpServletRequest request, HttpServletResponse response, Exception e) throws Exception {
+        e.printStackTrace();
+        String error = "全局捕获异常\n" + e.getMessage();
+        logger.error(error);
+        if (RequestUtil.isAjax(request)) {
+            return new ResponseResult(-1, false, e.getMessage());
+        } else {
+            ModelAndView modelAndView = new ModelAndView();
+            modelAndView.addObject("exception", e);
+            modelAndView.addObject("url", request.getRequestURL());
+            modelAndView.setViewName("app-error");
+            return modelAndView;
+        }
+    }
+}

+ 48 - 0
src/main/java/cn/minbb/edu/handler/LoginAuthenticationFailureHandler.java

@@ -0,0 +1,48 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.data.ResponseResult;
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.WebAttributes;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class LoginAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LoginAuthenticationFailureHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LoginAuthenticationFailureHandler(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
+        String message = objectMapper.writeValueAsString(exception.getMessage());
+        logger.info("用户登录失败 = {}", message);
+        if (RequestUtil.isAjax(request)) {
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write(objectMapper.writeValueAsString(
+                    new ResponseResult(-1, false, exception.getMessage())));
+        } else {
+            // 异常写入 Session
+            request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
+            response.setStatus(HttpStatus.OK.value());
+            response.sendRedirect("/sign-in?error=true");
+        }
+    }
+}

+ 60 - 0
src/main/java/cn/minbb/edu/handler/LoginAuthenticationSuccessHandler.java

@@ -0,0 +1,60 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.SavedRequest;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class LoginAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LoginAuthenticationSuccessHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LoginAuthenticationSuccessHandler(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    // Authentication 封装认证信息
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
+        String auth = objectMapper.writeValueAsString(authentication);
+        logger.info("用户登陆成功 = {}", auth);
+        // 登录方式不同,Authentication 不同
+        if (RequestUtil.isAjax(request)) {
+//            ResponseResult<>
+            response.setContentType("application/json;charset=UTF-8");
+            // 把 authentication 对象转成 json 格式字符串通过 response 以 application/json;charset=UTF-8 格式写到响应里面去
+            response.getWriter().write(objectMapper.writeValueAsString(authentication));
+        } else {
+            // 跳转到登录之前访问的页面
+            response.setStatus(HttpStatus.OK.value());
+            SavedRequest savedRequest = (SavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
+            if (savedRequest != null) {
+                response.sendRedirect(savedRequest.getRedirectUrl());
+                request.getSession().removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
+            } else {
+                // 重定向页面需要在登录页设置路径参数
+                String redirect = (String) request.getSession().getAttribute("REDIRECT");
+                if (null != redirect) {
+                    response.sendRedirect(redirect);
+                } else {
+                    response.sendRedirect("/");
+                }
+            }
+        }
+    }
+}

+ 19 - 0
src/main/java/cn/minbb/edu/handler/LoginUrlEntryPoint.java

@@ -0,0 +1,19 @@
+package cn.minbb.edu.handler;
+
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 当一个未授权的用户请求非公有资源时, commence 方法将会被调用
+ */
+public class LoginUrlEntryPoint implements AuthenticationEntryPoint {
+    @Override
+    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
+        httpServletResponse.sendRedirect("/sign-in");
+    }
+}

+ 47 - 0
src/main/java/cn/minbb/edu/handler/LogoutSuccessHandler.java

@@ -0,0 +1,47 @@
+package cn.minbb.edu.handler;
+
+import cn.minbb.edu.util.RequestUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * SimpleUrlLogoutSuccessHandler - 重定向到设置的URL地址。默认的地址是/login?logout
+ * HttpStatusReturningLogoutSuccessHandler - REST API场景,允许设置一个返回给客户端的HTTP状态码(默认返回200)来替换重定向到URL这个动作
+ */
+@Component
+public class LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
+
+    private Logger logger = LoggerFactory.getLogger(LogoutSuccessHandler.class);
+
+    private ObjectMapper objectMapper;
+
+    @Autowired
+    public LogoutSuccessHandler(  ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        String auth = objectMapper.writeValueAsString(authentication);
+        logger.info("用户登出成功 = {}", auth);
+        if (RequestUtil.isAjax(request)) {
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write(objectMapper.writeValueAsString(authentication));
+        } else {
+            response.setStatus(HttpStatus.OK.value());
+            response.sendRedirect("/sign-in?logout=true");
+            super.onLogoutSuccess(request, response, authentication);
+        }
+    }
+}

+ 63 - 14
src/main/java/cn/minbb/edu/model/User.java

@@ -6,17 +6,24 @@ import lombok.NoArgsConstructor;
 import lombok.Setter;
 import org.hibernate.annotations.CreationTimestamp;
 import org.hibernate.annotations.UpdateTimestamp;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 
 import javax.persistence.*;
 import javax.validation.constraints.NotNull;
-import java.io.Serializable;
-import java.util.Date;
+import java.util.*;
 
 @Entity
-@Table(name = "user")
+@Table(name = "user",
+        uniqueConstraints = {@UniqueConstraint(name = "unique_username", columnNames = {"username"})},
+        indexes = {
+                @Index(name = "index_id", columnList = "id"),
+                @Index(name = "index_username", columnList = "username")
+        })
 @NoArgsConstructor
 @AllArgsConstructor
-public class User implements Serializable {
+public class User implements UserDetails {
 
     @Getter
     @Setter
@@ -72,19 +79,37 @@ public class User implements Serializable {
 
     @Getter
     @Setter
-    @Enumerated(EnumType.STRING)
-    @Column(name = "type", columnDefinition = "VARCHAR(16) COMMENT '用户类型'")
-    private Type type;
+    @Column(name = "school", columnDefinition = "VARCHAR(64) COMMENT '学校'")
+    private String school;
 
     @Getter
     @Setter
-    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户启用'")
-    private Boolean isEnabled;
+    @ManyToMany(cascade = {CascadeType.REFRESH}, fetch = FetchType.EAGER)
+    @JoinTable(name = "user_user_role",
+            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
+            inverseJoinColumns = {@JoinColumn(name = "user_role_id", referencedColumnName = "id")}
+    )
+    private Set<UserRole> userRoleSet;
 
     @Getter
     @Setter
-    @Column(name = "school", columnDefinition = "VARCHAR(64) COMMENT '学校'")
-    private String school;
+    @Column(name = "is_account_non_expired", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户未过期'")
+    private Boolean isAccountNonExpired;
+
+    @Getter
+    @Setter
+    @Column(name = "is_account_non_locked", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户未锁定'")
+    private Boolean isAccountNonLocked;
+
+    @Getter
+    @Setter
+    @Column(name = "is_credentials_non_expired", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '凭据未过期'")
+    private Boolean isCredentialsNonExpired;
+
+    @Getter
+    @Setter
+    @Column(name = "is_enabled", nullable = false, columnDefinition = "TINYINT DEFAULT '1' COMMENT '帐户已启用'")
+    private Boolean isEnabled;
 
     @Getter
     @Setter
@@ -107,8 +132,32 @@ public class User implements Serializable {
         this.password = password;
     }
 
-    public enum Type {
-        // 学生,教师,管理员
-        STUDENT, TEACHER, ADMIN
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
+        for (UserRole userRole : this.getUserRoleSet()) {
+            grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + userRole.getName()));
+        }
+        return grantedAuthorityList;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return this.isAccountNonExpired;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return this.isAccountNonLocked;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return this.isCredentialsNonExpired;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return this.isEnabled;
     }
 }

+ 56 - 0
src/main/java/cn/minbb/edu/model/UserRole.java

@@ -0,0 +1,56 @@
+package cn.minbb.edu.model;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.persistence.*;
+import java.io.Serializable;
+
+@Entity
+@Table(name = "user_role",
+        uniqueConstraints = {@UniqueConstraint(name = "unique_name", columnNames = {"name"})},
+        indexes = {@Index(name = "index_name", columnList = "name")})
+@NoArgsConstructor
+public class UserRole implements Serializable {
+
+    @Getter
+    @Setter
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id", nullable = false, columnDefinition = "INT COMMENT '用户角色'")
+    private Integer id;
+
+    @Getter
+    @Setter
+    @Column(name = "name", nullable = false, columnDefinition = "VARCHAR(32) COMMENT '角色名称'")
+    private String name;
+
+    @Getter
+    @Setter
+    @Column(name = "description", columnDefinition = "VARCHAR(255) COMMENT '角色描述'")
+    private String description;
+
+    public UserRole(String name) {
+        this.name = name;
+    }
+
+    public UserRole(String name, String description) {
+        this.name = name;
+        this.description = description;
+    }
+
+    @Getter
+    public enum Role {
+        USER("用户"),
+        STUDENT("学生"),
+        TEACHER("教师"),
+        ADMIN("管理员");
+
+        String description;
+
+        Role(String description) {
+            this.description = description;
+        }
+    }
+}

+ 14 - 0
src/main/java/cn/minbb/edu/model/repository/UserRoleRepository.java

@@ -0,0 +1,14 @@
+package cn.minbb.edu.model.repository;
+
+import cn.minbb.edu.model.UserRole;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Set;
+
+@Repository
+public interface UserRoleRepository extends JpaRepository<UserRole, Integer> {
+    UserRole findOneByName(String name);
+
+    Set<UserRole> findAllByNameIn(Set<String> nameSet);
+}

+ 15 - 0
src/main/java/cn/minbb/edu/service/UserRoleService.java

@@ -0,0 +1,15 @@
+package cn.minbb.edu.service;
+
+import cn.minbb.edu.model.UserRole;
+
+import java.util.Set;
+
+public interface UserRoleService {
+    UserRole saveOne(UserRole userRole);
+
+    UserRole findOneByName(String name);
+
+    Set<UserRole> findAll();
+
+    Set<UserRole> findAllByNameSet(Set<String> nameSet);
+}

+ 2 - 1
src/main/java/cn/minbb/edu/service/UserService.java

@@ -1,8 +1,9 @@
 package cn.minbb.edu.service;
 
 import cn.minbb.edu.model.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
 
-public interface UserService {
+public interface UserService extends UserDetailsService {
     User save(User user);
 
     User findUserByUsername(String username);

+ 5 - 4
src/main/java/cn/minbb/edu/service/impl/CourseServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.minbb.edu.service.impl;
 
 import cn.minbb.edu.model.Course;
+import cn.minbb.edu.model.repository.CourseRepository;
 import cn.minbb.edu.service.CourseService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -8,15 +9,15 @@ import org.springframework.stereotype.Service;
 @Service
 public class CourseServiceImpl implements CourseService {
 
-    private CourseService courseService;
+    private CourseRepository courseRepository;
 
     @Autowired
-    public CourseServiceImpl(CourseService courseService) {
-        this.courseService = courseService;
+    public CourseServiceImpl(CourseRepository courseRepository) {
+        this.courseRepository = courseRepository;
     }
 
     @Override
     public Course save(Course course) {
-        return courseService.save(course);
+        return courseRepository.save(course);
     }
 }

+ 5 - 4
src/main/java/cn/minbb/edu/service/impl/HomeworkServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.minbb.edu.service.impl;
 
 import cn.minbb.edu.model.Homework;
+import cn.minbb.edu.model.repository.HomeworkRepository;
 import cn.minbb.edu.service.HomeworkService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -8,15 +9,15 @@ import org.springframework.stereotype.Service;
 @Service
 public class HomeworkServiceImpl implements HomeworkService {
 
-    private HomeworkService homeworkService;
+    private HomeworkRepository homeworkRepository;
 
     @Autowired
-    public HomeworkServiceImpl(HomeworkService homeworkService) {
-        this.homeworkService = homeworkService;
+    public HomeworkServiceImpl(HomeworkRepository homeworkRepository) {
+        this.homeworkRepository = homeworkRepository;
     }
 
     @Override
     public Homework save(Homework homework) {
-        return homeworkService.save(homework);
+        return homeworkRepository.save(homework);
     }
 }

+ 42 - 0
src/main/java/cn/minbb/edu/service/impl/UserRoleServiceImpl.java

@@ -0,0 +1,42 @@
+package cn.minbb.edu.service.impl;
+
+import cn.minbb.edu.model.UserRole;
+import cn.minbb.edu.model.repository.UserRoleRepository;
+import cn.minbb.edu.service.UserRoleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Service
+public class UserRoleServiceImpl implements UserRoleService {
+
+    private UserRoleRepository userRoleRepository;
+
+    @Autowired
+    public UserRoleServiceImpl(UserRoleRepository userRoleRepository) {
+        this.userRoleRepository = userRoleRepository;
+    }
+
+    @Override
+    public UserRole saveOne(UserRole userRole) {
+        return userRoleRepository.save(userRole);
+    }
+
+    @Override
+    public UserRole findOneByName(String name) {
+        return null == name ? null : userRoleRepository.findOneByName(name);
+    }
+
+    @Override
+    public Set<UserRole> findAll() {
+        return new HashSet<>(userRoleRepository.findAll());
+    }
+
+    @Override
+    public Set<UserRole> findAllByNameSet(Set<String> nameSet) {
+        if (nameSet.isEmpty()) return new HashSet<>();
+        return userRoleRepository.findAllByNameIn(nameSet);
+    }
+}

+ 39 - 0
src/main/java/cn/minbb/edu/service/impl/UserServiceImpl.java

@@ -1,11 +1,24 @@
 package cn.minbb.edu.service.impl;
 
 import cn.minbb.edu.model.User;
+import cn.minbb.edu.model.UserRole;
 import cn.minbb.edu.model.repository.UserRepository;
 import cn.minbb.edu.service.UserService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
 @Service
 public class UserServiceImpl implements UserService {
 
@@ -25,4 +38,30 @@ public class UserServiceImpl implements UserService {
     public User findUserByUsername(String username) {
         return userRepository.findUserByUsername(username);
     }
+
+    @Override
+    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
+        User user = findUserByUsername(s);
+        if (user == null) {
+            throw new BadCredentialsException("用户不存在");
+        } else {
+            if (!user.isAccountNonExpired()) {
+                throw new BadCredentialsException("账户已过期");
+            } else if (!user.isAccountNonLocked()) {
+                throw new LockedException("账户被锁定");
+            } else if (!user.isCredentialsNonExpired()) {
+                throw new CredentialsExpiredException("凭据已过期");
+            } else if (!user.isEnabled()) {
+                throw new DisabledException("账户被禁用");
+            } else {
+                List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
+                Set<UserRole> userRoleSet = user.getUserRoleSet();
+                for (UserRole userRole : userRoleSet) {
+                    grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_" + userRole.getName()));
+                }
+                new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorityList);
+            }
+        }
+        return user;
+    }
 }

+ 74 - 0
src/main/java/cn/minbb/edu/system/UserSession.java

@@ -0,0 +1,74 @@
+package cn.minbb.edu.system;
+
+import cn.minbb.edu.model.User;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+public class UserSession {
+
+    private UserSession() {
+    }
+
+    /**
+     * 用户认证 - 获取当前会话中已登录的用户
+     *
+     * @return 已登录用户
+     */
+    public static User getUserAuthentication() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (null != authentication) {
+            Object object = authentication.getPrincipal();
+            if (object instanceof User) {
+                return (User) object;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 更新用户认证信息,此操作会导致原会话失效,即需要用户重新登录
+     *
+     * @param request HttpServletRequest
+     * @param user    用户
+     */
+    public static void updateUserAuthentication(HttpServletRequest request, User user) {
+        // 1.从 HttpServletRequest 中获取 SecurityContextImpl 对象
+        SecurityContextImpl securityContextImpl = (SecurityContextImpl) request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
+        // 2.从 SecurityContextImpl 中获取 Authentication 对象
+        Authentication authentication = securityContextImpl.getAuthentication();
+        // 3.初始化 UsernamePasswordAuthenticationToken 实例,参数 user 就是我们要更新的用户信息
+        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, authentication.getCredentials());
+        auth.setDetails(authentication.getDetails());
+        // 4.重新设置 SecurityContextImpl 对象的 Authentication
+        securityContextImpl.setAuthentication(auth);
+        // SecurityContextHolder.getContext().setAuthentication(auth);
+    }
+
+    /**
+     * 更新用户会话信息,此操作不会导致原会话失效,不需要用户重新登录
+     *
+     * @param request HttpServletRequest
+     * @param user    包含认证凭证(即密码)和 Authorities 的用户
+     */
+    public static void updateUserSession(HttpServletRequest request, User user) {
+        // 根据 userDetails 构建新的 Authentication
+        // 这里使用 PreAuthenticatedAuthenticationToken
+        // 也可以用其他 token, 如 UsernamePasswordAuthenticationToken               
+        PreAuthenticatedAuthenticationToken authentication =
+                new PreAuthenticatedAuthenticationToken(user, user.getPassword(), user.getAuthorities());
+        // 设置 authentication 中 details
+        authentication.setDetails(new WebAuthenticationDetails(request));
+        // 存放 authentication 到 SecurityContextHolder
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        HttpSession session = request.getSession(true);
+        // 在 session 中存放 security context,方便同一个 session 中控制用户的其他操作
+        session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
+    }
+}

+ 35 - 0
src/main/java/cn/minbb/edu/task/InitDataRunner.java

@@ -0,0 +1,35 @@
+package cn.minbb.edu.task;
+
+import cn.minbb.edu.model.UserRole;
+import cn.minbb.edu.service.UserRoleService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+@Order(1)
+@Component
+public class InitDataRunner implements ApplicationRunner {
+
+    private Logger logger = LoggerFactory.getLogger(InitDataRunner.class);
+
+    private UserRoleService userRoleService;
+
+    public InitDataRunner(UserRoleService userRoleService) {
+        this.userRoleService = userRoleService;
+    }
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        // 检查用户角色 - 初始化用户角色数据
+        for (UserRole.Role roleEnum : UserRole.Role.values()) {
+            String name = roleEnum.name();
+            if (null == userRoleService.findOneByName(name)) {
+                logger.info("初始化用户角色数据 -> {} = {}", name, roleEnum.getDescription());
+                userRoleService.saveOne(new UserRole(roleEnum.name(), roleEnum.getDescription()));
+            }
+        }
+    }
+}

+ 16 - 0
src/main/java/cn/minbb/edu/util/RequestUtil.java

@@ -0,0 +1,16 @@
+package cn.minbb.edu.util;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequestUtil {
+
+    /**
+     * 判断请求是否为 Ajax 请求
+     *
+     * @param request HttpServletRequest
+     * @return 是否为 Ajax 请求
+     */
+    public static boolean isAjax(HttpServletRequest request) {
+        return (null != request.getHeader("X-Requested-With") && "XMLHttpRequest".equals(request.getHeader("X-Requested-With")));
+    }
+}

+ 3 - 1
src/main/resources/application.properties

@@ -5,11 +5,13 @@ server.servlet.context-path=/
 spring.datasource.url=
 spring.datasource.username=
 spring.datasource.password=
-spring.datasource.driver-class-name=com.mysql.jdbc.Driver
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
 spring.datasource.sql-script-encoding=UTF-8
 spring.jpa.show-sql=false
 spring.jpa.properties.hibernate.hbm2ddl.auto=update
 spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
+# ¿ªÆôÀÁ¼ÓÔØ
+spring.jpa.open-in-view=true
 # Ä£°åÒýÇæ
 spring.thymeleaf.mode=HTML
 spring.thymeleaf.cache=false

+ 60 - 0
src/main/resources/static/css/user-sign.css

@@ -0,0 +1,60 @@
+html, body {
+    height: 100%;
+}
+
+a {
+    color: gray;
+    font-size: small;
+}
+
+a:hover {
+    color: pink;
+    text-decoration: none;
+}
+
+body {
+    display: -ms-flexbox;
+    display: -webkit-box;
+    display: flex;
+    -ms-flex-align: center;
+    -ms-flex-pack: center;
+    -webkit-box-align: center;
+    align-items: center;
+    -webkit-box-pack: center;
+    justify-content: center;
+    padding-top: 40px;
+    padding-bottom: 40px;
+    background-color: #f5f5f5;
+}
+
+.form-signin {
+    width: 100%;
+    max-width: 360px;
+    padding: 15px;
+    margin: 0 auto;
+}
+
+.form-signin .form-control {
+    position: relative;
+    box-sizing: border-box;
+    height: auto;
+    padding: 10px;
+    font-size: 16px;
+}
+
+.form-signin .form-control:focus {
+    z-index: 2;
+}
+
+.form-signin input[id="name"] {
+    margin-top: 24px;
+    margin-bottom: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+.form-signin input[type="password"] {
+    margin-bottom: 10px;
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+}

BIN
src/main/resources/static/favicon.ico


+ 17 - 0
src/main/resources/templates/fragments/footer.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+</head>
+<body>
+<footer style="position: absolute; bottom: 0; width: 100%; height: 60px; line-height: 60px; background-color: #f5f5f5;" th:fragment="footer">
+    <div class="container" style="padding: 0 15px;">
+        <span class="text-muted">&copy; 2019 <a href="/"> 知学教育</a></span>
+    </div>
+</footer>
+</body>
+</html>

+ 61 - 0
src/main/resources/templates/fragments/header.html

@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+    <!-- Material Design for Bootstrap fonts and icons -->
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"/>
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+</head>
+<body>
+<nav class="navbar navbar-expand-lg navbar-light bg-light" th:fragment="header">
+    <a class="navbar-brand" href="/">知学教育</a>
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-supported-content"
+            aria-controls="navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
+        <span class="navbar-toggler-icon"></span>
+    </button>
+
+    <div class="collapse navbar-collapse" id="navbar-supported-content">
+        <ul class="navbar-nav mr-auto">
+            <li class="nav-item active">
+                <a class="nav-link" href="/">首页<span class="sr-only">(current)</span></a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" href="#">链接</a>
+            </li>
+        </ul>
+        <form class="form-inline my-2 my-lg-0">
+            <input class="form-control mr-sm-2" type="search" placeholder="搜索..." aria-label="Search"/>
+            <button class="btn btn-sm btn-outline-success my-2 my-sm-0" type="submit">搜索</button>
+        </form>
+        <ul class="navbar-nav ml-auto">
+            <li class="nav-item" th:if="${user == null}">
+                <a class="nav-link" href="/sign-in">登录 / 注册</a>
+            </li>
+            <li class="nav-item dropdown" th:unless="${user == null}">
+                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
+                   aria-expanded="false" th:text="${user.getName()}">
+                    用户名
+                </a>
+                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
+                    <a class="dropdown-item" href="#">用户中心</a>
+                    <a class="dropdown-item" href="#">课程中心</a>
+                    <div class="dropdown-divider"></div>
+                    <a class="dropdown-item" href="/sign-out">退出登录</a>
+                </div>
+            </li>
+        </ul>
+    </div>
+</nav>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
+<script>
+    $(document).ready(function () {
+        $('body').bootstrapMaterialDesign();
+    });
+</script>
+</body>
+</html>

+ 8 - 3
src/main/resources/templates/index.html

@@ -1,10 +1,15 @@
 <!DOCTYPE html>
-<html lang="zh-CN">
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layouts/layout}">
 <head>
     <meta charset="UTF-8"/>
-    <title>Title</title>
+    <title>知学教育</title>
 </head>
 <body>
-
+<th:block layout:fragment="content">
+    知学教育APP
+</th:block>
 </body>
 </html>

+ 44 - 0
src/main/resources/templates/layouts/layout.html

@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+<head>
+    <meta charset="UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title></title>
+
+    <!-- Material Design for Bootstrap fonts and icons -->
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons"/>
+    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css"/>
+    <style>
+        html {
+            position: relative;
+            min-height: 100%;
+        }
+
+        a:hover {
+            text-decoration: none;
+        }
+    </style>
+</head>
+<body style="margin-bottom: 60px;">
+<header th:replace="~{fragments/header :: header}"></header>
+
+<div class="container" style="margin-top: 24px;">
+    <div layout:fragment="content">
+        <p>内容</p>
+    </div>
+</div>
+
+<footer th:replace="~{fragments/footer :: footer}"></footer>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
+<script>
+    $(document).ready(function () {
+        $('body').bootstrapMaterialDesign();
+    });
+</script>
+</body>
+</html>

+ 47 - 34
src/main/resources/templates/sign-in.html

@@ -1,55 +1,68 @@
 <!DOCTYPE html>
-<html lang="zh-CN"
-      xmlns="http://www.w3.org/1999/html"
+<html lang="zh-CN" xmlns="http://www.w3.org/1999/html"
       xmlns:th="http://www.thymeleaf.org">
 <head>
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
     <title>用户登录</title>
 
-    <!-- Bootstrap -->
-    <link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
-
-    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
-    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
-    <!--[if lt IE 9]>
-    <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
-    <![endif]-->
+    <link href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link rel="stylesheet" href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}"/>
 </head>
-<body>
-<div class="container">
-    <div class="row">
-        <h1>用户登录</h1>
+<body class="text-center">
+<form class="form-signin" method="post" action="/sign-in">
+    <a href="/"><img class="mb-4" src="../static/favicon.ico" alt="" width="72" height="72" th:src="@{/static/favicon.ico}"/></a>
+    <h1 class="h3 mb-3 font-weight-normal">用户登录</h1>
+    <small style="color: blue;" th:if="${param.logout}">退出登录成功,重新登录以开始您的会话</small>
+    <div th:unless="${param.logout}">
+        <small style="color: red;" th:if="${param.error != null && session.SPRING_SECURITY_LAST_EXCEPTION != null}"
+               th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></small>
+        <small th:if="${param.error == null || session.SPRING_SECURITY_LAST_EXCEPTION == null}">登录以开始您的会话</small>
     </div>
 
-    <div class="row">
-        <div class="col-sm-12 col-md-6 col-lg-6">
-            <div class="form-group">
-                <input id="username" type="text" class="form-control" placeholder="用户名"/>
-            </div>
-
-            <div class="form-group">
-                <input id="password" type="password" class="form-control" placeholder="密码"/>
-            </div>
+    <label for="username" class="sr-only">用户名</label>
+    <input class="form-control" id="username" name="username" type="text" placeholder="用户名" style="height: 44px;" required autofocus/>
 
-            <button type="button" class="btn btn-default" onclick="login();">登录</button>
-        </div>
+    <label for="password" class="sr-only">密码</label>
+    <input class="form-control" id="password" name="password" type="password" placeholder="密码" style="height: 44px;" required/>
 
-        <div class="col-sm-12 col-md-6 col-lg-6">
+    <div class="mb-3">
+        <a href="/sign-up" style="margin-right: 36px;">注册账号</a>
+        <a href="#password-modal" data-toggle="modal">忘记密码</a>
+    </div>
+    <button class="btn btn-lg btn-success btn-block" type="submit">登录</button>
+    <p class="mt-5 mb-3 text-muted">&copy; 2019 知学教育</p>
+</form>
 
+<!-- Modal -->
+<div class="modal fade" id="password-modal" tabindex="-1" role="dialog" aria-labelledby="" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">忘记密码</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                请联系系统管理员
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary" data-dismiss="modal">我知道了</button>
+            </div>
         </div>
     </div>
 </div>
 
-<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
-<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
-<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
-
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js"></script>
+<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js"></script>
 <script>
+    $(document).ready(function () {
+    });
+
     function login() {
         $.ajax({
             url: '/user/login?username=' + $('#username').val() + '&password=' + $('#password').val(),

+ 47 - 0
src/main/resources/templates/sign-up.html

@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html lang="zh-CN"
+      xmlns="http://www.w3.org/1999/html"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
+    <title>用户注册</title>
+
+    <link href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css" rel="stylesheet"/>
+    <link rel="stylesheet" href="../static/css/user-sign.css" th:href="@{/css/user-sign.css}"/>
+</head>
+<body class="text-center">
+
+<form class="form-signin">
+    <a href="/"><img class="mb-4" src="../static/favicon.ico" alt="" width="72" height="72" th:src="@{/static/favicon.ico}"/></a>
+    <h1 class="h3 mb-3 font-weight-normal">用户注册</h1>
+
+    <label for="name" class="sr-only">姓名</label>
+    <input class="form-control" id="name" type="text" placeholder="姓名" maxlength="16"
+           style="height: 44px;" required autofocus/>
+
+    <label for="username" class="sr-only">用户名</label>
+    <input class="form-control" id="username" type="text" placeholder="用户名" maxlength="16"
+           style="height: 44px;" required/>
+
+    <label for="password" class="sr-only">密码</label>
+    <input class="form-control" id="password" type="password" placeholder="密码" minlength="6" maxlength="24"
+           style="height: 44px; margin-bottom: 0;" required/>
+
+    <label for="repeat-password" class="sr-only">重复密码</label>
+    <input class="form-control" id="repeat-password" type="password" placeholder="重复密码" minlength="6" maxlength="24"
+           style="height: 44px;" required/>
+
+    <div class="mb-3"><a href="/sign-in">我有账号? 去登录</a></div>
+    <button class="btn btn-lg btn-success btn-block" type="submit">注册</button>
+    <p class="mt-5 mb-3 text-muted">&copy; 2019 知学教育</p>
+</form>
+
+<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.js"></script>
+<script>
+    $(document).ready(function () {
+    });
+</script>
+</body>
+</html>