# 统一返回响应体封装
@Data | |
public class JsonResult<T> { | |
/** | |
* 错误码 | |
*/ | |
private Integer code; | |
/** | |
* 提示信息 | |
*/ | |
private String msg; | |
/** | |
* 返回的具体内容 | |
*/ | |
private T data; | |
public JsonResult(Integer code, String msg, T data) { | |
this.code = code; | |
this.msg = msg; | |
this.data = data; | |
} | |
public JsonResult() { | |
} | |
public JsonResult<T> fail() { | |
return new JsonResult<>(1000, "操作失败", null); | |
} | |
public JsonResult<T> fail(String msg) { | |
return new JsonResult<>(1001, msg, null); | |
} | |
public JsonResult<T> ok() { | |
return new JsonResult<>(2000, "操作成功", null); | |
} | |
public JsonResult<T> ok(T data) { | |
return new JsonResult<>(2001, "操作成功", data); | |
} | |
public JsonResult<T> jud(boolean condition) { | |
return condition ? ok() : fail(); | |
} | |
public static void returnMsg(HttpServletResponse response, String msg) throws IOException { | |
response.setCharacterEncoding("utf-8"); | |
PrintWriter out; | |
String jsonString; | |
out = response.getWriter(); | |
jsonString = new ObjectMapper().writeValueAsString(new JsonResult<>().fail(msg)); | |
out.println(jsonString); | |
out.flush(); | |
} | |
} |
# JWT 配置
<!--jwt 依赖 --> | |
<dependency> | |
<groupId>io.jsonwebtoken</groupId> | |
<artifactId>jjwt</artifactId> | |
<version>0.9.0</version> | |
</dependency> |
@Slf4j(topic = "jwtConfig") | |
@Data | |
@Component | |
@ConfigurationProperties(prefix = "jwt") | |
public class JwtConfig { | |
/** | |
* 密钥 | |
*/ | |
private String secret = "XXXXX"; | |
/** | |
* 过期时间(单位:秒)-- 1 小时 | |
**/ | |
private long expireTime = 60 * 60 * 1000L; | |
public String sign(User user) { | |
Map<String, Object> claim = new HashMap<>(); | |
claim.put("username", user.getUsername()); | |
claim.put("userId", user.getUserId()); | |
Date date = new Date(); | |
return Jwts.builder() | |
.setClaims(claim) | |
.setIssuedAt(date) | |
.setExpiration(new Date(date.getTime() + expireTime)) | |
.signWith(SignatureAlgorithm.HS512, secret) | |
.compact(); | |
} | |
/** | |
* 获取 token 中注册信息 | |
*/ | |
public Claims getTokenClaim(String token) { | |
try { | |
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); | |
} catch (Exception e) { | |
return null; | |
/* catch (ExpiredJwtException e){ | |
return e.getClaims (); // 防止 jwt 过期解析报错 | |
} | |
*/ | |
} | |
} | |
/** | |
* 验证 token 是否过期失效 | |
* | |
* @param token 令牌 | |
*/ | |
public boolean isTokenExpired(String token) { | |
Date expiration = getExpirationDateFromToken(token); | |
if (expiration != null) { | |
return getExpirationDateFromToken(token).before(new Date()); | |
} else { | |
return true; | |
} | |
} | |
/** | |
* 获取 token 失效时间 | |
* | |
* @param token 令牌 | |
*/ | |
public Date getExpirationDateFromToken(String token) { | |
Claims claims = getTokenClaim(token); | |
return (claims == null) ? null : claims.getExpiration(); | |
} | |
/** | |
* 获取用户名从 token 中 | |
*/ | |
public User getUser(String token) { | |
Claims map = getTokenClaim(token); | |
String username = (String) map.get("username"); | |
Integer userId = (Integer) map.get("userId"); | |
return new User(userId, username); | |
} | |
/** | |
* 获取 jwt 发布时间 | |
*/ | |
public Date getIssuedAtDateFromToken(String token) { | |
return getTokenClaim(token).getIssuedAt(); | |
} | |
} |
还有一个版本
/** | |
* JWT 工具类 | |
*/ | |
public class JwtUtil { | |
/** | |
* 有效期为 60 * 60 * 1000 一个小时 | |
*/ | |
public static final Long JWT_TTL = 60 * 60 * 1000L; | |
/** | |
* 设置秘钥明文 | |
*/ | |
public static final String JWT_KEY = "XXXXXX"; | |
public static String getUUID() { | |
return UUID.randomUUID().toString().replaceAll("-", "").substring(0,8); | |
} | |
/** | |
* 生成 jtw | |
* | |
* @param subject token 中要存放的数据(json 格式) | |
* @return | |
*/ | |
public static String createJWT(String subject) { | |
// 设置过期时间 | |
JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); | |
return builder.compact(); | |
} | |
/** | |
* 生成 jtw | |
* | |
* @param subject token 中要存放的数据(json 格式) | |
* @param ttlMillis token 超时时间 | |
* @return | |
*/ | |
public static String createJWT(String subject, Long ttlMillis) { | |
// 设置过期时间 | |
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID()); | |
return builder.compact(); | |
} | |
/** | |
* 创建 token | |
* | |
* @param id | |
* @param subject | |
* @param ttlMillis | |
* @return | |
*/ | |
public static String createJWT(String id, String subject, Long ttlMillis) { | |
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 | |
return builder.compact(); | |
} | |
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { | |
SecretKey secretKey = generalKey(); | |
long nowMillis = System.currentTimeMillis(); | |
Date now = new Date(nowMillis); | |
if (ttlMillis == null) { | |
ttlMillis = JwtUtil.JWT_TTL; | |
} | |
long expMillis = nowMillis + ttlMillis; | |
Date expDate = new Date(expMillis); | |
return Jwts.builder() | |
//jti:jwt 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。 | |
.setId(uuid) | |
//sub: jwt 所面向的用户 | |
.setSubject(subject) | |
//iss: 签发者 | |
.setIssuer("windlinxy.top") | |
//iat: jwt 的签发时间 | |
.setIssuedAt(now) | |
//exp: jwt 的过期时间 | |
.setExpiration(expDate) | |
// 使用 HS256 对称加密算法签名,第二个参数为秘钥 | |
.signWith(SignatureAlgorithm.HS256, secretKey); | |
} | |
public static void main(String[] args) throws Exception { | |
long now = System.currentTimeMillis(); | |
System.out.println(new Date()); | |
String token = createJWT("nihao"); | |
Claims claims = parseJWT("token"); | |
System.out.println(System.currentTimeMillis() - now); | |
System.out.println(new Date()); | |
System.out.println(token); | |
System.out.println(claims); | |
} | |
/** | |
* 生成加密后的秘钥 secretKey | |
* | |
* @return | |
*/ | |
public static SecretKey generalKey() { | |
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); | |
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); | |
return key; | |
} | |
/** | |
* 解析 | |
* | |
* @param jwt | |
* @return | |
* @throws Exception | |
*/ | |
public static Claims parseJWT(String jwt) throws Exception { | |
if(jwt == null) { | |
return null; | |
} | |
SecretKey secretKey = generalKey(); | |
return Jwts.parser() | |
.setSigningKey(secretKey) | |
.parseClaimsJws(jwt) | |
.getBody(); | |
} | |
} |
# CORS 跨域解决
@Configuration | |
public class WebAppConfig implements WebMvcConfigurer { | |
@Override | |
public void addCorsMappings(CorsRegistry registry) { | |
registry.addMapping("/**") | |
// 是否发送 Cookie | |
.allowCredentials(true) | |
// 放行哪些原始域 | |
.allowedOriginPatterns("*") | |
.allowedMethods("*") | |
.allowedHeaders("*") | |
.exposedHeaders("*"); | |
} | |
} |
# 拦截器(注解拦截)
@PassToken
/** | |
* @author Windlinxy | |
* @description: 权限判断跳过 | |
* @create 2023-01-30 19:11 | |
**/ | |
@Target({ElementType.METHOD, ElementType.TYPE}) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface PassToken { | |
boolean required() default true; | |
} |
拦截器
@Slf4j(topic = "AdminInterceptor") | |
public class AdminInterceptor implements HandlerInterceptor { | |
@Resource | |
private JwtConfig jwtConfig; | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
/* | |
* 放行预检请求 | |
*/ | |
String options = "OPTIONS"; | |
if (options.equalsIgnoreCase(request.getMethod())) { | |
return true; | |
} | |
if(judAdminCheck(handler, request)){ | |
return true; | |
}else { | |
JsonResult.returnMsg(response, "需要管理员权限"); | |
return false; | |
} | |
} | |
/** | |
* 检查是否有 AdminCheck 注解,有则验证是否是管理员 | |
* | |
* @param handler handler | |
* @return 是否有 PassToken 注释 | |
*/ | |
private boolean judAdminCheck(Object handler, HttpServletRequest request) { | |
AdminCheck adminCheck = ((HandlerMethod) handler).getMethodAnnotation(AdminCheck.class); | |
if (adminCheck == null){ | |
adminCheck = ((HandlerMethod) handler).getBean().getClass().getAnnotation(AdminCheck.class); | |
} | |
String username = jwtConfig.getUser(request.getHeader("Authorization")).getUsername(); | |
if ("admin".equals(username)) { | |
return adminCheck.required(); | |
} else { | |
return false; | |
} | |
} | |
} |
# RedisConfig
使用原生的 RedisTemplate
时存储数据时键值对会带上编码前缀,解决办法是使用 FastJson 实现一个 redis 序列化器
<!--redis 依赖 --> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-redis</artifactId> | |
<exclusions> | |
<exclusion> | |
<groupId>io.lettuce</groupId> | |
<artifactId>lettuce-core</artifactId> | |
</exclusion> | |
</exclusions> | |
</dependency> | |
<dependency> | |
<groupId>redis.clients</groupId> | |
<artifactId>jedis</artifactId> | |
</dependency> | |
<!--fastjson 依赖 --> | |
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>fastjson</artifactId> | |
<version>1.2.33</version> | |
</dependency> |
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { | |
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; | |
private final Class<T> clazz; | |
static { | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
} | |
public FastJsonRedisSerializer(Class<T> clazz) { | |
super(); | |
this.clazz = clazz; | |
} | |
@Override | |
public byte[] serialize(T t) throws SerializationException { | |
if (t == null) { | |
return new byte[0]; | |
} | |
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); | |
} | |
@Override | |
public T deserialize(byte[] bytes) throws SerializationException { | |
if (bytes == null || bytes.length <= 0) { | |
return null; | |
} | |
String str = new String(bytes, DEFAULT_CHARSET); | |
return JSON.parseObject(str, clazz); | |
} | |
protected JavaType getJavaType(Class<?> clazz) { | |
return TypeFactory.defaultInstance().constructType(clazz); | |
} | |
} |
配置 RedisTemplate
@Configuration | |
public class RedisConfig { | |
@Bean | |
@SuppressWarnings(value = { "unchecked", "rawtypes" }) | |
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) | |
{ | |
RedisTemplate<Object, Object> template = new RedisTemplate<>(); | |
template.setConnectionFactory(connectionFactory); | |
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); | |
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值 | |
template.setKeySerializer(new StringRedisSerializer()); | |
template.setValueSerializer(serializer); | |
// Hash 的 key 也采用 StringRedisSerializer 的序列化方式 | |
template.setHashKeySerializer(new StringRedisSerializer()); | |
template.setHashValueSerializer(serializer); | |
template.afterPropertiesSet(); | |
return template; | |
} | |
} |
# redis 工具类
参考三更草堂 SpringSecurity 课程
@Component | |
@SuppressWarnings(value = {"unchecked", "rawtypes", "unused", "all"}) | |
public class RedisCache { | |
@Resource | |
public RedisTemplate redisTemplate; | |
/** | |
* 缓存基本的对象,Integer、String、实体类等 | |
* | |
* @param key 缓存的键值 | |
* @param value 缓存的值 | |
*/ | |
public <T> void setCacheObject(final String key, final T value) { | |
redisTemplate.opsForValue().set(key, value); | |
} | |
/** | |
* 缓存基本的对象,Integer、String、实体类等 | |
* | |
* @param key 缓存的键值 | |
* @param value 缓存的值 | |
* @param timeout 时间 | |
* @param timeUnit 时间颗粒度 | |
*/ | |
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { | |
redisTemplate.opsForValue().set(key, value, timeout, timeUnit); | |
} | |
/** | |
* 设置有效时间 | |
* | |
* @param key Redis 键 | |
* @param timeout 超时时间 | |
* @return true = 设置成功;false = 设置失败 | |
*/ | |
public boolean expire(final String key, final long timeout) { | |
return expire(key, timeout, TimeUnit.SECONDS); | |
} | |
/** | |
* 设置有效时间 | |
* | |
* @param key Redis 键 | |
* @param timeout 超时时间 | |
* @param unit 时间单位 | |
* @return true = 设置成功;false = 设置失败 | |
*/ | |
public boolean expire(final String key, final long timeout, final TimeUnit unit) { | |
return redisTemplate.expire(key, timeout, unit); | |
} | |
/** | |
* 获得缓存的基本对象。 | |
* | |
* @param key 缓存键值 | |
* @return 缓存键值对应的数据 | |
*/ | |
public <T> T getCacheObject(final String key) { | |
ValueOperations<String, T> operation = redisTemplate.opsForValue(); | |
return operation.get(key); | |
} | |
/** | |
* 删除单个对象 | |
* | |
* @param key | |
*/ | |
public boolean deleteObject(final String key) { | |
return redisTemplate.delete(key); | |
} | |
/** | |
* 删除集合对象 | |
* | |
* @param collection 多个对象 | |
* @return | |
*/ | |
public long deleteObject(final Collection collection) { | |
return redisTemplate.delete(collection); | |
} | |
/** | |
* 缓存 List 数据 | |
* | |
* @param key 缓存的键值 | |
* @param dataList 待缓存的 List 数据 | |
* @return 缓存的对象 | |
*/ | |
public <T> long setCacheList(final String key, final List<T> dataList) { | |
Long count = redisTemplate.opsForList().rightPushAll(key, dataList); | |
return count == null ? 0 : count; | |
} | |
/** | |
* 获得缓存的 list 对象 | |
* | |
* @param key 缓存的键值 | |
* @return 缓存键值对应的数据 | |
*/ | |
public <T> List<T> getCacheList(final String key) { | |
return redisTemplate.opsForList().range(key, 0, -1); | |
} | |
/** | |
* 缓存 Set | |
* | |
* @param key 缓存键值 | |
* @param dataSet 缓存的数据 | |
* @return 缓存数据的对象 | |
*/ | |
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { | |
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); | |
Iterator<T> it = dataSet.iterator(); | |
while (it.hasNext()) { | |
setOperation.add(it.next()); | |
} | |
return setOperation; | |
} | |
/** | |
* 获得缓存的 set | |
* | |
* @param key | |
* @return | |
*/ | |
public <T> Set<T> getCacheSet(final String key) { | |
return redisTemplate.opsForSet().members(key); | |
} | |
/** | |
* 缓存 Map | |
* | |
* @param key | |
* @param dataMap | |
*/ | |
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { | |
if (dataMap != null) { | |
redisTemplate.opsForHash().putAll(key, dataMap); | |
} | |
} | |
/** | |
* 获得缓存的 Map | |
* | |
* @param key | |
* @return | |
*/ | |
public <T> Map<String, T> getCacheMap(final String key) { | |
return redisTemplate.opsForHash().entries(key); | |
} | |
/** | |
* 往 Hash 中存入数据 | |
* | |
* @param key Redis 键 | |
* @param hKey Hash 键 | |
* @param value 值 | |
*/ | |
public <T> void setCacheMapValue(final String key, final String hKey, final T value) { | |
redisTemplate.opsForHash().put(key, hKey, value); | |
} | |
/** | |
* 获取 Hash 中的数据 | |
* | |
* @param key Redis 键 | |
* @param hKey Hash 键 | |
* @return Hash 中的对象 | |
*/ | |
public <T> T getCacheMapValue(final String key, final String hKey) { | |
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); | |
return opsForHash.get(key, hKey); | |
} | |
/** | |
* 删除 Hash 中的数据 | |
* | |
* @param key | |
* @param hkey | |
*/ | |
public void delCacheMapValue(final String key, final String hkey) { | |
HashOperations hashOperations = redisTemplate.opsForHash(); | |
hashOperations.delete(key, hkey); | |
} | |
/** | |
* 获取多个 Hash 中的数据 | |
* | |
* @param key Redis 键 | |
* @param hKeys Hash 键集合 | |
* @return Hash 对象集合 | |
*/ | |
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { | |
return redisTemplate.opsForHash().multiGet(key, hKeys); | |
} | |
/** | |
* 获得缓存的基本对象列表 | |
* | |
* @param pattern 字符串前缀 | |
* @return 对象列表 | |
*/ | |
public Collection<String> keys(final String pattern) { | |
return redisTemplate.keys(pattern); | |
} | |
} |