Browse Source

spring-boot-demo-rbac-security 基本完成

1. 引入 redis,消除 JWT 无法手动过期的缺陷
2. 保证一个用户同一时间只能保证一个设备登录
3. 支持“记住我”操作,开启后,JWT过期时间默认为7天
pull/1/head
Yangkai.Shen 5 years ago
parent
commit
bf8534d217
17 changed files with 242 additions and 78 deletions
  1. +11
    -0
      spring-boot-demo-rbac-security/pom.xml
  2. +1
    -1
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java
  3. +5
    -0
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java
  4. +34
    -24
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java
  5. +1
    -8
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java
  6. +7
    -2
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java
  7. +5
    -2
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java
  8. +44
    -0
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java
  9. +3
    -11
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java
  10. +1
    -11
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java
  11. +16
    -1
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java
  12. +4
    -2
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java
  13. +5
    -0
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java
  14. +7
    -2
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java
  15. +69
    -12
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java
  16. +7
    -2
      spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java
  17. +22
    -0
      spring-boot-demo-rbac-security/src/main/resources/application.yml

+ 11
- 0
spring-boot-demo-rbac-security/pom.xml View File

@@ -39,6 +39,17 @@
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> </dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 对象池,使用redis时必须引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>


+ 1
- 1
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/BaseException.java View File

@@ -16,8 +16,8 @@ import lombok.EqualsAndHashCode;
* @version: V1.0 * @version: V1.0
* @modified: yangkai.shen * @modified: yangkai.shen
*/ */
@Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data
public class BaseException extends RuntimeException { public class BaseException extends RuntimeException {
private Integer code; private Integer code;
private String message; private String message;


+ 5
- 0
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Consts.java View File

@@ -32,4 +32,9 @@ public interface Consts {
* 按钮 * 按钮
*/ */
Integer BUTTON = 2; Integer BUTTON = 2;

/**
* JWT 在 Redis 中保存的key前缀
*/
String REDIS_JWT_KEY_PREFIX = "security:jwt:";
} }

+ 34
- 24
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/common/Status.java View File

@@ -18,49 +18,54 @@ import lombok.Getter;
@Getter @Getter
public enum Status implements IStatus { 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, "当前用户已被锁定,请联系管理员解锁!"), 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,"当前用户已在别处登录,请尝试更改密码或重新登录!");


/** /**
* 状态码 * 状态码


+ 1
- 8
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtAuthenticationFilter.java View File

@@ -50,7 +50,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (antPathMatcher.match("/**/api/auth/**", request.getRequestURI())) { if (antPathMatcher.match("/**/api/auth/**", request.getRequestURI())) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} else { } else {
String jwt = getJwtFromRequest(request);
String jwt = jwtUtil.getJwtFromRequest(request);


if (StrUtil.isNotBlank(jwt)) { if (StrUtil.isNotBlank(jwt)) {
try { 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;
}
} }

+ 7
- 2
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/JwtConfig.java View File

@@ -20,12 +20,17 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @Data
public class JwtConfig { public class JwtConfig {
/** /**
* jwt 加密 key, 默认值:xkcoding.
* jwt 加密 key默认值:xkcoding.
*/ */
private String key = "xkcoding"; private String key = "xkcoding";


/** /**
* jwt 过期时间, 默认值:600000 {@code 10 分钟}.
* jwt 过期时间默认值:600000 {@code 10 分钟}.
*/ */
private Long ttl = 600000L; private Long ttl = 600000L;

/**
* 开启 记住我 之后 jwt 过期时间,默认值 604800000 {@code 7 天}
*/
private Long remember = 604800000L;
} }

+ 5
- 2
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RbacAuthorityService.java View File

@@ -55,13 +55,16 @@ public class RbacAuthorityService {


//获取资源,前后端分离,所以过滤页面权限,只保留按钮权限 //获取资源,前后端分离,所以过滤页面权限,只保留按钮权限
List<Permission> btnPerms = permissions.stream() List<Permission> btnPerms = permissions.stream()
// 过滤页面权限
.filter(permission -> Objects.equals(permission.getType(), Consts.BUTTON)) .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())) .filter(permission -> StrUtil.isNotBlank(permission.getMethod()))
.collect(Collectors.toList()); .collect(Collectors.toList());


for (Permission btnPerm : btnPerms) { for (Permission btnPerm : btnPerms) {
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getPermission(), btnPerm.getMethod());
AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());
if (antPathMatcher.matches(request)) { if (antPathMatcher.matches(request)) {
hasPermission = true; hasPermission = true;
break; break;


+ 44
- 0
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/RedisConfig.java View File

@@ -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;

/**
* <p>
* redis配置
* </p>
*
* @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<String, String>,也就是只能存入字符串,因此支持序列化
*/
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

+ 3
- 11
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityConfig.java View File

@@ -32,9 +32,6 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter { public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

@Autowired @Autowired
private AccessDeniedHandler accessDeniedHandler; private AccessDeniedHandler accessDeniedHandler;


@@ -70,6 +67,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.csrf() .csrf()
.disable() .disable()


// 登录行为由自己实现,参考 AuthController#login
.formLogin() .formLogin()
.disable() .disable()
.httpBasic() .httpBasic()
@@ -86,17 +84,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.anyRequest() .anyRequest()
.access("@rbacAuthorityService.hasPermission(request,authentication)") .access("@rbacAuthorityService.hasPermission(request,authentication)")


// 登出处理
// 登出行为由自己实现,参考 AuthController#logout
.and() .and()
.logout()
// 登出请求默认为POST请求,改为GET请求
.logoutRequestMatcher(new AntPathRequestMatcher("/**/logout", "GET"))
// 登出成功处理器
.logoutSuccessHandler(logoutSuccessHandler)
.permitAll()
.logout().disable()


// Session 管理 // Session 管理
.and()
.sessionManagement() .sessionManagement()
// 因为使用了JWT,所以这里不管理Session // 因为使用了JWT,所以这里不管理Session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) .sessionCreationPolicy(SessionCreationPolicy.STATELESS)


+ 1
- 11
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/config/SecurityHandlerConfig.java View File

@@ -5,7 +5,6 @@ import com.xkcoding.rbac.security.util.ResponseUtil;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;


/** /**
* <p> * <p>
@@ -23,18 +22,9 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
@Configuration @Configuration
public class SecurityHandlerConfig { public class SecurityHandlerConfig {


/**
* 退出成功处理器
*
* @return 退出成功处理器
*/
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return (request, response, authentication) -> ResponseUtil.renderJson(response, Status.LOGOUT, null);
}

@Bean @Bean
public AccessDeniedHandler accessDeniedHandler() { public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, Status.ACCESS_DENIED, null); return (request, response, accessDeniedException) -> ResponseUtil.renderJson(response, Status.ACCESS_DENIED, null);
} }

} }

+ 16
- 1
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java View File

@@ -1,11 +1,14 @@
package com.xkcoding.rbac.security.controller; package com.xkcoding.rbac.security.controller;


import com.xkcoding.rbac.security.common.ApiResponse; 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.payload.LoginRequest;
import com.xkcoding.rbac.security.util.JwtUtil; import com.xkcoding.rbac.security.util.JwtUtil;
import com.xkcoding.rbac.security.vo.JwtResponse; import com.xkcoding.rbac.security.vo.JwtResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; 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.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; 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.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;


import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid; import javax.validation.Valid;


/** /**
@@ -51,7 +55,18 @@ public class AuthController {
SecurityContextHolder.getContext() SecurityContextHolder.getContext()
.setAuthentication(authentication); .setAuthentication(authentication);


String jwt = jwtUtil.createJWT(authentication);
String jwt = jwtUtil.createJWT(authentication,loginRequest.getRememberMe());
return ApiResponse.ofSuccess(new JwtResponse(jwt)); 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);
}
} }

+ 4
- 2
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/SecurityException.java View File

@@ -2,7 +2,8 @@ package com.xkcoding.rbac.security.exception;


import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.BaseException;
import com.xkcoding.rbac.security.common.Status; import com.xkcoding.rbac.security.common.Status;
import lombok.Getter;
import lombok.Data;
import lombok.EqualsAndHashCode;


/** /**
* <p> * <p>
@@ -17,7 +18,8 @@ import lombok.Getter;
* @version: V1.0 * @version: V1.0
* @modified: yangkai.shen * @modified: yangkai.shen
*/ */
@Getter
@EqualsAndHashCode(callSuper = true)
@Data
public class SecurityException extends BaseException { public class SecurityException extends BaseException {
public SecurityException(Status status) { public SecurityException(Status status) {
super(status); super(status);


+ 5
- 0
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/exception/handler/GlobalExceptionHandler.java View File

@@ -1,6 +1,7 @@
package com.xkcoding.rbac.security.exception.handler; package com.xkcoding.rbac.security.exception.handler;


import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.ApiResponse;
import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.BaseException;
import com.xkcoding.rbac.security.common.Status; 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.http.converter.HttpMessageNotReadableException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.DisabledException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -40,6 +42,9 @@ public class GlobalExceptionHandler {
if (e instanceof NoHandlerFoundException) { if (e instanceof NoHandlerFoundException) {
log.error("【全局异常拦截】NoHandlerFoundException: 请求方法 {}, 请求路径 {}", ((NoHandlerFoundException) e).getRequestURL(), ((NoHandlerFoundException) e).getHttpMethod()); log.error("【全局异常拦截】NoHandlerFoundException: 请求方法 {}, 请求路径 {}", ((NoHandlerFoundException) e).getRequestURL(), ((NoHandlerFoundException) e).getHttpMethod());
return ApiResponse.ofStatus(Status.REQUEST_NOT_FOUND); 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) { } else if (e instanceof MethodArgumentNotValidException) {
log.error("【全局异常拦截】MethodArgumentNotValidException", e); log.error("【全局异常拦截】MethodArgumentNotValidException", e);
return ApiResponse.of(Status.BAD_REQUEST.getCode(), ((MethodArgumentNotValidException) e).getBindingResult() return ApiResponse.of(Status.BAD_REQUEST.getCode(), ((MethodArgumentNotValidException) e).getBindingResult()


+ 7
- 2
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/payload/LoginRequest.java View File

@@ -23,13 +23,18 @@ public class LoginRequest {
/** /**
* 用户名或邮箱或手机号 * 用户名或邮箱或手机号
*/ */
@NotBlank
@NotBlank(message = "用户名不能为空")
private String usernameOrEmailOrPhone; private String usernameOrEmailOrPhone;


/** /**
* 密码 * 密码
*/ */
@NotBlank
@NotBlank(message = "密码不能为空")
private String password; private String password;


/**
* 记住我
*/
private Boolean rememberMe = false;

} }

+ 69
- 12
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/JwtUtil.java View File

@@ -1,6 +1,8 @@
package com.xkcoding.rbac.security.util; package com.xkcoding.rbac.security.util;


import cn.hutool.core.date.DateUtil; 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.common.Status;
import com.xkcoding.rbac.security.config.JwtConfig; import com.xkcoding.rbac.security.config.JwtConfig;
import com.xkcoding.rbac.security.exception.SecurityException; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;


import javax.servlet.http.HttpServletRequest;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


/** /**
* <p> * <p>
@@ -34,23 +40,23 @@ import java.util.List;
@Configuration @Configuration
@Slf4j @Slf4j
public class JwtUtil { public class JwtUtil {
private final JwtConfig jwtConfig;
@Autowired
private JwtConfig jwtConfig;


@Autowired @Autowired
public JwtUtil(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
}
private StringRedisTemplate stringRedisTemplate;


/** /**
* 创建JWT * 创建JWT
* *
* @param rememberMe 记住我
* @param id 用户id * @param id 用户id
* @param subject 用户名 * @param subject 用户名
* @param roles 用户角色 * @param roles 用户角色
* @param authorities 用户权限 * @param authorities 用户权限
* @return JWT * @return JWT
*/ */
public String createJWT(Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
public String createJWT(Boolean rememberMe, Long id, String subject, List<String> roles, Collection<? extends GrantedAuthority> authorities) {
Date now = new Date(); Date now = new Date();
JwtBuilder builder = Jwts.builder() JwtBuilder builder = Jwts.builder()
.setId(id.toString()) .setId(id.toString())
@@ -59,22 +65,30 @@ public class JwtUtil {
.signWith(SignatureAlgorithm.HS256, jwtConfig.getKey()) .signWith(SignatureAlgorithm.HS256, jwtConfig.getKey())
.claim("roles", roles) .claim("roles", roles)
.claim("authorities", authorities); .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 * 创建JWT
* *
* @param authentication 用户认证信息 * @param authentication 用户认证信息
* @param rememberMe 记住我
* @return JWT * @return JWT
*/ */
public String createJWT(Authentication authentication) {
public String createJWT(Authentication authentication, Boolean rememberMe) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); 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) { public Claims parseJWT(String jwt) {
try { try {
return Jwts.parser()
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getKey()) .setSigningKey(jwtConfig.getKey())
.parseClaimsJws(jwt) .parseClaimsJws(jwt)
.getBody(); .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) { } catch (ExpiredJwtException e) {
log.error("Token 已过期"); log.error("Token 已过期");
throw new SecurityException(Status.TOKEN_EXPIRED); 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 获取用户名 * 根据 jwt 获取用户名
* *
@@ -118,4 +161,18 @@ public class JwtUtil {
return claims.getSubject(); 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;
}

} }

+ 7
- 2
spring-boot-demo-rbac-security/src/main/java/com/xkcoding/rbac/security/util/ResponseUtil.java View File

@@ -1,5 +1,6 @@
package com.xkcoding.rbac.security.util; package com.xkcoding.rbac.security.util;


import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.xkcoding.rbac.security.common.ApiResponse; import com.xkcoding.rbac.security.common.ApiResponse;
import com.xkcoding.rbac.security.common.BaseException; import com.xkcoding.rbac.security.common.BaseException;
@@ -39,8 +40,10 @@ public class ResponseUtil {
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
response.setStatus(200); response.setStatus(200);


// FIXME: hutool 的 BUG:JSONUtil.toJsonStr()
// 将JSON转为String的时候,忽略null值的时候转成的String存在错误
response.getWriter() response.getWriter()
.write(JSONUtil.toJsonStr(ApiResponse.ofStatus(status, data)));
.write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofStatus(status, data), true)));
} catch (IOException e) { } catch (IOException e) {
log.error("Response写出JSON异常,", e); log.error("Response写出JSON异常,", e);
} }
@@ -59,8 +62,10 @@ public class ResponseUtil {
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
response.setStatus(200); response.setStatus(200);


// FIXME: hutool 的 BUG:JSONUtil.toJsonStr()
// 将JSON转为String的时候,忽略null值的时候转成的String存在错误
response.getWriter() response.getWriter()
.write(JSONUtil.toJsonStr(ApiResponse.ofException(exception)));
.write(JSONUtil.toJsonStr(new JSONObject(ApiResponse.ofException(exception), true)));
} catch (IOException e) { } catch (IOException e) {
log.error("Response写出JSON异常,", e); log.error("Response写出JSON异常,", e);
} }


+ 22
- 0
spring-boot-demo-rbac-security/src/main/resources/application.yml View File

@@ -18,7 +18,29 @@ spring:
properties: properties:
hibernate: hibernate:
dialect: org.hibernate.dialect.MySQL57InnoDBDialect 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: jwt:
config: config:
key: xkcoding key: xkcoding
ttl: 600000 ttl: 600000
remember: 604800000

Loading…
Cancel
Save