From bf8534d2174ce77abb12d825a97d28db7587bd51 Mon Sep 17 00:00:00 2001 From: "Yangkai.Shen" <237497819@qq.com> Date: Tue, 11 Dec 2018 17:46:54 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20spring-boot-demo-rbac-security=20?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=AE=8C=E6=88=90=201.=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=20redis=EF=BC=8C=E6=B6=88=E9=99=A4=20JWT=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E8=BF=87=E6=9C=9F=E7=9A=84=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=202.=20=E4=BF=9D=E8=AF=81=E4=B8=80=E4=B8=AA=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E6=97=B6=E9=97=B4=E5=8F=AA=E8=83=BD=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E4=B8=80=E4=B8=AA=E8=AE=BE=E5=A4=87=E7=99=BB=E5=BD=95?= =?UTF-8?q?=203.=20=E6=94=AF=E6=8C=81=E2=80=9C=E8=AE=B0=E4=BD=8F=E6=88=91?= =?UTF-8?q?=E2=80=9D=E6=93=8D=E4=BD=9C=EF=BC=8C=E5=BC=80=E5=90=AF=E5=90=8E?= =?UTF-8?q?=EF=BC=8CJWT=E8=BF=87=E6=9C=9F=E6=97=B6=E9=97=B4=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=B8=BA7=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-boot-demo-rbac-security/pom.xml | 11 +++ .../rbac/security/common/BaseException.java | 2 +- .../xkcoding/rbac/security/common/Consts.java | 5 ++ .../xkcoding/rbac/security/common/Status.java | 58 +++++++------ .../config/JwtAuthenticationFilter.java | 9 +-- .../rbac/security/config/JwtConfig.java | 9 ++- .../security/config/RbacAuthorityService.java | 7 +- .../rbac/security/config/RedisConfig.java | 44 ++++++++++ .../rbac/security/config/SecurityConfig.java | 14 +--- .../config/SecurityHandlerConfig.java | 12 +-- .../security/controller/AuthController.java | 17 +++- .../security/exception/SecurityException.java | 6 +- .../handler/GlobalExceptionHandler.java | 5 ++ .../rbac/security/payload/LoginRequest.java | 9 ++- .../xkcoding/rbac/security/util/JwtUtil.java | 81 ++++++++++++++++--- .../rbac/security/util/ResponseUtil.java | 9 ++- .../src/main/resources/application.yml | 22 +++++ 17 files changed, 242 insertions(+), 78 deletions(-) create mode 100644 spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java diff --git a/spring-boot-demo-rbac-security/pom.xml b/spring-boot-demo-rbac-security/pom.xml index ef64e4a..91d894c 100644 --- a/spring-boot-demo-rbac-security/pom.xml +++ b/spring-boot-demo-rbac-security/pom.xml @@ -39,6 +39,17 @@ spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + org.springframework.boot spring-boot-configuration-processor diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java index 77a2901..90547ef 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java @@ -16,8 +16,8 @@ import lombok.EqualsAndHashCode; * @version: V1.0 * @modified: yangkai.shen */ -@Data @EqualsAndHashCode(callSuper = true) +@Data public class BaseException extends RuntimeException { private Integer code; private String message; diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java index cc67517..6b5efd9 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java @@ -32,4 +32,9 @@ public interface Consts { * 按钮 */ Integer BUTTON = 2; + + /** + * JWT 在 Redis 中保存的key前缀 + */ + String REDIS_JWT_KEY_PREFIX = "security:jwt:"; } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java index a871b20..7b06bfd 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java @@ -18,49 +18,54 @@ import lombok.Getter; @Getter public enum Status implements IStatus { /** - * 操作成功 + * 操作成功! */ - SUCCESS(200, "操作成功"), + SUCCESS(200, "操作成功!"), /** - * 操作异常 + * 操作异常! */ - ERROR(500, "操作异常"), + ERROR(500, "操作异常!"), /** - * 退出成功 + * 退出成功! */ - LOGOUT(200, "退出成功"), + LOGOUT(200, "退出成功!"), /** - * 请先登录 + * 请先登录! */ - UNAUTHORIZED(401, "请先登录"), + UNAUTHORIZED(401, "请先登录!"), /** - * 暂无权限访问 + * 暂无权限访问! */ - ACCESS_DENIED(403, "权限不足"), + ACCESS_DENIED(403, "权限不足!"), /** - * 请求不存在 + * 请求不存在! */ - REQUEST_NOT_FOUND(404, "请求不存在"), + REQUEST_NOT_FOUND(404, "请求不存在!"), /** - * 请求异常 + * 请求方式不支持! */ - BAD_REQUEST(400, "请求异常"), + HTTP_BAD_METHOD(405, "请求方式不支持!"), /** - * 参数不匹配 + * 请求异常! */ - PARAM_NOT_MATCH(400, "参数不匹配"), + BAD_REQUEST(400, "请求异常!"), /** - * 参数不能为空 + * 参数不匹配! */ - PARAM_NOT_NULL(400, "参数不能为空"), + PARAM_NOT_MATCH(400, "参数不匹配!"), + + /** + * 参数不能为空! + */ + PARAM_NOT_NULL(400, "参数不能为空!"), /** * 当前用户已被锁定,请联系管理员解锁! @@ -68,19 +73,24 @@ public enum Status implements IStatus { USER_DISABLED(403, "当前用户已被锁定,请联系管理员解锁!"), /** - * 用户名或密码错误 + * 用户名或密码错误! + */ + USERNAME_PASSWORD_ERROR(5001, "用户名或密码错误!"), + + /** + * token 已过期,请重新登录! */ - USERNAME_PASSWORD_ERROR(5001, "用户名或密码错误"), + TOKEN_EXPIRED(5002, "token 已过期,请重新登录!"), /** - * token 已过期,请重新登录 + * token 解析失败,请尝试重新登录! */ - TOKEN_EXPIRED(5002, "token 已过期,请重新登录"), + TOKEN_PARSE_ERROR(5002, "token 解析失败,请尝试重新登录!"), /** - * token 解析失败,请尝试重新登录 + * 当前用户已在别处登录,请尝试更改密码或重新登录! */ - TOKEN_PARSE_ERROR(5002, "token 解析失败,请尝试重新登录"); + TOKEN_OUT_OF_CTRL(5003,"当前用户已在别处登录,请尝试更改密码或重新登录!"); /** * 状态码 diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java index 795ee3e..845772c 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java @@ -50,7 +50,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { if (antPathMatcher.match("/**/api/auth/**", request.getRequestURI())) { filterChain.doFilter(request, response); } else { - String jwt = getJwtFromRequest(request); + String jwt = jwtUtil.getJwtFromRequest(request); if (StrUtil.isNotBlank(jwt)) { try { @@ -72,11 +72,4 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } - private String getJwtFromRequest(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java index 0bb1df1..1cc0988 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java @@ -20,12 +20,17 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @Data public class JwtConfig { /** - * jwt 加密 key, 默认值:xkcoding. + * jwt 加密 key,默认值:xkcoding. */ private String key = "xkcoding"; /** - * jwt 过期时间, 默认值:600000 {@code 10 分钟}. + * jwt 过期时间,默认值:600000 {@code 10 分钟}. */ private Long ttl = 600000L; + + /** + * 开启 记住我 之后 jwt 过期时间,默认值 604800000 {@code 7 天} + */ + private Long remember = 604800000L; } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java index 52b355d..2c74b2f 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java @@ -55,13 +55,16 @@ public class RbacAuthorityService { //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 List btnPerms = permissions.stream() + // 过滤页面权限 .filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) - .filter(permission -> StrUtil.isNotBlank(permission.getPermission())) + // 过滤 URL 为空 + .filter(permission -> StrUtil.isNotBlank(permission.getUrl())) + // 过滤 METHOD 为空 .filter(permission -> StrUtil.isNotBlank(permission.getMethod())) .collect(Collectors.toList()); for (Permission btnPerm : btnPerms) { - AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getPermission(), btnPerm.getMethod()); + AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod()); if (antPathMatcher.matches(request)) { hasPermission = true; break; diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java new file mode 100644 index 0000000..5ab166e --- /dev/null +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java @@ -0,0 +1,44 @@ +package com.xkcoding.rbac.security.config; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.io.Serializable; + +/** + *

+ * redis配置 + *

+ * + * @package: com.xkcoding.rbac.security.config + * @description: redis配置 + * @author: yangkai.shen + * @date: Created in 2018-12-11 15:16 + * @copyright: Copyright (c) 2018 + * @version: V1.0 + * @modified: yangkai.shen + */ +@Configuration +@AutoConfigureAfter(RedisAutoConfiguration.class) +@EnableCaching +public class RedisConfig { + + /** + * 默认情况下的模板只能支持RedisTemplate,也就是只能存入字符串,因此支持序列化 + */ + @Bean + public RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + return template; + } +} diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java index ebc8f60..f5fdcd7 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java @@ -32,9 +32,6 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { - @Autowired - private LogoutSuccessHandler logoutSuccessHandler; - @Autowired private AccessDeniedHandler accessDeniedHandler; @@ -70,6 +67,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { .csrf() .disable() + // 登录行为由自己实现,参考 AuthController#login .formLogin() .disable() .httpBasic() @@ -86,17 +84,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { .anyRequest() .access("@rbacAuthorityService.hasPermission(request,authentication)") - // 登出处理 + // 登出行为由自己实现,参考 AuthController#logout .and() - .logout() - // 登出请求默认为POST请求,改为GET请求 - .logoutRequestMatcher(new AntPathRequestMatcher("/**/logout", "GET")) - // 登出成功处理器 - .logoutSuccessHandler(logoutSuccessHandler) - .permitAll() + .logout().disable() // Session 管理 - .and() .sessionManagement() // 因为使用了JWT,所以这里不管理Session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java index cc7eb17..2db9de8 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java @@ -5,7 +5,6 @@ import com.xkcoding.rbac.security.util.ResponseUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; /** *

@@ -23,18 +22,9 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl @Configuration public class SecurityHandlerConfig { - /** - * 退出成功处理器 - * - * @return 退出成功处理器 - */ - @Bean - public LogoutSuccessHandler logoutSuccessHandler() { - return (request, response, authentication) -> ResponseUtil.renderJson(response, Status.LOGOUT, null); - } - @Bean public AccessDeniedHandler accessDeniedHandler() { return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, Status.ACCESS_DENIED, null); } + } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java index db3605e..6e5f578 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java @@ -1,11 +1,14 @@ package com.xkcoding.rbac.security.controller; import com.xkcoding.rbac.security.common.ApiResponse; +import com.xkcoding.rbac.security.common.Status; +import com.xkcoding.rbac.security.exception.SecurityException; import com.xkcoding.rbac.security.payload.LoginRequest; import com.xkcoding.rbac.security.util.JwtUtil; import com.xkcoding.rbac.security.vo.JwtResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -15,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; /** @@ -51,7 +55,18 @@ public class AuthController { SecurityContextHolder.getContext() .setAuthentication(authentication); - String jwt = jwtUtil.createJWT(authentication); + String jwt = jwtUtil.createJWT(authentication,loginRequest.getRememberMe()); return ApiResponse.ofSuccess(new JwtResponse(jwt)); } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + try { + // 设置JWT过期 + jwtUtil.invalidateJWT(request); + } catch (SecurityException e) { + throw new SecurityException(Status.UNAUTHORIZED); + } + return ApiResponse.ofStatus(Status.LOGOUT); + } } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java index 884d643..4b02465 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java @@ -2,7 +2,8 @@ package com.xkcoding.rbac.security.exception; import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.Status; -import lombok.Getter; +import lombok.Data; +import lombok.EqualsAndHashCode; /** *

@@ -17,7 +18,8 @@ import lombok.Getter; * @version: V1.0 * @modified: yangkai.shen */ -@Getter +@EqualsAndHashCode(callSuper = true) +@Data public class SecurityException extends BaseException { public SecurityException(Status status) { super(status); diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java index e38b98d..909638c 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.xkcoding.rbac.security.exception.handler; import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.Status; @@ -8,6 +9,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -40,6 +42,9 @@ public class GlobalExceptionHandler { if (e instanceof NoHandlerFoundException) { log.error("【全局异常拦截】NoHandlerFoundException: 请求方法 {}, 请求路径 {}", ((NoHandlerFoundException) e).getRequestURL(), ((NoHandlerFoundException) e).getHttpMethod()); return ApiResponse.ofStatus(Status.REQUEST_NOT_FOUND); + } else if (e instanceof HttpRequestMethodNotSupportedException) { + log.error("【全局异常拦截】HttpRequestMethodNotSupportedException: 当前请求方式 {}, 支持请求方式 {}", ((HttpRequestMethodNotSupportedException) e).getMethod(), JSONUtil.toJsonStr(((HttpRequestMethodNotSupportedException) e).getSupportedHttpMethods())); + return ApiResponse.ofStatus(Status.HTTP_BAD_METHOD); } else if (e instanceof MethodArgumentNotValidException) { log.error("【全局异常拦截】MethodArgumentNotValidException", e); return ApiResponse.of(Status.BAD_REQUEST.getCode(), ((MethodArgumentNotValidException) e).getBindingResult() diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java index eb473d0..3c9a0c5 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java @@ -23,13 +23,18 @@ public class LoginRequest { /** * 用户名或邮箱或手机号 */ - @NotBlank + @NotBlank(message = "用户名不能为空") private String usernameOrEmailOrPhone; /** * 密码 */ - @NotBlank + @NotBlank(message = "密码不能为空") private String password; + /** + * 记住我 + */ + private Boolean rememberMe = false; + } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java index c66c398..305279c 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java @@ -1,6 +1,8 @@ package com.xkcoding.rbac.security.util; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.xkcoding.rbac.security.common.Consts; import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.config.JwtConfig; import com.xkcoding.rbac.security.exception.SecurityException; @@ -10,12 +12,16 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; /** *

@@ -34,23 +40,23 @@ import java.util.List; @Configuration @Slf4j public class JwtUtil { - private final JwtConfig jwtConfig; + @Autowired + private JwtConfig jwtConfig; @Autowired - public JwtUtil(JwtConfig jwtConfig) { - this.jwtConfig = jwtConfig; - } + private StringRedisTemplate stringRedisTemplate; /** * 创建JWT * + * @param rememberMe 记住我 * @param id 用户id * @param subject 用户名 * @param roles 用户角色 * @param authorities 用户权限 * @return JWT */ - public String createJWT(Long id, String subject, List roles, Collection authorities) { + public String createJWT(Boolean rememberMe, Long id, String subject, List roles, Collection authorities) { Date now = new Date(); JwtBuilder builder = Jwts.builder() .setId(id.toString()) @@ -59,22 +65,30 @@ public class JwtUtil { .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) .claim("roles", roles) .claim("authorities", authorities); - if (jwtConfig.getTtl() > 0) { - builder.setExpiration(DateUtil.offsetMillisecond(now, jwtConfig.getTtl() - .intValue())); + + // 设置过期时间 + Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl(); + if (ttl > 0) { + builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue())); } - return builder.compact(); + + String jwt = builder.compact(); + // 将生成的JWT保存至Redis + stringRedisTemplate.opsForValue() + .set(Consts.REDIS_JWT_KEY_PREFIX + subject, jwt, ttl, TimeUnit.MILLISECONDS); + return jwt; } /** * 创建JWT * * @param authentication 用户认证信息 + * @param rememberMe 记住我 * @return JWT */ - public String createJWT(Authentication authentication) { + public String createJWT(Authentication authentication, Boolean rememberMe) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); - return createJWT(userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); + return createJWT(rememberMe, userPrincipal.getId(), userPrincipal.getUsername(), userPrincipal.getRoles(), userPrincipal.getAuthorities()); } /** @@ -85,10 +99,27 @@ public class JwtUtil { */ public Claims parseJWT(String jwt) { try { - return Jwts.parser() + Claims claims = Jwts.parser() .setSigningKey(jwtConfig.getKey()) .parseClaimsJws(jwt) .getBody(); + + String username = claims.getSubject(); + String redisKey = Consts.REDIS_JWT_KEY_PREFIX + username; + + // 校验redis中的JWT是否存在 + Long expire = stringRedisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS); + if (Objects.isNull(expire) || expire <= 0) { + throw new SecurityException(Status.TOKEN_EXPIRED); + } + + // 校验redis中的JWT是否与当前的一致,不一致则代表用户已注销/用户在不同设备登录,均代表JWT已过期 + String redisToken = stringRedisTemplate.opsForValue() + .get(redisKey); + if (!StrUtil.equals(jwt, redisToken)) { + throw new SecurityException(Status.TOKEN_OUT_OF_CTRL); + } + return claims; } catch (ExpiredJwtException e) { log.error("Token 已过期"); throw new SecurityException(Status.TOKEN_EXPIRED); @@ -107,6 +138,18 @@ public class JwtUtil { } } + /** + * 设置JWT过期 + * + * @param request 请求 + */ + public void invalidateJWT(HttpServletRequest request) { + String jwt = getJwtFromRequest(request); + String username = getUsernameFromJWT(jwt); + // 从redis中清除JWT + stringRedisTemplate.delete(Consts.REDIS_JWT_KEY_PREFIX + username); + } + /** * 根据 jwt 获取用户名 * @@ -118,4 +161,18 @@ public class JwtUtil { return claims.getSubject(); } + /** + * 从 request 的 header 中获取 JWT + * + * @param request 请求 + * @return JWT + */ + public String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + } diff --git a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java index 0d9a879..1321113 100644 --- a/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java +++ b/spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java @@ -1,5 +1,6 @@ package com.xkcoding.rbac.security.util; +import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.BaseException; @@ -39,8 +40,10 @@ public class ResponseUtil { response.setContentType("application/json;charset=UTF-8"); response.setStatus(200); + // FIXME: hutool 的 BUG:JSONUtil.toJsonStr() + // 将JSON转为String的时候,忽略null值的时候转成的String存在错误 response.getWriter() - .write(JSONUtil.toJsonStr(ApiResponse.ofStatus(status, data))); + .write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofStatus(status, data), true))); } catch (IOException e) { log.error("Response写出JSON异常,", e); } @@ -59,8 +62,10 @@ public class ResponseUtil { response.setContentType("application/json;charset=UTF-8"); response.setStatus(200); + // FIXME: hutool 的 BUG:JSONUtil.toJsonStr() + // 将JSON转为String的时候,忽略null值的时候转成的String存在错误 response.getWriter() - .write(JSONUtil.toJsonStr(ApiResponse.ofException(exception))); + .write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofException(exception), true))); } catch (IOException e) { log.error("Response写出JSON异常,", e); } diff --git a/spring-boot-demo-rbac-security/src/main/resources/application.yml b/spring-boot-demo-rbac-security/src/main/resources/application.yml index fc104da..4415e53 100644 --- a/spring-boot-demo-rbac-security/src/main/resources/application.yml +++ b/spring-boot-demo-rbac-security/src/main/resources/application.yml @@ -18,7 +18,29 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQL57InnoDBDialect + resources: + add-mappings: false + mvc: + throw-exception-if-no-handler-found: true + redis: + host: localhost + port: 6379 + # 连接超时时间(记得添加单位,Duration) + timeout: 10000ms + # Redis默认情况下有16个分片,这里配置具体使用的分片 + # database: 0 + lettuce: + pool: + # 连接池最大连接数(使用负值表示没有限制) 默认 8 + max-active: 8 + # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 + max-wait: -1ms + # 连接池中的最大空闲连接 默认 8 + max-idle: 8 + # 连接池中的最小空闲连接 默认 0 + min-idle: 0 jwt: config: key: xkcoding ttl: 600000 + remember: 604800000