From fc8eac548a698174554892f6bba8ca9758cf34f9 Mon Sep 17 00:00:00 2001 From: YunaiV <> Date: Sun, 3 Jan 2021 12:15:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=20Spring=20Security=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=87=8D=E6=9E=84=E5=90=8E=EF=BC=8C?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E9=80=BB=E8=BE=91=E6=9B=B4=E5=8A=A0=E6=B8=85?= =?UTF-8?q?=E6=99=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http-client.env.json | 2 +- .../config/SecurityConfiguration.java | 295 ++++++------ .../security/config/SecurityProperties.java | 52 ++ .../framework/security/core/LoginUser.java | 90 ++++ .../filter/JwtAuthenticationTokenFilter.java | 89 ++++ .../core/handler/AccessDeniedHandlerImpl.java | 44 ++ .../handler/AuthenticationEntryPointImpl.java | 37 ++ .../handler/LogoutSuccessHandlerImpl.java | 42 ++ .../service/SecurityFrameworkService.java | 35 ++ .../security/core/util/SecurityUtils.java | 59 +++ .../framework/security/package-info.java | 7 + ...pring Boot 安全框架 Spring Security 入门》.md | 2 + .../system/convert/auth/SysAuthConvert.java | 17 + .../system/dal/mysql/dao/package-info.java | 1 + .../dal/mysql/dao/user/SysUserMapper.java | 15 + .../dal/mysql/dataobject/user/SysUserDO.java | 154 ++++++ .../system/enums/SysErrorCodeConstants.java | 21 + .../system/enums/user}/UserStatus.java | 60 +-- .../system/service/auth/SysAuthService.java | 16 + .../system/service/auth/SysTokenService.java | 30 ++ .../service/auth/impl/SysAuthServiceImpl.java | 215 +++++++++ .../auth/impl/SysTokenServiceImpl.java | 58 +++ .../system/service/user/SysUserService.java | 166 +++++++ .../service/user/SysUserServiceImpl.java | 447 ++++++++++++++++++ .../dashboard/util/date/DateUtils.java | 19 + .../dashboard/util/servlet/ServletUtils.java | 23 + src/main/resources/application.yaml | 2 + 27 files changed, 1821 insertions(+), 177 deletions(-) rename ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java => src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java (60%) create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityUtils.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/package-info.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/package-info.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/user/SysUserMapper.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/user/SysUserDO.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java rename {ruoyi-common/src/main/java/com/ruoyi/common/enums => src/main/java/cn/iocoder/dashboard/modules/system/enums/user}/UserStatus.java (86%) create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysTokenService.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysTokenServiceImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java create mode 100644 src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java create mode 100644 src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java diff --git a/http-client.env.json b/http-client.env.json index 8cad13896..33c8482f6 100644 --- a/http-client.env.json +++ b/http-client.env.json @@ -1,6 +1,6 @@ { "local": { "baseUrl": "http://127.0.0.1:8080/api", - "token": "eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2MDk2ODE2MzEsInN1YiI6ImE3ZGE1MWE2YWUyYTQxOWRhNmExYTlkYmJiMTVmZjc4In0.RXG7alSz64lE9oPSgbnYT_KsX7kvoHVhF5oHxXHztr1KjsttOqOppSmHGBYFI7Y75bsjEBSxSqbGsS1O1S2b1w" + "token": "yudaoyuanma1" } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java similarity index 60% rename from ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java rename to src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java index 786611507..f1fb68dca 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.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 Password Encoding with Spring Security + */ + @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); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java b/src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityProperties.java new file mode 100644 index 000000000..6aa83bfb8 --- /dev/null +++ b/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"; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/LoginUser.java new file mode 100644 index 000000000..316a12ba0 --- /dev/null +++ b/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 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 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 判断 + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 000000000..506c7caea --- /dev/null +++ b/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); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 000000000..8698ef2de --- /dev/null +++ b/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)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 000000000..6545a95dc --- /dev/null +++ b/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)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java new file mode 100644 index 000000000..59a49207f --- /dev/null +++ b/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(), "退出成功"))); + } +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 000000000..b77a5b4a0 --- /dev/null +++ b/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); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityUtils.java b/src/main/java/cn/iocoder/dashboard/framework/security/core/util/SecurityUtils.java new file mode 100644 index 000000000..52989c05f --- /dev/null +++ b/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); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java new file mode 100644 index 000000000..bc4451641 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package cn.iocoder.dashboard.framework.security; diff --git a/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 000000000..dddf73635 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java b/src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java new file mode 100644 index 000000000..789bef1c6 --- /dev/null +++ b/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); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/package-info.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/package-info.java new file mode 100644 index 000000000..5d6e72f0e --- /dev/null +++ b/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; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/user/SysUserMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/user/SysUserMapper.java new file mode 100644 index 000000000..de4bbfd69 --- /dev/null +++ b/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 { + + default SysUserDO selectByUsername(String username) { + return selectOne(new QueryWrapper().eq("username", username)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/user/SysUserDO.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dataobject/user/SysUserDO.java new file mode 100644 index 000000000..76b8743d9 --- /dev/null +++ b/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 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; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java new file mode 100644 index 000000000..9f6e9b991 --- /dev/null +++ b/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 解析失败"); + +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java b/src/main/java/cn/iocoder/dashboard/modules/system/enums/user/UserStatus.java similarity index 86% rename from ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java rename to src/main/java/cn/iocoder/dashboard/modules/system/enums/user/UserStatus.java index a4613ff9c..4616034e2 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/enums/UserStatus.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/enums/user/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; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java new file mode 100644 index 000000000..e212554a9 --- /dev/null +++ b/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); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysTokenService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysTokenService.java new file mode 100644 index 000000000..c1f8c8d91 --- /dev/null +++ b/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); + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysAuthServiceImpl.java new file mode 100644 index 000000000..9b2065d30 --- /dev/null +++ b/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 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); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysTokenServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/auth/impl/SysTokenServiceImpl.java new file mode 100644 index 000000000..3b3112f60 --- /dev/null +++ b/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 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()); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java new file mode 100644 index 000000000..f9d34486b --- /dev/null +++ b/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 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 userList, Boolean isUpdateSupport, String operName); +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java new file mode 100644 index 000000000..09afeb521 --- /dev/null +++ b/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 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 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 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 list = new ArrayList(); +// 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 list = new ArrayList(); +// 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 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("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); +// } +// else if (isUpdateSupport) +// { +// user.setUpdateBy(operName); +// this.updateUser(user); +// successNum++; +// successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); +// } +// else +// { +// failureNum++; +// failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); +// } +// } +// catch (Exception e) +// { +// failureNum++; +// String msg = "
" + 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(); +// } + +} diff --git a/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java b/src/main/java/cn/iocoder/dashboard/util/date/DateUtils.java new file mode 100644 index 000000000..611ef05c6 --- /dev/null +++ b/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(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java b/src/main/java/cn/iocoder/dashboard/util/servlet/ServletUtils.java new file mode 100644 index 000000000..9e648c3f7 --- /dev/null +++ b/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); + } + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 95f968d37..0e82dca16 100644 --- a/src/main/resources/application.yaml +++ b/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: 提供管理员管理的所有功能