fix(JwtUtils): JWT载体有中文解析出来是问号乱码

Closes I3X8V6
This commit is contained in:
haoxr 2021-07-08 00:34:15 +08:00
parent 2af3e2f493
commit 75ad8cffbd
16 changed files with 99 additions and 181 deletions

Binary file not shown.

View File

@ -11,26 +11,3 @@ spring:
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
sentinel:
enabled: true
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 限流规则flow为key随便定义
flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: flow
# 降级规则
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: degrade

View File

@ -61,7 +61,7 @@ public class OAuthController {
* 方式一client_idclient_secret放在请求路径中(当前版本已废弃)
* 方式二放在请求头Request Headers中的Authorization字段且经过加密例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
*/
String clientId = JwtUtils.getAuthClientId();
String clientId = JwtUtils.getOAuthClientId();
OAuthClientEnum client = OAuthClientEnum.getByClientId(clientId);
switch (client) {
case TEST: // knife4j接口测试文档使用 client_id/client_secret : client/123456

View File

@ -124,11 +124,9 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
/**
* 密码编码器
* <p>
*
* 委托方式根据密码的前缀选择对应的encoder例如{bcypt}前缀->标识BCYPT算法加密{noop}->标识不使用任何加密即明文的方式
* 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {

View File

@ -34,7 +34,7 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String clientId = JwtUtils.getAuthClientId();
String clientId = JwtUtils.getOAuthClientId();
OAuthClientEnum client = OAuthClientEnum.getByClientId(clientId);
Result result;

View File

@ -9,38 +9,8 @@ spring:
discovery:
server-addr: http://localhost:8848
config:
# docker启动nacos-server需要配置
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
group: DEFAULT_GROUP
sentinel:
enabled: true
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 降级规则
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: degrade
# 开启feign对sentinel的支持
feign:
sentinel:
enabled: true
# jwt 配置
jwt:
config:
enabled: true
key-location: jwt.jks
key-alias: jwt
key-pass: 123456
iss: youlai.tech
sub: all
access-exp-days: 30

View File

@ -2,7 +2,6 @@ package com.youlai.common.constant;
public interface AuthConstants {
/**
* 认证请求头key
*/

View File

@ -10,26 +10,40 @@ import org.apache.logging.log4j.util.Strings;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import sun.misc.BASE64Decoder;
import javax.servlet.http.HttpServletRequest;
import java.net.URLDecoder;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT工具类
*
* @author xianrui
*/
@Slf4j
public class JwtUtils {
@SneakyThrows
public static JSONObject getJwtPayload() {
String jwtPayload = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader(AuthConstants.JWT_PAYLOAD_KEY);
JSONObject jsonObject = JSONUtil.parseObj(jwtPayload);
String payload = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader(AuthConstants.JWT_PAYLOAD_KEY);
JSONObject jsonObject = JSONUtil.parseObj(URLDecoder.decode(payload,"UTF-8"));
return jsonObject;
}
/**
* 解析JWT获取用户ID
*
* @return
*/
public static Long getUserId() {
Long id = getJwtPayload().getLong(AuthConstants.USER_ID_KEY);
return id;
}
/**
* 解析JWT获取获取用户名
*
* @return
*/
public static String getUsername() {
String username = getJwtPayload().getStr(AuthConstants.USER_NAME_KEY);
return username;
@ -37,7 +51,7 @@ public class JwtUtils {
/**
* 获取登录认证的客户端ID
* <p>
*
* 兼容两种方式获取Oauth2客户端信息client_idclient_secret
* 方式一client_idclient_secret放在请求路径中
* 方式二放在请求头Request Headers中的Authorization字段且经过加密例如 Basic Y2xpZW50OnNlY3JldA== 明文等于 client:secret
@ -45,7 +59,7 @@ public class JwtUtils {
* @return
*/
@SneakyThrows
public static String getAuthClientId() {
public static String getOAuthClientId() {
String clientId;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
@ -66,15 +80,17 @@ public class JwtUtils {
return clientId;
}
/**
* JWT获取用户角色列表
*
* @return 角色列表
*/
public static List<String> getRoles() {
List<String> roles = null;
JSONObject payload = getJwtPayload();
if (payload != null && payload.size() > 0) {
List<String> list = payload.get(AuthConstants.JWT_AUTHORITIES_KEY, List.class);
List<String> roles = list.stream().collect(Collectors.toList());
return roles;
if (payload != null && payload.containsKey(AuthConstants.JWT_AUTHORITIES_KEY)) {
roles = payload.get(AuthConstants.JWT_AUTHORITIES_KEY, List.class);
}
return null;
return roles;
}
}

View File

@ -1,6 +1,5 @@
package com.youlai.gateway.component;
import cn.hutool.core.util.StrUtil;
import com.youlai.common.constant.GlobalConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
@ -11,13 +10,13 @@ import java.nio.charset.StandardCharsets;
public class RedisChannelListener implements MessageListener {
@Autowired
private AdminRoleLocalCache adminRoleLocalCache;
private UrlPermRolesLocalCache urlPermRolesLocalCache;
@Override
public void onMessage(Message message, byte[] bytes) {
String msg = new String(message.getBody(), StandardCharsets.UTF_8);
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
adminRoleLocalCache.remove(GlobalConstants.URL_PERM_ROLES_KEY);
urlPermRolesLocalCache.remove(GlobalConstants.URL_PERM_ROLES_KEY);
}
}

View File

@ -6,7 +6,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
@ -17,7 +16,7 @@ import java.util.concurrent.TimeUnit;
*/
@Slf4j
@Component
public class AdminRoleLocalCache<T> {
public class UrlPermRolesLocalCache<T> {
private Cache<String,T> localCache = null;
@PostConstruct

View File

@ -3,13 +3,11 @@ package com.youlai.gateway.security;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.youlai.common.constant.AuthConstants;
import com.youlai.common.constant.GlobalConstants;
import com.youlai.gateway.component.AdminRoleLocalCache;
import com.youlai.gateway.component.UrlPermRolesLocalCache;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
@ -25,27 +23,29 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 网关自定义鉴权管理器
*
* @author haoxr
* @date 2020-05-01
* @author <a href="mailto:xianrui0365@163.com">xianrui</a>
*/
@Component
@AllArgsConstructor
@RequiredArgsConstructor
@Slf4j
public class ResourceServerManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private RedisTemplate redisTemplate;
private AdminRoleLocalCache adminRoleLocalCache;
private final RedisTemplate redisTemplate;
// 本地缓存
private final UrlPermRolesLocalCache urlPermRolesLocalCache;
// 是否演示环境
@Value("${demo}")
private Boolean isDemoEnv;
@Value("${local-cache.enabled}")
private Boolean localCacheEnabled;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
@ -53,66 +53,68 @@ public class ResourceServerManager implements ReactiveAuthorizationManager<Autho
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
PathMatcher pathMatcher = new AntPathMatcher(); // 声明定义Ant路径匹配模式请求路径和缓存中权限规则的URL权限标识匹配
PathMatcher pathMatcher = new AntPathMatcher(); // Ant匹配器
String method = request.getMethodValue();
String path = request.getURI().getPath();
String restfulPath = method + ":" + path; // Restful接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14961707.html
// 移动端请求需认证但无需鉴权判断
String token = request.getHeaders().getFirst(AuthConstants.AUTHORIZATION_KEY);
// 移动端请求无需鉴权只需认证即JWT的验签和是否过期判断
if (pathMatcher.match(GlobalConstants.APP_API_PATTERN, path)) {
// 如果token以"bearer "为前缀到这一步说明是经过NimbusReactiveJwtDecoder#decode和JwtTimestampValidator#validate等解析和验证通过的即已认证
if (StrUtil.isNotBlank(token) && token.startsWith(AuthConstants.AUTHORIZATION_PREFIX)) {
// 如果token以"bearer "为前缀到这里说明JWT有效即已认证
if (StrUtil.isNotBlank(token)
&& token.startsWith(AuthConstants.AUTHORIZATION_PREFIX)) {
return Mono.just(new AuthorizationDecision(true));
} else {
return Mono.just(new AuthorizationDecision(false));
}
}
// Restful接口权限设计 @link https://www.cnblogs.com/haoxianrui/p/14396990.html
String restfulPath = request.getMethodValue() + ":" + path;
log.info("请求方法:RESTFul请求路径{}", restfulPath);
Map<String, Object> permRolesRules = (Map<String, Object>) adminRoleLocalCache.getCache(GlobalConstants.URL_PERM_ROLES_KEY);
if (isDemoEnv){
// 缓存取URL权限标识->角色集合权限规则
if(null==permRolesRules){
permRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
adminRoleLocalCache.setLocalCache(GlobalConstants.URL_PERM_ROLES_KEY,permRolesRules);
// 缓存取 URL权限-角色集合 规则数据
// urlPermRolesRules = [{'key':'GET:/api/v1/users/*','value':['ADMIN','TEST']},...]
Map<String, Object> urlPermRolesRules;
if (localCacheEnabled) {
urlPermRolesRules = (Map<String, Object>) urlPermRolesLocalCache.getCache(GlobalConstants.URL_PERM_ROLES_KEY);
if (null == urlPermRolesRules) {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
urlPermRolesLocalCache.setLocalCache(GlobalConstants.URL_PERM_ROLES_KEY, urlPermRolesRules);
}
} else {
urlPermRolesRules = redisTemplate.opsForHash().entries(GlobalConstants.URL_PERM_ROLES_KEY);
}
// 根据 请求路径 权限规则中的URL权限标识进行Ant匹配得出拥有权限的角色集合
Set<String> hasPermissionRoles = CollectionUtil.newHashSet(); // 声明定义有权限的角色集合
boolean needToCheck = false; // 声明定义是否需要被拦截检查的请求如果缓存中权限规则中没有任何URL权限标识和此次请求的URL匹配默认不需要被鉴权
for (Map.Entry<String, Object> permRoles : permRolesRules.entrySet()) {
String perm = permRoles.getKey(); // 缓存权限规则的键URL权限标识
// 根据请求路径判断有访问权限的角色列表
List<String> authorizedRoles = new ArrayList<>(); // 拥有访问权限的角色
boolean requireCheck = false; // 是否需要鉴权默认没有设置权限规则不用鉴权
for (Map.Entry<String, Object> permRoles : urlPermRolesRules.entrySet()) {
String perm = permRoles.getKey();
if (pathMatcher.match(perm, restfulPath)) {
List<String> roles = Convert.toList(String.class, permRoles.getValue()); // 缓存权限规则的值有请求路径访问权限的角色集合
hasPermissionRoles.addAll(Convert.toList(String.class, roles));
if (needToCheck == false) {
needToCheck = true;
List<String> roles = Convert.toList(String.class, permRoles.getValue());
authorizedRoles.addAll(Convert.toList(String.class, roles));
if (requireCheck == false) {
requireCheck = true;
}
}
}
log.info("拥有接口访问权限的角色:{}", hasPermissionRoles.toString());
// 没有设置权限规则放行如果默认想拦截所有的请求请移除needToCheck变量逻辑即可根据需求定制
if (needToCheck == false) {
if (requireCheck == false) {
return Mono.just(new AuthorizationDecision(true));
}
// 判断用户JWT中携带的角色是否有能通过权限拦截的角色
// 判断JWT中携带的用户角色是否有权限访问
Mono<AuthorizationDecision> authorizationDecisionMono = mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authority -> {
log.info("用户权限 : {}", authority); // ROLE_ROOT
String role = authority.substring(AuthConstants.AUTHORITY_PREFIX.length()); // 角色编码ROOT
if (GlobalConstants.ROOT_ROLE_CODE.equals(role)) { // 如果是超级管理员则放行
return true;
String roleCode = authority.substring(AuthConstants.AUTHORITY_PREFIX.length()); // 用户的角色
if (GlobalConstants.ROOT_ROLE_CODE.equals(roleCode)) {
return true; // 如果是超级管理员则放行
}
boolean hasPermission = CollectionUtil.isNotEmpty(hasPermissionRoles) && hasPermissionRoles.contains(role); // 用户角色中只要有一个满足则通过权限校验
return hasPermission;
boolean hasAuthorized = CollectionUtil.isNotEmpty(authorizedRoles) && authorizedRoles.contains(roleCode);
return hasAuthorized;
})
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));

View File

@ -7,10 +7,10 @@ import com.nimbusds.jose.JWSObject;
import com.youlai.common.constant.AuthConstants;
import com.youlai.common.result.ResultCode;
import com.youlai.gateway.util.ResponseUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
@ -23,22 +23,23 @@ import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLEncoder;
/**
* 安全拦截全局过滤器
*
* @author haoxr
* @date 2020-06-12
* @author <a href="mailto:xianrui0365@163.com">xianrui</a>
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class SecurityGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate redisTemplate;
private final RedisTemplate redisTemplate;
// 是否演示环境
@Value("${demo}")
private Boolean isDemoEnv;
@Value("${spring.profiles.active}")
private String env;
@SneakyThrows
@Override
@ -47,8 +48,8 @@ public class SecurityGlobalFilter implements GlobalFilter, Ordered {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 演示环境禁止删除和修改
if (isDemoEnv
// 线上演示环境禁止修改和删除
if (env.equals("prod")
&& (HttpMethod.DELETE.toString().equals(request.getMethodValue()) // 删除方法
|| HttpMethod.PUT.toString().equals(request.getMethodValue())) // 修改方法
) {
@ -74,7 +75,7 @@ public class SecurityGlobalFilter implements GlobalFilter, Ordered {
// 存在token且不是黑名单request写入JWT的载体信息
request = exchange.getRequest().mutate()
.header(AuthConstants.JWT_PAYLOAD_KEY, payload)
.header(AuthConstants.JWT_PAYLOAD_KEY, URLEncoder.encode(payload,"UTF-8"))
.build();
exchange = exchange.mutate().request(request).build();
return chain.filter(exchange);

View File

@ -11,7 +11,7 @@ import springfox.documentation.swagger.web.*;
import java.util.Optional;
/**
* @Author haoxr
* @author xianrui
* @Date 2021-02-25 16:34
* @Version 1.0.0
* @ https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/handler/SwaggerHandler.java

View File

@ -8,9 +8,9 @@ import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* @auth haoxr
* @link https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/config/SwaggerHeaderFilter.java
* @auth xianrui
* @date 2021-02-25 16:29
* @link https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/blob/master/knife4j-spring-cloud-gateway/service-doc/src/main/java/com/xiaominfo/swagger/service/doc/config/SwaggerHeaderFilter.java
*/
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {

View File

@ -13,29 +13,7 @@ spring:
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
sentinel:
enabled: false # 网关流控开关
eager: true # 取消控制台懒加载项目启动即连接Sentinel
transport:
client-ip: localhost
dashboard: localhost:8080
datasource:
# 网关限流规则gw-flow为key随便定义
gw-flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-flow
# 网关API自定义分组
gw-api-group:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-api-group-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-api-group

View File

@ -15,24 +15,3 @@ spring:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
namespace: prod_namespace_id
sentinel:
eager: true
transport:
dashboard: e.youlai.tech:8858
datasource:
# 网关限流
gw-flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-flow-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-flow
# 网关API自定义分组
gw-api-group:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-gw-api-group-rules
groupId: SENTINEL_GROUP
data-type: json
rule-type: gw-api-group