Browse Source

集成 Spring Security 组件,重构后,整体逻辑更加清晰

YunaiV 4 years ago
parent
commit
fc8eac548a
27 changed files with 1821 additions and 177 deletions
  1. 1 1
      http-client.env.json
  2. 149 146
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  3. 52 0
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java
  4. 90 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java
  5. 89 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java
  6. 44 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java
  7. 37 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java
  8. 42 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
  9. 35 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java
  10. 59 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityUtils.java
  11. 7 0
      src/main/java/cn/iocoder/dashboard/framework/security/package-info.java
  12. 2 0
      src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md
  13. 17 0
      src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java
  14. 1 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/package-info.java
  15. 15 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/user/SysUserMapper.java
  16. 154 0
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/user/SysUserDO.java
  17. 21 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java
  18. 30 30
      ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java
  19. 16 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java
  20. 30 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysTokenService.java
  21. 215 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java
  22. 58 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysTokenServiceImpl.java
  23. 166 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java
  24. 447 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java
  25. 19 0
      src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java
  26. 23 0
      src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java
  27. 2 0
      src/main/resources/application.yaml

+ 1 - 1
http-client.env.json

@@ -1,6 +1,6 @@
 {
   "local": {
     "baseUrl": "http://127.0.0.1:8080/api",
-    "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w"
+    "token": "yudaoyuanma1"
   }
 }

+ 149 - 146
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -1,146 +1,149 @@
-package com.ruoyi.framework.config;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.http.HttpMethod;
-import org.springframework.security.authentication.AuthenticationManager;
-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.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-import org.springframework.security.web.authentication.logout.LogoutFilter;
-import org.springframework.web.filter.CorsFilter;
-import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
-import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
-import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
-
-/**
- * spring security配置
- * 
- * @author ruoyi
- */
-@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
-public class SecurityConfig extends WebSecurityConfigurerAdapter
-{
-    /**
-     * 自定义用户认证逻辑
-     */
-    @Autowired
-    private UserDetailsService userDetailsService;
-    
-    /**
-     * 认证失败处理类
-     */
-    @Autowired
-    private AuthenticationEntryPointImpl unauthorizedHandler;
-
-    /**
-     * 退出处理类
-     */
-    @Autowired
-    private LogoutSuccessHandlerImpl logoutSuccessHandler;
-
-    /**
-     * token认证过滤器
-     */
-    @Autowired
-    private JwtAuthenticationTokenFilter authenticationTokenFilter;
-
-    /**
-     * 跨域过滤器
-     */
-    @Autowired
-    private CorsFilter corsFilter;
-    
-    /**
-     * 解决 无法直接注入 AuthenticationManager
-     *
-     * @return
-     * @throws Exception
-     */
-    @Bean
-    @Override
-    public AuthenticationManager authenticationManagerBean() throws Exception
-    {
-        return super.authenticationManagerBean();
-    }
-
-    /**
-     * anyRequest          |   匹配所有请求路径
-     * access              |   SpringEl表达式结果为true时可以访问
-     * anonymous           |   匿名可以访问
-     * denyAll             |   用户不能访问
-     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
-     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
-     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
-     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
-     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
-     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
-     * permitAll           |   用户可以任意访问
-     * rememberMe          |   允许通过remember-me登录的用户访问
-     * authenticated       |   用户登录后可访问
-     */
-    @Override
-    protected void configure(HttpSecurity httpSecurity) throws Exception
-    {
-        httpSecurity
-                // CSRF禁用,因为不使用session
-                .csrf().disable()
-                // 认证失败处理类
-                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
-                // 基于token,所以不需要session
-                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
-                // 过滤请求
-                .authorizeRequests()
-                // 对于登录login 验证码captchaImage 允许匿名访问
-                .antMatchers("/login", "/captchaImage").anonymous()
-                .antMatchers(
-                        HttpMethod.GET,
-                        "/*.html",
-                        "/**/*.html",
-                        "/**/*.css",
-                        "/**/*.js"
-                ).permitAll()
-                .antMatchers("/profile/**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
-                .antMatchers("/swagger-ui.html").anonymous()
-                .antMatchers("/swagger-resources/**").anonymous()
-                .antMatchers("/webjars/**").anonymous()
-                .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
-                // 除上面外的所有请求全部需要鉴权认证
-                .anyRequest().authenticated()
-                .and()
-                .headers().frameOptions().disable();
-        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
-        // 添加JWT filter
-        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
-        // 添加CORS filter
-        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
-        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
-    }
-
-    
-    /**
-     * 强散列哈希加密实现
-     */
-    @Bean
-    public BCryptPasswordEncoder bCryptPasswordEncoder()
-    {
-        return new BCryptPasswordEncoder();
-    }
-
-    /**
-     * 身份认证接口
-     */
-    @Override
-    protected void configure(AuthenticationManagerBuilder auth) throws Exception
-    {
-        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
-    }
-}
+package cn.iocoder.dashboard.framework.security.config;
+
+import cn.iocoder.dashboard.framework.security.core.filter.JwtAuthenticationTokenFilter;
+import cn.iocoder.dashboard.framework.security.core.handler.AuthenticationEntryPointImpl;
+import cn.iocoder.dashboard.framework.security.core.handler.LogoutSuccessHandlerImpl;
+import cn.iocoder.dashboard.framework.web.config.WebProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+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.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.filter.CorsFilter;
+
+import javax.annotation.Resource;
+
+/**
+ * spring security配置
+ *
+ * @author ruoyi
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+@EnableConfigurationProperties(SecurityProperties.class)
+public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+    /**
+     * 自定义用户认证逻辑
+     */
+    @Resource
+    private UserDetailsService userDetailsService;
+
+    /**
+     * 认证失败处理类
+     */
+    @Resource
+    private AuthenticationEntryPoint unauthorizedHandler;
+    /**
+     * 权限不够处理器
+     */
+    @Resource
+    private AccessDeniedHandler accessDeniedHandler;
+    /**
+     * 退出处理类
+     */
+    @Resource
+    private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+    /**
+     * Token 认证过滤器
+     */
+    @Resource
+    private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+    @Resource
+    private WebProperties webProperties;
+
+    /**
+     * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
+     * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
+     */
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+    /**
+     * Spring Security 加密器
+     * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器
+     *
+     * @see <a href="http://stackabuse.com/password-encoding-with-spring-security/">Password Encoding with Spring Security</a>
+     */
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    /**
+     * 身份认证接口
+     */
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth.userDetailsService(userDetailsService)
+                .passwordEncoder(passwordEncoder());
+    }
+
+    /**
+     * 配置 URL 的安全配置
+     *
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity httpSecurity) throws Exception {
+        httpSecurity
+                // CSRF 禁用,因为不使用 Session
+                .csrf().disable()
+                // 基于 token 机制,所以不需要 Session
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+                // 一堆自定义的 Spring Security 处理器
+                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
+                    .accessDeniedHandler(accessDeniedHandler).and()
+                // TODO 过滤请求
+                .authorizeRequests()
+                // 登陆的接口,可匿名访问
+                .antMatchers(webProperties.getApiPrefix() + "/login").anonymous()
+                // 通用的接口,可匿名访问
+                .antMatchers( webProperties.getApiPrefix() + "/captcha/**").anonymous()
+                // TODO
+                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
+                .antMatchers("/profile/**").anonymous()
+                .antMatchers("/common/download**").anonymous()
+                .antMatchers("/common/download/resource**").anonymous()
+                .antMatchers("/swagger-ui.html").anonymous()
+                .antMatchers("/swagger-resources/**").anonymous()
+                .antMatchers("/webjars/**").anonymous()
+                .antMatchers("/*/api-docs").anonymous()
+                .antMatchers("/druid/**").hasAnyAuthority("druid") // TODO 芋艿,未来需要在拓展下
+                // 除上面外的所有请求全部需要鉴权认证
+                .anyRequest().authenticated()
+                .and()
+                .headers().frameOptions().disable();
+        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
+        // 添加 JWT Filter
+        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+    }
+
+}

+ 52 - 0
src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java

@@ -0,0 +1,52 @@
+package cn.iocoder.dashboard.framework.security.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.Duration;
+
+@ConfigurationProperties(prefix = "yudao.security")
+@Validated
+@Data
+public class SecurityProperties {
+
+    /**
+     * HTTP 请求时,访问令牌的请求 Header
+     */
+    @NotEmpty(message = "Token Header 不能为空")
+    private String tokenHeader;
+    /**
+     * Token 过期时间
+     */
+    @NotNull(message = "Token 过期时间不能为空")
+    private Duration tokenTimeout;
+    /**
+     * Token 秘钥
+     */
+    @NotEmpty(message = "Token 秘钥不能为空")
+    private String tokenSecret;
+    /**
+     * Session 过期时间
+     *
+     * 当 User 用户超过当前时间未操作,则 Session 会过期
+     */
+    @NotNull(message = "Session 过期时间不能为空")
+    private Duration sessionTimeout;
+
+    /**
+     * mock 模式的开关
+     */
+    @NotNull(message = "mock 模式的开关不能为空")
+    private Boolean mockEnable;
+    /**
+     * mock 模式的秘钥
+     * 一定要配置秘钥,保证安全性
+     */
+    @NotEmpty(message = "mock 模式的秘钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
+    private String mockSecret = "yudaoyuanma";
+
+}

+ 90 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java

@@ -0,0 +1,90 @@
+package cn.iocoder.dashboard.framework.security.core;
+
+import cn.iocoder.dashboard.modules.system.enums.user.UserStatus;
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import org.springframework.data.annotation.Transient;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * 登陆用户信息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class LoginUser implements UserDetails {
+
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 角色编号数组
+     */
+    private Set<Integer> roleIds;
+    /**
+     * 最后更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 用户名
+     */
+    private String username;
+    /**
+     * 密码
+     */
+    private String password;
+    /**
+     * 状态
+     */
+    private String status;
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isEnabled() {
+        return UserStatus.OK.getCode().equals(status);
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return null;
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isAccountNonExpired() {
+        return true; // 返回 true,不依赖 Spring Security 判断
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isAccountNonLocked() {
+        return true; // 返回 true,不依赖 Spring Security 判断
+    }
+
+    @Override
+    @JSONField(serialize = false) // 避免序列化
+    public boolean isCredentialsNonExpired() {
+        return true;  // 返回 true,不依赖 Spring Security 判断
+    }
+
+}

+ 89 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,89 @@
+package cn.iocoder.dashboard.framework.security.core.filter;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
+import cn.iocoder.dashboard.framework.web.core.handler.GlobalExceptionHandler;
+import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.annotation.Resource;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * JWT 过滤器,验证 token 的有效性
+ * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
+ *
+ * @author ruoyi
+ */
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+
+    @Resource
+    private SecurityProperties securityProperties;
+    @Resource
+    private SysAuthService authService;
+    @Resource
+    private GlobalExceptionHandler globalExceptionHandler;
+
+    @Override
+    @SuppressWarnings("NullableProblems")
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        String token = SecurityUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+        if (StrUtil.isNotEmpty(token)) {
+            try {
+                // 验证 token 有效性
+                LoginUser loginUser = authService.verifyTokenAndRefresh(token);
+                // 模拟 Login 功能,方便日常开发调试
+                if (loginUser == null) {
+                    loginUser = this.mockLoginUser(token);
+                }
+                // 设置当前用户
+                if (loginUser != null) {
+                    SecurityUtils.setLoginUser(loginUser, request);
+                }
+            } catch (Throwable ex) {
+                CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
+                ServletUtils.writeJSON(response, result);
+                return;
+            }
+        }
+
+        // 继续过滤链
+        chain.doFilter(request, response);
+    }
+
+    /**
+     * 模拟登陆用户,方便日常开发调试
+     *
+     * 注意,在线上环境下,一定要关闭该功能!!!
+     *
+     * @param token 模拟的 token,格式为 {@link SecurityProperties#getTokenSecret()} + 用户编号
+     * @return 模拟的 LoginUser
+     */
+    private LoginUser mockLoginUser(String token) {
+        if (!securityProperties.getMockEnable()) {
+            return null;
+        }
+        // 必须以 mockSecret 开头
+        if (!token.startsWith(securityProperties.getMockSecret())) {
+            return null;
+        }
+        Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
+        return authService.mockLogin(userId);
+    }
+
+}

+ 44 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java

@@ -0,0 +1,44 @@
+package cn.iocoder.dashboard.framework.security.core.handler;
+
+import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.access.ExceptionTranslationFilter;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+
+/**
+ * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。
+ *
+ * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类
+ *
+ * @author 芋道源码
+ */
+@Component
+@Slf4j
+@SuppressWarnings("JavadocReference")
+public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
+
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
+            throws IOException, ServletException {
+        // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏
+        log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(),
+                SecurityUtils.getLoginUser().getUserId(), e);
+        // 返回 403
+        ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED));
+    }
+
+}

+ 37 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java

@@ -0,0 +1,37 @@
+package cn.iocoder.dashboard.framework.security.core.handler;
+
+import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.access.ExceptionTranslationFilter;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+
+/**
+ * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页
+ *
+ * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类
+ *
+ * @author ruoyi
+ */
+@Component
+@Slf4j
+@SuppressWarnings("JavadocReference") // 忽略文档引用报错
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
+        log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e);
+        // 返回 401
+        ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED));
+    }
+
+}

+ 42 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java

@@ -0,0 +1,42 @@
+package cn.iocoder.dashboard.framework.security.core.handler;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
+import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * 自定义退出处理器
+ *
+ * @author ruoyi
+ */
+@Component
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
+
+    @Resource
+    private SecurityProperties securityProperties;
+
+    @Resource
+    private SecurityFrameworkService securityFrameworkService;
+
+    @Override
+    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
+        // 执行退出
+        String token = SecurityUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+        if (StrUtil.isNotBlank(token)) {
+            securityFrameworkService.logout(token);
+        }
+        // 返回成功
+        ServletUtils.writeJSON(response, null);
+//        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.OK.value(), "退出成功")));
+    }
+}

+ 35 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java

@@ -0,0 +1,35 @@
+package cn.iocoder.dashboard.framework.security.core.service;
+
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+/**
+ * Security 框架 Service 接口,定义 security 组件需要的功能
+ */
+public interface SecurityFrameworkService extends UserDetailsService {
+
+    /**
+     * 基于 token 退出登录
+     *
+     * @param token token
+     */
+    void logout(String token);
+
+    /**
+     * 校验 token 的有效性,并获取用户信息
+     * 通过后,刷新 token 的过期时间
+     *
+     * @param token token
+     * @return 用户信息
+     */
+    LoginUser verifyTokenAndRefresh(String token);
+
+    /**
+     * 模拟指定用户编号的 LoginUser
+     *
+     * @param userId 用户编号
+     * @return 登录用户
+     */
+    LoginUser mockLogin(Long userId);
+
+}

+ 59 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityUtils.java

@@ -0,0 +1,59 @@
+package cn.iocoder.dashboard.framework.security.core.util;
+
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 安全服务工具类
+ *
+ * @author ruoyi
+ */
+public class SecurityUtils {
+
+    /**
+     * 从请求中,获得认证 Token
+     *
+     * @param request 请求
+     * @param header 认证 Token 对应的 Header 名字
+     * @return 认证 Token
+     */
+    public static String obtainAuthorization(HttpServletRequest request, String header) {
+        String authorization = request.getHeader(header);
+        if (!StringUtils.hasText(authorization)) {
+            return null;
+        }
+        int index = authorization.indexOf("Bearer ");
+        if (index == -1) { // 未找到
+            return null;
+        }
+        return authorization.substring(index + 7).trim();
+    }
+
+    /**
+     * 获取当前用户
+     */
+    public static LoginUser getLoginUser() {
+        return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+    }
+
+    /**
+     * 设置当前用户
+     *
+     * @param loginUser 登陆用户
+     * @param request 请求
+     */
+    public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
+        // 创建 UsernamePasswordAuthenticationToken 对象
+        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
+                loginUser, null, null);
+        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+        // 设置到上下文
+        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+    }
+
+}

+ 7 - 0
src/main/java/cn/iocoder/dashboard/framework/security/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 基于 Spring Security 框架
+ * 实现安全认证功能
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.dashboard.framework.security;

+ 2 - 0
src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md

@@ -0,0 +1,2 @@
+* 芋道 Spring Security 入门:<http://www.iocoder.cn/Spring-Boot/Spring-Security/?dashboard>
+* Spring Security 基本概念:<http://www.iocoder.cn/Fight/Spring-Security-4-1-0-Basic-concept-description/?dashboard>

+ 17 - 0
src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java

@@ -0,0 +1,17 @@
+package cn.iocoder.dashboard.modules.system.convert.auth;
+
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface SysAuthConvert {
+
+    SysAuthConvert INSTANCE = Mappers.getMapper(SysAuthConvert.class);
+
+    @Mapping(source = "updateTime", target = "updateTime", ignore = true) // 字段相同,但是含义不同,忽略
+    LoginUser convert(SysUserDO bean);
+
+}

+ 1 - 0
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.dashboard.modules.system.dal.mysql.dao;

+ 15 - 0
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/user/SysUserMapper.java

@@ -0,0 +1,15 @@
+package cn.iocoder.dashboard.modules.system.dal.mysql.dao.user;
+
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysUserMapper extends BaseMapper<SysUserDO> {
+
+    default SysUserDO selectByUsername(String username) {
+        return selectOne(new QueryWrapper<SysUserDO>().eq("username", username));
+    }
+
+}

+ 154 - 0
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/user/SysUserDO.java

@@ -0,0 +1,154 @@
+package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user;
+
+import java.util.Date;
+import java.util.List;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+import cn.iocoder.dashboard.framework.excel.Excel;
+import cn.iocoder.dashboard.framework.excel.Excels;
+import cn.iocoder.dashboard.framework.mybatis.core.BaseDO;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dept.SysDept;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRole;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+/**
+ * 用户 DO
+ *
+ * @author ruoyi
+ */
+@TableName("sys_user")
+@Data
+@EqualsAndHashCode(callSuper = true)
+// TODO 芋艿:数据库的字段默认值
+public class SysUserDO extends BaseDO {
+
+    /**
+     * 用户ID
+     */
+    @TableId
+    @Excel(name = "用户序号", cellType = Excel.ColumnType.NUMERIC, prompt = "用户编号")
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    @Excel(name = "部门编号", type = Excel.Type.IMPORT)
+    private Long deptId;
+
+    /**
+     * 用户账号
+     */
+    @Excel(name = "登录名称")
+    @NotBlank(message = "用户账号不能为空")
+    @Size(max = 30, message = "用户账号长度不能超过30个字符")
+    private String username;
+
+    /**
+     * 加密后的密码
+     *
+     * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐
+     */
+    private String password;
+
+    /**
+     * 用户昵称
+     */
+    @Excel(name = "用户名称")
+    @Size(max = 30, message = "用户昵称长度不能超过30个字符")
+    private String nickname;
+
+    /**
+     * 用户邮箱
+     */
+    @Excel(name = "用户邮箱")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 50, message = "邮箱长度不能超过50个字符")
+    private String email;
+
+    /**
+     * 手机号码
+     */
+    @Excel(name = "手机号码")
+    @Size(max = 11, message = "手机号码长度不能超过11个字符")
+    private String mobile;
+
+    /**
+     * 用户性别
+     */
+    @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
+    private String sex;
+
+    /**
+     * 用户头像
+     */
+    private String avatar;
+
+    /**
+     * 帐号状态(0正常 1停用)
+     */
+    @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
+    // TODO 芋艿:修改成枚举
+    private String status;
+
+    /**
+     * 最后登录IP
+     */
+    @Excel(name = "最后登录IP", type = Excel.Type.EXPORT)
+    private String loginIp;
+
+    /**
+     * 最后登录时间
+     */
+    @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Excel.Type.EXPORT)
+    private Date loginDate;
+
+
+    // TODO FROM 芋艿:下面的字段,需要忽略
+
+    /**
+     * 部门对象
+     */
+    @Excels({
+            @Excel(name = "部门名称", targetAttr = "deptName", type = Excel.Type.EXPORT),
+            @Excel(name = "部门负责人", targetAttr = "leader", type = Excel.Type.EXPORT)
+    })
+    @TableField(exist = false)
+    private SysDept dept;
+
+    /**
+     * 角色对象
+     */
+    @TableField(exist = false)
+    private List<SysRole> roles;
+
+    /**
+     * 角色组
+     */
+    @TableField(exist = false)
+    private Long[] roleIds;
+
+    /**
+     * 岗位组
+     */
+    @TableField(exist = false)
+    private Long[] postIds;
+
+    // TODO 芋艿:后续清理掉
+    public boolean isAdmin() {
+        return isAdmin(this.userId);
+    }
+
+    public static boolean isAdmin(Long userId) {
+        return userId != null && 1L == userId;
+    }
+
+}

+ 21 - 0
src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java

@@ -0,0 +1,21 @@
+package cn.iocoder.dashboard.modules.system.enums;
+
+import cn.iocoder.dashboard.common.exception.ErrorCode;
+
+/**
+ * 错误码枚举类
+ *
+ * system 系统,使用 1-002-000-000 段
+ */
+public interface SysErrorCodeConstants {
+
+    // ========== AUTH 模块 1002000000 ==========
+    ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
+    ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
+    ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1002000002, "登录失败"); // 登陆失败的兜底,位置原因
+
+    // ========== TOKEN 模块 1002001000 ==========
+    ErrorCode TOKEN_EXPIRED = new ErrorCode(1002001000, "Token 已经过期");
+    ErrorCode TOKEN_PARSE_FAIL = new ErrorCode(1002001001, "Token 解析失败");
+
+}

+ 30 - 30
ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java

@@ -1,30 +1,30 @@
-package com.ruoyi.common.enums;
-
-/**
- * 用户状态
- * 
- * @author ruoyi
- */
-public enum UserStatus
-{
-    OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除");
-
-    private final String code;
-    private final String info;
-
-    UserStatus(String code, String info)
-    {
-        this.code = code;
-        this.info = info;
-    }
-
-    public String getCode()
-    {
-        return code;
-    }
-
-    public String getInfo()
-    {
-        return info;
-    }
-}
+package cn.iocoder.dashboard.modules.system.enums.user;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+public enum UserStatus
+{
+    OK("0", "正常"), DISABLE("1", "停用"), DELETED("2", "删除");
+
+    private final String code;
+    private final String info;
+
+    UserStatus(String code, String info)
+    {
+        this.code = code;
+        this.info = info;
+    }
+
+    public String getCode()
+    {
+        return code;
+    }
+
+    public String getInfo()
+    {
+        return info;
+    }
+}

+ 16 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java

@@ -0,0 +1,16 @@
+package cn.iocoder.dashboard.modules.system.service.auth;
+
+import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
+
+/**
+ * 认证 Service 接口
+ *
+ * 提供用户的账号密码登陆、token 的校验等认证相关的功能
+ *
+ * @author 芋道源码
+ */
+public interface SysAuthService extends SecurityFrameworkService {
+
+    String login(String username, String password, String captchaUUID, String captchaCode);
+
+}

+ 30 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysTokenService.java

@@ -0,0 +1,30 @@
+package cn.iocoder.dashboard.modules.system.service.auth;
+
+import io.jsonwebtoken.Claims;
+
+import java.util.Map;
+
+/**
+ * Token Service 接口
+ *
+ * 提供访问 Token 令牌,目前基于 JWT 实现
+ */
+public interface SysTokenService {
+
+    /**
+     * 创建 Token
+     *
+     * @param subject 主体
+     * @return Token 字符串
+     */
+    String createToken(String subject);
+
+    /**
+     * 解析 Token,返回 claims 数据声明
+     *
+     * @param token Token
+     * @return claims
+     */
+    Claims parseToken(String token);
+
+}

+ 215 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -0,0 +1,215 @@
+package cn.iocoder.dashboard.modules.system.service.auth.impl;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
+import cn.iocoder.dashboard.framework.security.core.LoginUser;
+import cn.iocoder.dashboard.modules.system.enums.user.UserStatus;
+import cn.iocoder.dashboard.modules.system.convert.auth.SysAuthConvert;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
+import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO;
+import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService;
+import cn.iocoder.dashboard.modules.system.service.auth.SysTokenService;
+import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
+import cn.iocoder.dashboard.util.date.DateUtils;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Set;
+
+import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
+
+/**
+ * Auth Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Slf4j
+public class SysAuthServiceImpl implements SysAuthService {
+
+    @Resource
+    private SecurityProperties securityProperties;
+
+    @Resource
+    private SysTokenService tokenService;
+    @Resource
+    private AuthenticationManager authenticationManager;
+    @Resource
+    private SysUserService userService;
+    @Resource
+    private SysLoginUserRedisDAO loginUserRedisDAO;
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        // 获取 username 对应的 SysUserDO
+        SysUserDO user = userService.getUserByUserName(username);
+        if (user == null) {
+            throw new UsernameNotFoundException(username);
+        }
+
+        // 创建 LoginUser 对象
+        return SysAuthConvert.INSTANCE.convert(user);
+    }
+
+    @Override
+    public LoginUser mockLogin(Long userId) {
+        // 获取用户编号对应的 SysUserDO
+        SysUserDO user = userService.getUser(userId);
+        if (user == null) {
+            throw new UsernameNotFoundException(String.valueOf(userId));
+        }
+
+        // 创建 LoginUser 对象
+        LoginUser loginUser = SysAuthConvert.INSTANCE.convert(user);
+        loginUser.setUpdateTime(new Date());
+        loginUser.setRoleIds(this.getUserRoleIds(loginUser.getUserId()));
+        return loginUser;
+    }
+
+    @Override
+    public String login(String username, String password, String captchaUUID, String captchaCode) {
+        // 判断验证码是否正确
+        this.verifyCaptcha(captchaUUID, captchaCode);
+
+        // 使用账号密码,进行登陆。
+        LoginUser loginUser = this.login0(username, password);
+
+        // 缓存登陆用户到 Redis 中
+        String sessionId = IdUtil.fastSimpleUUID();
+        loginUser.setUpdateTime(new Date());
+        loginUser.setRoleIds(this.getUserRoleIds(loginUser.getUserId()));
+        loginUserRedisDAO.set(sessionId, loginUser);
+
+        // 创建 Token
+        // 我们在返回给前端的 JWT 中,使用 sessionId 作为 subject 主体,标识当前 User 用户
+        return tokenService.createToken(sessionId);
+    }
+
+    private void verifyCaptcha(String captchaUUID, String captchaCode) {
+        //        String verifyKey = Constants.CAPTCHA_CODE_KEY + captchaUUID;
+//        String captcha = redisCache.getCacheObject(verifyKey);
+//        redisCache.deleteObject(verifyKey);
+//        if (captcha == null) {
+//            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+//            throw new CaptchaExpireException();
+//        }
+//        if (!code.equalsIgnoreCase(captcha)) {
+//            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+//            throw new CaptchaException();
+//        }
+    }
+
+    private LoginUser login0(String username, String password) {
+        // 用户验证
+        Authentication authentication;
+        try {
+            // 调用 Spring Security 的 AuthenticationManager#authenticate(...) 方法,使用账号密码进行认证
+            // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息
+            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
+        } catch (BadCredentialsException badCredentialsException) {
+            // TODO 日志优化
+//            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
+        } catch (DisabledException disabledException) {
+            // TODO 日志优化
+            throw exception(AUTH_LOGIN_USER_DISABLED);
+        } catch (AuthenticationException authenticationException) {
+            // TODO 日志优化
+            throw exception(AUTH_LOGIN_FAIL_UNKNOWN);
+        }
+        // TODO 需要优化
+//        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+        Assert.notNull(authentication.getPrincipal(), "Principal 不会为空");
+        return (LoginUser) authentication.getPrincipal();
+    }
+
+    /**
+     * 获得 User 拥有的角色编号数组
+     *
+     * @param userId 用户编号
+     * @return 角色编号数组
+     */
+    private Set<Integer> getUserRoleIds(Long userId) {
+        // TODO 芋艿:读取角色编号
+        return Collections.emptySet();
+    }
+
+    @Override
+    public void logout(String token) {
+//        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功")); TODO 需要搞一搞
+    }
+
+    @Override
+    public LoginUser verifyTokenAndRefresh(String token) {
+        // 验证 token 的有效性
+        String sessionId = this.verifyToken(token);
+        // 获得 LoginUser
+        LoginUser loginUser = loginUserRedisDAO.get(sessionId);
+        if (loginUser == null) {
+//            throw exception(AUTH_SESSION_TIMEOUT);
+            return null;
+        }
+        // 刷新 LoginUser 缓存
+        this.refreshLoginUserCache(sessionId, loginUser);
+        return loginUser;
+    }
+
+    private String verifyToken(String token) {
+        Claims claims;
+        try {
+            claims = tokenService.parseToken(token);
+        } catch (JwtException jwtException) {
+            log.warn("[verifyToken][token({}) 解析发生异常]", token);
+//            throw exception(TOKEN_PARSE_FAIL);
+            return null;
+        }
+        // token 已经过期
+        if (DateUtils.isExpired(claims.getExpiration())) {
+//            throw exception(TOKEN_EXPIRED);
+            return null;
+        }
+        // 判断 sessionId 是否存在
+        String sessionId = claims.getSubject();
+        if (StrUtil.isBlank(sessionId)) {
+//            throw exception(AUTH_SESSION_ID_NOT_FOUND);
+            return null;
+        }
+        return sessionId;
+    }
+
+    private void refreshLoginUserCache(String sessionId, LoginUser loginUser) {
+        // 每 1/3 的 Session 超时时间,刷新 LoginUser 缓存
+        if (System.currentTimeMillis() - loginUser.getUpdateTime().getTime() <
+                securityProperties.getSessionTimeout().toMillis() / 3) {
+            return;
+        }
+
+        // 重新加载 SysUserDO 信息
+        SysUserDO user = userService.getUser(loginUser.getUserId());
+        if (user == null || UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            throw exception(TOKEN_EXPIRED); // 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登陆界面
+        }
+
+        // 刷新 LoginUser 缓存
+        loginUser.setUpdateTime(new Date());
+        loginUser.setRoleIds(this.getUserRoleIds(loginUser.getUserId()));
+        loginUserRedisDAO.set(sessionId, loginUser);
+    }
+
+}

+ 58 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysTokenServiceImpl.java

@@ -0,0 +1,58 @@
+package cn.iocoder.dashboard.modules.system.service.auth.impl;
+
+import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
+import cn.iocoder.dashboard.modules.system.service.auth.SysTokenService;
+import cn.iocoder.dashboard.util.date.DateUtils;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Token Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class SysTokenServiceImpl implements SysTokenService {
+
+    @Resource
+    private SecurityProperties securityProperties;
+
+    @Override
+    public String createToken(String subject) {
+        return Jwts.builder()
+                .signWith(SignatureAlgorithm.HS512, securityProperties.getTokenSecret())
+                .setExpiration(DateUtils.addTime(securityProperties.getTokenTimeout()))
+                .setSubject(subject)
+                .compact();
+    }
+
+    @Override
+    public Claims parseToken(String token) {
+        return Jwts.parser()
+                .setSigningKey(securityProperties.getTokenSecret())
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    public static void main(String[] args) {
+        String secret = "abcdefghijklmnopqrstuvwxyz";
+        Map<String, Object> map = new HashMap<>();
+        map.put("key1", "value1");
+        System.out.println(Jwts.builder()
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .setClaims(map)
+                .compact());
+
+        System.out.println(Jwts.parser()
+                .setSigningKey(secret)
+                .parseClaimsJws("qyJhbGciOiJIUzUxMiJ9.eyJrZXkxIjoidmFsdWUxIn0.AHWncLRBlJkqrKaoWHZmMgbqYIT7rfLs8KCp9LuC0mdNfnx1xEMm1N9bgcD-0lc5sjySqsKiWzqJ3rpoyUSh0g")
+                .getBody());
+    }
+
+}

+ 166 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java

@@ -0,0 +1,166 @@
+package cn.iocoder.dashboard.modules.system.service.user;
+
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
+
+/**
+ * 用户 Service 接口
+ *
+ * @author ruoyi
+ */
+public interface SysUserService {
+//    /**
+//     * 根据条件分页查询用户列表
+//     *
+//     * @param user 用户信息
+//     * @return 用户信息集合信息
+//     */
+//    public List<SysUser> selectUserList(SysUser user);
+
+    /**
+     * 通过用户名查询用户
+     *
+     * @param username 用户名
+     * @return 用户对象信息
+     */
+    SysUserDO getUserByUserName(String username);
+
+    /**
+     * 通过用户 ID 查询用户
+     *
+     * @param userId 用户ID
+     * @return 用户对象信息
+     */
+    SysUserDO getUser(Long userId);
+
+//
+//    /**
+//     * 根据用户ID查询用户所属角色组
+//     *
+//     * @param userName 用户名
+//     * @return 结果
+//     */
+//    public String selectUserRoleGroup(String userName);
+//
+//    /**
+//     * 根据用户ID查询用户所属岗位组
+//     *
+//     * @param userName 用户名
+//     * @return 结果
+//     */
+//    public String selectUserPostGroup(String userName);
+//
+//    /**
+//     * 校验用户名称是否唯一
+//     *
+//     * @param userName 用户名称
+//     * @return 结果
+//     */
+//    public String checkUserNameUnique(String userName);
+//
+//    /**
+//     * 校验手机号码是否唯一
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public String checkPhoneUnique(SysUser user);
+//
+//    /**
+//     * 校验email是否唯一
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public String checkEmailUnique(SysUser user);
+//
+//    /**
+//     * 校验用户是否允许操作
+//     *
+//     * @param user 用户信息
+//     */
+//    public void checkUserAllowed(SysUser user);
+//
+//    /**
+//     * 新增用户信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public int insertUser(SysUser user);
+//
+//    /**
+//     * 修改用户信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public int updateUser(SysUser user);
+//
+//    /**
+//     * 修改用户状态
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public int updateUserStatus(SysUser user);
+//
+//    /**
+//     * 修改用户基本信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public int updateUserProfile(SysUser user);
+//
+//    /**
+//     * 修改用户头像
+//     *
+//     * @param userName 用户名
+//     * @param avatar 头像地址
+//     * @return 结果
+//     */
+//    public boolean updateUserAvatar(String userName, String avatar);
+//
+//    /**
+//     * 重置用户密码
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    public int resetPwd(SysUser user);
+//
+//    /**
+//     * 重置用户密码
+//     *
+//     * @param userName 用户名
+//     * @param password 密码
+//     * @return 结果
+//     */
+//    public int resetUserPwd(String userName, String password);
+//
+//    /**
+//     * 通过用户ID删除用户
+//     *
+//     * @param userId 用户ID
+//     * @return 结果
+//     */
+//    public int deleteUserById(Long userId);
+//
+//    /**
+//     * 批量删除用户信息
+//     *
+//     * @param userIds 需要删除的用户ID
+//     * @return 结果
+//     */
+//    public int deleteUserByIds(Long[] userIds);
+//
+//    /**
+//     * 导入用户数据
+//     *
+//     * @param userList 用户数据列表
+//     * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+//     * @param operName 操作用户
+//     * @return 结果
+//     */
+//    public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName);
+}

+ 447 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java

@@ -0,0 +1,447 @@
+package cn.iocoder.dashboard.modules.system.service.user;
+
+import cn.iocoder.dashboard.modules.system.dal.mysql.dao.user.SysUserMapper;
+import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+
+/**
+ * 用户 业务层处理
+ *
+ * @author ruoyi
+ */
+@Service
+@Slf4j
+public class SysUserServiceImpl implements SysUserService {
+
+    @Resource
+    private SysUserMapper userMapper;
+
+//    @Autowired
+//    private SysUserMapper userMapper;
+//
+//    @Autowired
+//    private SysRoleMapper roleMapper;
+//
+//    @Autowired
+//    private SysPostMapper postMapper;
+//
+//    @Autowired
+//    private SysUserRoleMapper userRoleMapper;
+//
+//    @Autowired
+//    private SysUserPostMapper userPostMapper;
+//
+//    @Autowired
+//    private ISysConfigService configService;
+
+//    /**
+//     * 根据条件分页查询用户列表
+//     *
+//     * @param user 用户信息
+//     * @return 用户信息集合信息
+//     */
+//    @Override
+//    @DataScope(deptAlias = "d", userAlias = "u")
+//    public List<SysUser> selectUserList(SysUser user)
+//    {
+//        return userMapper.selectUserList(user);
+//    }
+
+    @Override
+    public SysUserDO getUserByUserName(String username) {
+        return userMapper.selectByUsername(username);
+    }
+
+    @Override
+    public SysUserDO getUser(Long userId) {
+        return userMapper.selectById(userId);
+    }
+
+//    /**
+//     * 通过用户ID查询用户
+//     *
+//     * @param userId 用户ID
+//     * @return 用户对象信息
+//     */
+//    @Override
+//    public SysUser selectUserById(Long userId)
+//    {
+//        return userMapper.selectUserById(userId);
+//    }
+//
+//    /**
+//     * 查询用户所属角色组
+//     *
+//     * @param userName 用户名
+//     * @return 结果
+//     */
+//    @Override
+//    public String selectUserRoleGroup(String userName)
+//    {
+//        List<SysRole> list = roleMapper.selectRolesByUserName(userName);
+//        StringBuffer idsStr = new StringBuffer();
+//        for (SysRole role : list)
+//        {
+//            idsStr.append(role.getRoleName()).append(",");
+//        }
+//        if (StringUtils.isNotEmpty(idsStr.toString()))
+//        {
+//            return idsStr.substring(0, idsStr.length() - 1);
+//        }
+//        return idsStr.toString();
+//    }
+//
+//    /**
+//     * 查询用户所属岗位组
+//     *
+//     * @param userName 用户名
+//     * @return 结果
+//     */
+//    @Override
+//    public String selectUserPostGroup(String userName)
+//    {
+//        List<SysPost> list = postMapper.selectPostsByUserName(userName);
+//        StringBuffer idsStr = new StringBuffer();
+//        for (SysPost post : list)
+//        {
+//            idsStr.append(post.getPostName()).append(",");
+//        }
+//        if (StringUtils.isNotEmpty(idsStr.toString()))
+//        {
+//            return idsStr.substring(0, idsStr.length() - 1);
+//        }
+//        return idsStr.toString();
+//    }
+//
+//    /**
+//     * 校验用户名称是否唯一
+//     *
+//     * @param userName 用户名称
+//     * @return 结果
+//     */
+//    @Override
+//    public String checkUserNameUnique(String userName)
+//    {
+//        int count = userMapper.checkUserNameUnique(userName);
+//        if (count > 0)
+//        {
+//            return UserConstants.NOT_UNIQUE;
+//        }
+//        return UserConstants.UNIQUE;
+//    }
+//
+//    /**
+//     * 校验用户名称是否唯一
+//     *
+//     * @param user 用户信息
+//     * @return
+//     */
+//    @Override
+//    public String checkPhoneUnique(SysUser user)
+//    {
+//        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+//        SysUser info = userMapper.checkPhoneUnique(user.getPhonenumber());
+//        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
+//        {
+//            return UserConstants.NOT_UNIQUE;
+//        }
+//        return UserConstants.UNIQUE;
+//    }
+//
+//    /**
+//     * 校验email是否唯一
+//     *
+//     * @param user 用户信息
+//     * @return
+//     */
+//    @Override
+//    public String checkEmailUnique(SysUser user)
+//    {
+//        Long userId = StringUtils.isNull(user.getUserId()) ? -1L : user.getUserId();
+//        SysUser info = userMapper.checkEmailUnique(user.getEmail());
+//        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
+//        {
+//            return UserConstants.NOT_UNIQUE;
+//        }
+//        return UserConstants.UNIQUE;
+//    }
+//
+//    /**
+//     * 校验用户是否允许操作
+//     *
+//     * @param user 用户信息
+//     */
+//    @Override
+//    public void checkUserAllowed(SysUser user)
+//    {
+//        if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin())
+//        {
+//            throw new CustomException("不允许操作超级管理员用户");
+//        }
+//    }
+//
+//    /**
+//     * 新增保存用户信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    @Override
+//    @Transactional
+//    public int insertUser(SysUser user)
+//    {
+//        // 新增用户信息
+//        int rows = userMapper.insertUser(user);
+//        // 新增用户岗位关联
+//        insertUserPost(user);
+//        // 新增用户与角色管理
+//        insertUserRole(user);
+//        return rows;
+//    }
+//
+//    /**
+//     * 修改保存用户信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    @Override
+//    @Transactional
+//    public int updateUser(SysUser user)
+//    {
+//        Long userId = user.getUserId();
+//        // 删除用户与角色关联
+//        userRoleMapper.deleteUserRoleByUserId(userId);
+//        // 新增用户与角色管理
+//        insertUserRole(user);
+//        // 删除用户与岗位关联
+//        userPostMapper.deleteUserPostByUserId(userId);
+//        // 新增用户与岗位管理
+//        insertUserPost(user);
+//        return userMapper.updateUser(user);
+//    }
+//
+//    /**
+//     * 修改用户状态
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    @Override
+//    public int updateUserStatus(SysUser user)
+//    {
+//        return userMapper.updateUser(user);
+//    }
+//
+//    /**
+//     * 修改用户基本信息
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    @Override
+//    public int updateUserProfile(SysUser user)
+//    {
+//        return userMapper.updateUser(user);
+//    }
+//
+//    /**
+//     * 修改用户头像
+//     *
+//     * @param userName 用户名
+//     * @param avatar 头像地址
+//     * @return 结果
+//     */
+//    @Override
+//    public boolean updateUserAvatar(String userName, String avatar)
+//    {
+//        return userMapper.updateUserAvatar(userName, avatar) > 0;
+//    }
+//
+//    /**
+//     * 重置用户密码
+//     *
+//     * @param user 用户信息
+//     * @return 结果
+//     */
+//    @Override
+//    public int resetPwd(SysUser user)
+//    {
+//        return userMapper.updateUser(user);
+//    }
+//
+//    /**
+//     * 重置用户密码
+//     *
+//     * @param userName 用户名
+//     * @param password 密码
+//     * @return 结果
+//     */
+//    @Override
+//    public int resetUserPwd(String userName, String password)
+//    {
+//        return userMapper.resetUserPwd(userName, password);
+//    }
+//
+//    /**
+//     * 新增用户角色信息
+//     *
+//     * @param user 用户对象
+//     */
+//    public void insertUserRole(SysUser user)
+//    {
+//        Long[] roles = user.getRoleIds();
+//        if (StringUtils.isNotNull(roles))
+//        {
+//            // 新增用户与角色管理
+//            List<SysUserRole> list = new ArrayList<SysUserRole>();
+//            for (Long roleId : roles)
+//            {
+//                SysUserRole ur = new SysUserRole();
+//                ur.setUserId(user.getUserId());
+//                ur.setRoleId(roleId);
+//                list.add(ur);
+//            }
+//            if (list.size() > 0)
+//            {
+//                userRoleMapper.batchUserRole(list);
+//            }
+//        }
+//    }
+//
+//    /**
+//     * 新增用户岗位信息
+//     *
+//     * @param user 用户对象
+//     */
+//    public void insertUserPost(SysUser user)
+//    {
+//        Long[] posts = user.getPostIds();
+//        if (StringUtils.isNotNull(posts))
+//        {
+//            // 新增用户与岗位管理
+//            List<SysUserPost> list = new ArrayList<SysUserPost>();
+//            for (Long postId : posts)
+//            {
+//                SysUserPost up = new SysUserPost();
+//                up.setUserId(user.getUserId());
+//                up.setPostId(postId);
+//                list.add(up);
+//            }
+//            if (list.size() > 0)
+//            {
+//                userPostMapper.batchUserPost(list);
+//            }
+//        }
+//    }
+//
+//    /**
+//     * 通过用户ID删除用户
+//     *
+//     * @param userId 用户ID
+//     * @return 结果
+//     */
+//    @Override
+//    @Transactional
+//    public int deleteUserById(Long userId)
+//    {
+//        // 删除用户与角色关联
+//        userRoleMapper.deleteUserRoleByUserId(userId);
+//        // 删除用户与岗位表
+//        userPostMapper.deleteUserPostByUserId(userId);
+//        return userMapper.deleteUserById(userId);
+//    }
+//
+//    /**
+//     * 批量删除用户信息
+//     *
+//     * @param userIds 需要删除的用户ID
+//     * @return 结果
+//     */
+//    @Override
+//    @Transactional
+//    public int deleteUserByIds(Long[] userIds)
+//    {
+//        for (Long userId : userIds)
+//        {
+//            checkUserAllowed(new SysUser(userId));
+//        }
+//        // 删除用户与角色关联
+//        userRoleMapper.deleteUserRole(userIds);
+//        // 删除用户与岗位关联
+//        userPostMapper.deleteUserPost(userIds);
+//        return userMapper.deleteUserByIds(userIds);
+//    }
+//
+//    /**
+//     * 导入用户数据
+//     *
+//     * @param userList 用户数据列表
+//     * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据
+//     * @param operName 操作用户
+//     * @return 结果
+//     */
+//    @Override
+//    public String importUser(List<SysUser> userList, Boolean isUpdateSupport, String operName)
+//    {
+//        if (StringUtils.isNull(userList) || userList.size() == 0)
+//        {
+//            throw new CustomException("导入用户数据不能为空!");
+//        }
+//        int successNum = 0;
+//        int failureNum = 0;
+//        StringBuilder successMsg = new StringBuilder();
+//        StringBuilder failureMsg = new StringBuilder();
+//        String password = configService.selectConfigByKey("sys.user.initPassword");
+//        for (SysUser user : userList)
+//        {
+//            try
+//            {
+//                // 验证是否存在这个用户
+//                SysUser u = userMapper.selectUserByUserName(user.getUserName());
+//                if (StringUtils.isNull(u))
+//                {
+//                    user.setPassword(SecurityUtils.encryptPassword(password));
+//                    user.setCreateBy(operName);
+//                    this.insertUser(user);
+//                    successNum++;
+//                    successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 导入成功");
+//                }
+//                else if (isUpdateSupport)
+//                {
+//                    user.setUpdateBy(operName);
+//                    this.updateUser(user);
+//                    successNum++;
+//                    successMsg.append("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
+//                }
+//                else
+//                {
+//                    failureNum++;
+//                    failureMsg.append("<br/>" + failureNum + "、账号 " + user.getUserName() + " 已存在");
+//                }
+//            }
+//            catch (Exception e)
+//            {
+//                failureNum++;
+//                String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
+//                failureMsg.append(msg + e.getMessage());
+//                log.error(msg, e);
+//            }
+//        }
+//        if (failureNum > 0)
+//        {
+//            failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+//            throw new CustomException(failureMsg.toString());
+//        }
+//        else
+//        {
+//            successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+//        }
+//        return successMsg.toString();
+//    }
+
+}

+ 19 - 0
src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java

@@ -0,0 +1,19 @@
+package cn.iocoder.dashboard.util.date;
+
+import java.time.Duration;
+import java.util.Date;
+
+/**
+ * 时间工具类
+ */
+public class DateUtils {
+
+    public static Date addTime(Duration duration) {
+        return new Date(System.currentTimeMillis() + duration.toMillis());
+    }
+
+    public static boolean isExpired(Date time) {
+        return System.currentTimeMillis() > time.getTime();
+    }
+
+}

+ 23 - 0
src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java

@@ -0,0 +1,23 @@
+package cn.iocoder.dashboard.util.servlet;
+
+import cn.hutool.extra.servlet.ServletUtil;
+import com.alibaba.fastjson.JSON;
+import org.springframework.http.MediaType;
+
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 客户端工具类
+ *
+ * @author 芋道源码
+ */
+public class ServletUtils {
+
+    @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
+    public static void writeJSON(HttpServletResponse response, Object object) {
+        String content = JSON.toJSONString(object);
+        ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE);
+    }
+
+}

+ 2 - 0
src/main/resources/application.yaml

@@ -27,6 +27,8 @@ yudao:
     token-secret: abcdefghijklmnopqrstuvwxyz
     token-timeout: 1d
     session-timeout: 30m
+    mock-enable: true
+    mock-secret: yudaoyuanma
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能