1. 前言
微服务下架构下,借助JWT和Spring Gateway过滤器进行统一用户认证,并通过拦截器解析用户携带Token获取当前用户信息存储到ThreadLocal方便业务模块使用
2. 服务架构
主要包含四个模块,使用Nacos作为注册和配置中心
服务 | 类型 | 端口 | 描述 |
---|---|---|---|
jtool-gateway | gateway | 8080 | 网关,用作接口转发和认证 |
jtool-security | authentication | 8083 | 用户认证 |
jtool-template | service | 8081 | 业务模块1 |
jtool-demo | service | 8082 | 业务模块2 |
3. 网关过滤器
处于白名单中的接口不需要认证,直接转发(比如/login接口,网关会转发到security模块),非白名单接口需要解析request携带的token,若token合法,将解析token获取的用户名放到request中转发到指定接口即可
@Component
@Import(IgnoreUrlsConfig.class)
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (ignoreUrlsConfig.getUrls().stream().anyMatch(e ->
request.getPath().pathWithinApplication().value().equals(e)
)) {
return chain.filter(exchange);
}
//校验token
List<String> token = request.getHeaders().get(SecurityConst.TOKEN);
AssertUtil.isNotEmpty(token, "非法访问");
try {
String userName = JwtUtil.getUserName(token.get(0));
request.mutate().header(SecurityConst.USERNAME,userName);
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
网关配置文件
server:
port: 8080
spring:
main:
web-application-type: reactive
application:
name: jtool-gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
gateway:
default-filters:
- PrintAny
routes:
- id: jtool-template-service
uri: lb://jtool-template-service
predicates:
- Path=/test/**,/code/**
- id: jtool-demo-service
uri: lb://jtool-demo-service
predicates:
- Path=/demo/**
- id: jtool-security
uri: lb://jtool-security
predicates:
- Path=/login
security:
whitelist:
urls:
- /login
4. 用户认证
认证模块仅仅有个/login接口,比较用户名和密码是否一致即可
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result<Map<String, String>> login(@RequestBody Map<String, String> userInfo) {
String username = userInfo.get(SecurityConst.USERNAME);
userService.authenticate(username, userInfo.get(SecurityConst.PASSWORD));
String jwt = JwtUtil.createJWT(username);
return Result.success(Map.of(SecurityConst.TOKEN, jwt));
}
}
配置文件
spring:
profiles:
active: dev
application:
name: jtool-security
cloud:
nacos:
username: nacos
password: nacos
discovery:
server-addr: 127.0.0.1:8848
config:
#避免没有加载bootstrap导致的错误
import-check:
enabled: false
server-addr: 127.0.0.1:8848
server:
port: 8083
#mybatis-plus
mybatis-plus:
mapperLocations: classpath*:mapper/*.xml
5. Token拦截器
业务模块通过拦截器获取网关过滤器设置的用户名,并存储到ThreadLocal中,拦截器在接口结束后会将用户信息从ThreadLocal中移除,避免Memory Leak
/**
* 将用户信息保存到ThreadLocal上下文中
*/
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String header = request.getHeader(SecurityConst.USERNAME);
if (StringUtils.hasText(header)) {
UserContextHolder.set(SecurityConst.USERNAME, header);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContextHolder.clear();
}
}
ThreadLocal代码
package com.cll.common.security.context;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class UserContextHolder {
private static ThreadLocal<Map<String, Object>> userThreadLocal = new ThreadLocal<>();
private static void initialize() {
if (userThreadLocal.get() == null) {
synchronized (userThreadLocal) {
if (userThreadLocal.get() == null) {
userThreadLocal.set(new ConcurrentHashMap<>());
}
}
}
}
public static Object get(String key) {
initialize();
return userThreadLocal.get().get(key);
}
public static void set(String key, String value) {
initialize();
userThreadLocal.get().put(key, value);
}
public static void clear() {
Map<String, Object> stringObjectMap = userThreadLocal.get();
if (stringObjectMap != null) {
stringObjectMap.clear();
}
}
}
配置文件
server:
port: 8081
spring:
profiles:
active: dev
application:
name: jtool-template-service
cloud:
nacos:
username: nacos
password: nacos
discovery:
server-addr: 127.0.0.1:8848
config:
import-check:
enabled: false
server-addr: 127.0.0.1:8848
security:
whitelist:
- /test/user/login
#mybatis-plus
mybatis:
mapper-locations: classpath*:mapper/*.xml
6. 源代码
https://gitee.com/doc4j/jtool.git
参考ruoyi-cloud