授权中心的主要职责:
- 用户登录鉴权:
- 接收用户的登录请求,
- 通过用户中心的接口校验用户名密码
- 使用私钥生成JWT并返回
- 用户登录状态校验
- 判断用户是否登录,其实就是token的校验
- 用户登出
- 用户选择退出登录后,要让token失效
- 用户登录状态刷新
- 用户登录一段时间后,JWT可能过期,需要刷新有效期
创建jwt-token存入cookie
1.创建一个springcloud项目-下的授权模块
- 校验用户名密码必须到用户中心去做,因此用户中心必须对外提供的接口(feign),根据用户名和密码查询用户。
- 生成JWT的过程需要私钥,验证签名需要公钥,因此需要在授权中心启动时加载公钥和私钥
- 返回JWT给用户,需要在以后的请求中携带jwt,那么客户端该把这个JWT保存在哪里呢?
2.yml
授权的yml
ly:
jwt:
pubKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key.pub # 公钥地址
priKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key # 私钥地址
cookie:
expire: 30 #过期时间设置 单位分钟
cookieName: LY_TOKEN # cookie名称
cookieDomain: leyou.com # cookie的域
网关的yml
spring:
cloud:
gateway:
routes:
# 其他省略
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=2
3.jwt的配置类
package com.leyou.auth.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;
private String priKeyPath;
private PublicKey publicKey;
private PrivateKey privateKey;
private CookiePojo cookie = new CookiePojo();
@Data
public class CookiePojo{
private Integer expire;
private String cookieName;
private String cookieDomain;
}
/**
* 指定初始化方法
* @throws Exception
*/
@PostConstruct
public void initMethod() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
}
4.启动类加注解
5.对应的service
package com.leyou.auth.service;
import com.leyou.auth.config.JwtProperties;
import com.leyou.common.auth.pojo.UserInfo;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.exception.pojo.ExceptionEnum;
import com.leyou.common.exception.pojo.LyException;
import com.leyou.common.utils.CookieUtils;
import com.leyou.user.client.UserClient;
import com.leyou.user.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Service
public class AuthService {
@Autowired
private JwtProperties jwtProp;
@Autowired
private UserClient userClient;
/*认证业务代码*/
public void login(String username, String password, HttpServletRequest request, HttpServletResponse response) {
//校验用户名和密码是否正确
User user = userClient.findUserByNameAndPassword(username, password);
if(user==null){
throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD);
}
//封装jwt中载荷中的用户对象
UserInfo userInfo = new UserInfo(user.getId(), user.getUsername(), "admin");
//构建token并写入浏览器的cookie中
this.createTokenWriteToCookie(request, response, userInfo);
}
/*构建token并写入浏览器的cookie中*/
private void createTokenWriteToCookie(HttpServletRequest request, HttpServletResponse response, UserInfo userInfo) {
//生成token
String token = JwtUtils.generateTokenExpireInMinutes(userInfo, jwtProp.getPrivateKey(), jwtProp.getCookie().getExpire());
//把token写入浏览器的cookie中
CookieUtils.newCookieBuilder()
.request(request)
.response(response)
.name(jwtProp.getCookie().getCookieName())
.value(token)
.httpOnly(true)
.domain(jwtProp.getCookie().getCookieDomain())
.build();
}
}
6.Cookie工具类
package utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Cookie 工具类
*/
@Slf4j
public final class CookieUtils {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, null);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String charset) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (charset != null && charset.length() > 0) {
retValue = URLDecoder.decode(cookieList[i].getValue(), charset);
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
log.error("Cookie Decode Error.", e);
}
return retValue;
}
public static CookieBuilder newCookieBuilder() {
return new CookieBuilder();
}
public static void deleteCookie(String cookieName, String domain,HttpServletResponse response) {
Cookie cookie = new Cookie(cookieName, "");
cookie.setMaxAge(0);
cookie.setPath("/");
cookie.setDomain(domain);
response.addCookie(cookie);
}
public static class CookieBuilder {
private HttpServletRequest request;
private HttpServletResponse response;
private Integer maxAge;
private String charset;
private boolean httpOnly = false;
private String domain;
private String path = "/";
private String name;
private String value;
public CookieBuilder() {
}
public CookieBuilder request(HttpServletRequest request) {
this.request = request;
return this;
}
public CookieBuilder response(HttpServletResponse response) {
this.response = response;
return this;
}
public CookieBuilder maxAge(int maxAge) {
this.maxAge = maxAge;
return this;
}
public CookieBuilder charset(String charset) {
this.charset = charset;
return this;
}
public CookieBuilder domain(String domain) {
this.domain = domain;
return this;
}
public CookieBuilder path(String path) {
this.path = path;
return this;
}
public CookieBuilder value(String value) {
this.value = value;
return this;
}
public CookieBuilder name(String name) {
this.name = name;
return this;
}
public CookieBuilder httpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
public void build() {
try {
if (StringUtils.isBlank(charset)) {
charset = "utf-8";
}
if(StringUtils.isBlank(name)||StringUtils.isBlank(value)){
throw new RuntimeException("cookie名称和值不能为空!");
}
if (StringUtils.isNotBlank(charset)) {
value = URLEncoder.encode(value, charset);
}
Cookie cookie = new Cookie(name, value);
if (maxAge != null && maxAge >= 0)
cookie.setMaxAge(maxAge);
if(StringUtils.isNotBlank(domain)){
cookie.setDomain(domain);
}else if (null != request) {
// 设置域名的cookie
cookie.setDomain(getDomainName(request));
}
// 设置path
cookie.setPath("/");
if(StringUtils.isNotBlank(path)){
cookie.setPath(path);
}
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
} catch (Exception e) {
log.error("Cookie Encode Error.", e);
}
}
/**
* 得到cookie的域名
*/
private String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
}
校验用户登录状态
/*校验用户的认证状态*/
public UserInfo verify(HttpServletRequest request, HttpServletResponse response) {
//获取cookie中的token
String token = CookieUtils.getCookieValue(request, jwtProp.getCookie().getCookieName());
//解析token
Payload<UserInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), UserInfo.class);
}catch (Exception e){
//如果解析失败,表示用户没有登录,抛出异常
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
//获取载荷中的用户信息
UserInfo userInfo = payload.getUserInfo();
return userInfo;
}
刷新用户登录状态
JWT内部设置了token的有效期,默认是30分钟,30分钟后用户的登录信息就无效了,用户需要重新登录,用户体验不好,怎么解决呢?
JWT的缺点就凸显出来了:
- JWT是生成后无法更改,因此我们无法修改token中的有效期,也就是无法续签。
怎么办?
3种解决方案:
- 方案1:每次用户访问网站,都重新生成token。操作简单粗暴,但是token写入频率过高,效率实在不好。
- 方案2:登录时,除了生成jwt,还另外生成一个刷新token,每当token过期,如果用户持有刷新token,则为其重新生成一个token。这种方式比较麻烦,而且会增加header大小。
- 方案3:cookie即将到期时,重新生成一个token。比如token有效期为30分钟,当用户请求我们时,我们可以判断如果用户的token有效期还剩下10分钟,那么就重新生成token,可以看做上面两种的折中方案。
我们采用方案3,在验证登录的逻辑中,加入一段时间判断逻辑,如果距离有效期不足10分钟,则生成一个新token:
实现步骤
1.yml
2.修改AuthService中的校验verify方法
/*校验用户的认证状态*/
public UserInfo verify(HttpServletRequest request, HttpServletResponse response) {
//获取cookie中的token
String token = CookieUtils.getCookieValue(request, jwtProp.getCookie().getCookieName());
//解析token
Payload<UserInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), UserInfo.class);
}catch (Exception e){
//如果解析失败,表示用户没有登录,抛出异常
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
//获取载荷中的用户信息
UserInfo userInfo = payload.getUserInfo();
//得到认证token的过期时间
Date expDate = payload.getExpiration();
//得到一个刷新时间点
DateTime refreshDateTime = new DateTime(expDate).minusMinutes(jwtProp.getCookie().getRefreshTime());
//如果刷新时间点在当前时间之前就刷新认证token
if (refreshDateTime.isBeforeNow()) {
createTokenWriteToCookie(request, response, userInfo);
}
return userInfo;
}
退出登录
是不是删除了cookie,用户就完成了退出登录呢?
设想一下,删除了cookie,只是让用户在当前浏览器上的token删除了,但是这个token依然是有效的!这就是JWT的另外一个缺点了,无法控制TOKEN让其失效。如果用户提前备份了token,那么重新填写到cookie后,登录状态依然有效。
所以,我们不仅仅要让浏览器端清除cookie,而且要让这个cookie中的token失效!
实现思路:
大家肯定能想到很多办法,但是无论哪种思路,都绕不可一点:JWT的无法修改特性。因此我们不能修改token来标记token无效,而是在服务端记录token状态,于是就违背了无状态性的特性。
如果要记录每一个token状态,会造成极大的服务端压力,我提供一种思路,可以在轻量级数据量下,解决这个问题:
- 用户进行注销类型操作时(比如退出、修改密码),校验token有效性,并解析token信息
- 把token的id存入redis,并设置有效期为token的剩余有效期
- 校验用户登录状态的接口,除了要正常逻辑外,还必须判断token的id是否存在于redis
- 如果存在,则证明token无效;如果不存在,则证明有效
等于是在Redis中记录失效token的黑名单,黑名单的时间不用太长,最长也就是token的有效期:30分钟,因此服务端数据存储压力会减少。
1.pom
reids依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.yml
spring:
redis:
host: 127.0.0.1
3.把token放进黑名单
@Autowired
private StringRedisTemplate redisTemplate;
public void logout(HttpServletRequest request, HttpServletResponse response) {
//获取token
String token = CookieUtils.getCookieValue(request, jwtProp.getCookie().getCookieName());
//校验token
Payload<Object> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey());
//获取当前要退出的用户的token的id
String tokenId = payload.getId();
//获取当前token的过期时间
Date expDate = payload.getExpiration();
//得到距离过期时间还剩余的毫秒数
long remainTime = expDate.getTime() - System.currentTimeMillis();
//将剩余时间超过6秒的token放入黑名单,其余的不放也差不多该挂了
if(remainTime>6000){
redisTemplate.opsForValue().set(tokenId, "1", remainTime, TimeUnit.MILLISECONDS);
}
}catch (Exception e){
//标识当前要退出登录的用户的token已经失效了,无需任何操作
log.info("当前要退出登录的用户token已经失效!");
}
//无论token是否有效,都要删除token
CookieUtils.deleteCookie(jwtProp.getCookie().getCookieName(),
jwtProp.getCookie().getCookieDomain(), response);
}
4.修改校验token 的service
import org.joda.time.DateTime;
/*校验用户的认证状态*/
public UserInfo verify(HttpServletRequest request, HttpServletResponse response) {
//获取cookie中的token
String token = CookieUtils.getCookieValue(request, jwtProp.getCookie().getCookieName());
//解析token
Payload<UserInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), UserInfo.class);
}catch (Exception e){
//如果解析失败,表示用户没有登录,抛出异常
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
//查看当前token是否在黑名单
if(redisTemplate.hasKey(payload.getId())){
//如果redis中有当前token的id,就认为当前token已经失效了
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
//获取载荷中的用户信息
UserInfo userInfo = payload.getUserInfo();
//得到认证token的过期时间
Date expDate = payload.getExpiration();
//得到一个刷新时间点
// import org.joda.time.DateTime;
DateTime refreshDateTime = new DateTime(expDate).minusMinutes(jwtProp.getCookie().getRefreshTime());
//如果刷新时间点在当前时间之前就刷新认证token
if (refreshDateTime.isBeforeNow()) {
createTokenWriteToCookie(request, response, userInfo);
}
return userInfo;
}
网关的修改
我们实现了登录相关的几个功能,也就是给用户授权。接下来,用户访问我们的系统,我们还需要根据用户的身份,判断是否有权限访问微服务资源,就是鉴权。
大部分的微服务都必须做这样的权限判断,但是如果在每个微服务单独做权限控制,每个微服务上的权限代码就会有重复,如何更优雅的完成权限控制呢?
我们可以在整个服务的入口完成服务的权限控制,这样微服务中就无需再做了,如图:
流程分析:
- 1)获取用户的登录凭证jwt
- 2)解析jwt,获取用户身份
- 如果解析失败,证明没有登录,返回401
- 如果解析成功,继续向下
- 3)根据身份,查询用户权限信息
- 4)获取当前请求资源(微服务接口路径)
- 5)判断是否有访问资源的权限
一般权限信息会存储到数据库,会对应角色表和权限表:
- 角色:就是身份,例如普通用户,金钻用户,黑钻用户,商品管理员
- 权限:就是可访问的访问资源,如果是URL级别的权限控制,包含请求方式、请求路径、等信息
一个角色一般会有多个权限,一个权限也可以属于多个用户,属于多对多关系。根据角色可以查询到对应的所有权限,再根据权限判断是否可以访问当前资源即可。
1.pom
<dependency>
<groupId>com.leyou</groupId>
<artifactId>ly-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.yml
ly:
jwt:
pubKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
3.配置类
package com.leyou.gateway.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;
private PublicKey publicKey;
private CookiePojo cookie = new CookiePojo();
@Data
public class CookiePojo{
private String cookieName;
}
/**
* 指定初始化方法
* @throws Exception
*/
@PostConstruct
public void initMethod() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
4.启动器
@SpringCloudApplication
@EnableConfigurationProperties(JwtProperties.class)
public class LyGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LyGatewayApplication.class, args);
}
}
5.过滤器
package com.leyou.gateway.filter;
import com.leyou.common.auth.pojo.Payload;
import com.leyou.common.auth.pojo.UserInfo;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.gateway.config.JwtProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private JwtProperties jwtProp;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//通过上下文获取request域
ServerHttpRequest request = exchange.getRequest();
//通过上下文获取response域
ServerHttpResponse response = exchange.getResponse();
//获取当前请求的uri
String path = request.getURI().getPath();
//判断当前uri是否在白名单,白名单都是不需要登录的请求
//获取token
String token = null;
//解析token
Payload<UserInfo> payload = null;
try {
token = request.getCookies().getFirst(jwtProp.getCookie().getCookieName()).getValue();
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), UserInfo.class);
}catch (Exception e){
//解析token失败,表示当前用户未登录
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//直接终止当前请求
return response.setComplete();
}
//获取当前用户信息
UserInfo userInfo = payload.getUserInfo();
//成功通过当前过滤器,继续执行其他过滤器
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
6. :网关登录拦截白名单(不拦截)
yml
ly:
filter:
allowPaths:
- /api/auth/login
- /api/search
- /api/user/register
- /api/user/check
- /api/user/code
- /api/item
package com.leyou.gateway.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@ConfigurationProperties(prefix = "ly.filter")
public class FilterProperties {
private List<String> allowPaths;
public List<String> getAllowPaths() {
return allowPaths;
}
public void setAllowPaths(List<String> allowPaths) {
this.allowPaths = allowPaths;
}
}
package com.leyou.gateway.filter;
import com.leyou.common.auth.pojo.Payload;
import com.leyou.common.auth.pojo.UserInfo;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private JwtProperties jwtProp;
@Autowired
private FilterProperties filterProp;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//通过上下文获取request域
ServerHttpRequest request = exchange.getRequest();
//通过上下文获取response域
ServerHttpResponse response = exchange.getResponse();
//获取当前请求的uri
String path = request.getURI().getPath();
//判断当前uri是否在白名单,白名单都是不需要登录的请求
if(isAllowPath(path)){
//成功通过当前过滤器,继续执行其他过滤器
return chain.filter(exchange);
}
//获取token
String token = null;
//解析token
Payload<UserInfo> payload = null;
try {
token = request.getCookies().getFirst(jwtProp.getCookie().getCookieName()).getValue();
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), UserInfo.class);
}catch (Exception e){
//解析token失败,表示当前用户未登录
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//直接终止当前请求
return response.setComplete();
}
//获取当前用户信息
UserInfo userInfo = payload.getUserInfo();
//成功通过当前过滤器,继续执行其他过滤器
return chain.filter(exchange);
}
//判断当前请求是否是白名单
private Boolean isAllowPath(String path) {
for (String allowPath : filterProp.getAllowPaths()) {
if(path.contains(allowPath)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
添加微服务的token
为微服务之间的调用提供token
如果你的微服务地址不小心暴露了呢?
一旦微服务地址暴露,用户就可以绕过网关,直接请求微服务,那么我们之前做的一切权限控制就白费了!
因此,我们的每个微服务都需要对调用者的身份进行认证,如果不是有效的身份,则应该阻止访问。
实现步骤:
1.我们首先需要把这些合法的调用者身份存入数据库,并给每一个调用者都设置密钥。
2.当访问某个微服务时,需要携带自己的身份信息,比如密钥
3.被调用者验证身份信息身份合法
4.如果验证通过则放行,允许访问
因此,我们必须在一个微服务来管理调用者身份、权限、当然还包括用户的权限,角色等,并对外提供验证调用者身份、查询调用者权限的接口,我们可以再 auth授权中心 中完成这些业务。
1.创建数据库表
服务信息表:tb_application
将来需要根据serviceName和secret来做身份认证,获取token。
CREATE TABLE `tb_application` (
`id` int(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`service_name` varchar(32) NOT NULL COMMENT '服务名称',
`secret` varchar(60) NOT NULL COMMENT '密钥',
`info` varchar(128) DEFAULT NULL COMMENT '服务介绍',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_key_service_name` (`service_name`) USING HASH
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='服务信息表,记录微服务的id,名称,密文,用来做服务认证'
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (1,'user-service','$2a$10$Xw9OGi5vK8FESKJRscHAtu4T2a02CYpdY3Msg2bpq4gZNHSoOXfPi','用户微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (2,'item-service','$2a$10$s5d4LhJT6ScGtsrF/vcHDuV/pQKdPUqkyvw0dSWay70Jxq8ZUd2Am','商品微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (3,'page-service','$2a$10$wYEAcocOu4B.SLNUYMna1uhO8kZQ207ZaDljlro27PWmYuIY29pke','静态页微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (4,'search-service','$2a$10$x/M8Xyr0U6.pUupqtZQEqeSMqIc864g1uEcGU7LKP1rk5gdbuDRM2','搜索微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (5,'cart-service','$2a$10$p/v.iZp/X4wuW8Kj57KnRu1T6nIIrz8NBpEgdxKwoBjVml2M1IE.a','购物车微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (6,'order-service','$2a$10$ntfF/rKCL9tbwEuhDYBHluCbDaiedo29pJyv5A7iENAyg/qe1GPCq','订单微服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (7,'api-gateway','$2a$10$iW4/EHiMFSew/5GmEPyoi.B40Q5gMCFGoTYykyn0ZAe5XoFzNy7O.','网关服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (8,'privilege-service','$2a$10$B4I7M.OGH1UX3cT9UXA3zOdPclznHjsRYLX.v5Fo5pP1zgdiVBbH.','授权服务','2019-04-10 15:55:11','2019-04-10 15:55:11');
insert into `tb_application`(`id`,`service_name`,`secret`,`info`,`create_time`,`update_time`) values (9,'pay-service','$2a$10$5fdUjIWxG5qfyLtUk.B3oOehweZUHDSkGqc5rgAK96MV7Bv8Kfaaa','支付微服务','2019-04-10 15:56:38','2019-04-10 15:56:38');
服务权限表:tb_application_privilege
服务权限记录包含2个信息:
- serviceId:服务id
- targetId:当前服务可以访问的微服务id
可以看做是一个中间表,记录服务与被调用服务的关系。这张表中没有的,就不能调用。
- 每一个服务,都可以有一个或多个可以调用的目标服务。
- 每一个服务,也可以被一个或多个其它服务调用。
因此可以认为是tb_application
的表自关联,多对多关系。
CREATE TABLE `tb_application_privilege` (
`service_id` int(20) NOT NULL COMMENT '服务id',
`target_id` int(20) NOT NULL COMMENT '目标服务id',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`service_id`,`target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='服务权限表,记录服务id以及服务能访问的目标服务的id';
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,1,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,2,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,3,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,4,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,5,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,6,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,7,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,8,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (1,9,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,1,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,2,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,3,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,4,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,5,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,6,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,7,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,8,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (2,9,'2019-04-10 15:55:11');
insert into `tb_application_privilege`(`service_id`,`target_id`,`create_time`) values (3,1,'2019-04-10 15:55:11');
2.授权中心-添加操作
2.1实体类
package com.leyou.auth.entity;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Data
@Table(name = "tb_application")
public class ApplicationInfo {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
/**
* 服务名称
*/
private String serviceName;
/**
* 服务密钥
*/
private String secret;
/**
* 服务信息
*/
private String info;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
2.2yml
添加数据库的配置
spring:
application:
name: auth-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/leyou?allowMultiQueries=true
username: root
password: passw0rd
mybatis:
type-aliases-package: com.leyou.auth.entity
mapper-locations: mappers/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.leyou: trace
2.3查询微服务可访问的微服务列表
application的id查询出可以访问的target的id的集合----写sql
SELECT target_id FROM tb_application_privilege WHERE service_id = #{id}
2.4 提供加密对象所需配置文件
yml
ly:
encoder:
crypt:
secret: ${random.uuid}
strength: 10
创建属性类
package com.leyou.auth.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.security.SecureRandom;
@Data
@Configuration
@ConfigurationProperties(prefix = "ly.encoder.crypt")
public class PasswordConfig {
private int strength;
private String secret;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
// 利用密钥生成随机安全码
SecureRandom secureRandom = new SecureRandom(secret.getBytes());
// 初始化BCryptPasswordEncoder
return new BCryptPasswordEncoder(strength, secureRandom);
}
}
修改属性类
###2.5 提供返回token的客户端信息对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor//无参构造方法
@AllArgsConstructor//全参构造方法
public class AppInfo {
private Long id;
private String serviceName;
private List<Long> targetList;
}
2.6定义controller
/**
* 微服务的授权功能
* @param id 服务id
* @param serviceName 服务名称
* @return 给服务签发的token
*/
@GetMapping("/authorization")
public ResponseEntity<String> authorize(@RequestParam("id") Long id,
@RequestParam("serviceName") String serviceName){
return ResponseEntity.ok(authService.authorize(id, serviceName));
}
2.7service
public String authorize(Long id, String serviceName) {
//校验用户信息是否可用
if(!isUsable(id, serviceName)){
log.error("【服务器申请token】异常!服务id或者服务名称不正确!");
throw new LyException(ExceptionEnum.INVALID_SERVER_ID_SECRET);
}
//查询出当前服务所能访问的服务id列表
List<Long> serviceIds = applicationInfoMapper.queryTargetIdList(id);
//创建返回token的服务信息存储对象
AppInfo appInfo = new AppInfo(id, serviceName, serviceIds);
//生成token
String token = JwtUtils.generateTokenExpireInMinutes(appInfo, jwtProp.getPrivateKey(), jwtProp.getApp().getExpire());
return token;
}
/*根据服务id和服务名称查询服务是否存在*/
public Boolean isUsable(Long id, String serviceName){
//根据服务id查询服务对象
ApplicationInfo applicationInfo = applicationInfoMapper.selectByPrimaryKey(id);
if(applicationInfo==null){
//服务id不对,直接返回false
return false;
}
//验证服务名称是否正确
if(!passwordEncoder.matches(serviceName, applicationInfo.getSecret())){
//服务名称不对,返回false
return false;
}
return true;
}
2.8 对外提供feign接口的模块
@FeignClient("auth-service")
public interface AuthClient {
/**
* 微服务的授权功能
* @param id 服务id
* @param serviceName 服务名称
* @return 给服务签发的token
*/
@GetMapping("/authorization")
public String authorize(@RequestParam("id") Long id,
@RequestParam("serviceName") String serviceName);
}
微服务获取token
现在我们可以把所有要获取token的服务分成两类:
第一:除去ly-auth之外都要通过feign请求的微服务。
第二:ly-auth直接访问自己方法的。
这里不管是通过feign获取token,还是直接在服务内获取token,这个请求都要通过定时任务,自动发送。
网关模块 - 服务获取token之通过feign获取
1.开启定时任务
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringCloudApplication
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
@EnableScheduling v
@EnableFeignClients
public class LyGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LyGatewayApplication.class, args);
}
}
###2. yml
ly:
jwt:
pubKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key.pub # 公钥地址
cookie:
cookieName: LY_TOKEN # cookie名称
app:
id: 7
serviceName: api-gateway
###3. 修改解析配置文件的配置类
package com.leyou.gateway.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
……
private AppTokenPojo app = new AppTokenPojo();
@Data
public class AppTokenPojo{
private Long id;
private String serviceName;
}
……
}
###4.编写定时任务获取服务的认证token
package com.leyou.gateway.scheduled;
import com.leyou.auth.client.AuthClient;
import com.leyou.gateway.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AppTokenScheduled {
/**
* token刷新间隔
*/
private static final long TOKEN_REFRESH_INTERVAL = 86400000L;
/**
* token获取失败后重试的间隔
*/
private static final long TOKEN_RETRY_INTERVAL = 10000L;
/**
* 提供一个存储token的属性
*/
private String token;
/**
* 获取token相关配置信息
*/
@Autowired
private JwtProperties jwtProp;
/**
* 调用获取token的feign接口
*/
@Autowired
private AuthClient authClient;
/**
* 获取token定时任务,每24小时执行一次
*/
@Scheduled(fixedDelay = TOKEN_REFRESH_INTERVAL)
public void autoAppAuth(){
while (true){
try {
//调用远程feign接口获取token
String token = authClient.authorize(jwtProp.getApp().getId(), jwtProp.getApp().getServiceName());
//把token赋值给当前存储属性
this.token = token;
//控制台日志
log.info("【{}微服务自动获取token】成功!",jwtProp.getApp().getServiceName());
//跳出当前死循环
break;
}catch (Exception e){
log.error("【{}微服务自动获取token】失败!十秒钟后会再次获取!",jwtProp.getApp().getServiceName());
}
try {
//保证重试是十秒钟一次
Thread.sleep(TOKEN_RETRY_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 提供一个获取token的方法
*/
public String getToken() {
return token;
}
}
授权中心 - 服务获取token之ly-auth直接获取
###1.开启定时任务
package com.leyou;
import com.leyou.auth.config.JwtProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableScheduling
@EnableConfigurationProperties(JwtProperties.class)
@MapperScan("com.leyou.auth.mapper")
public class LyAuthApplication {
public static void main(String[] args) {
SpringApplication.run(LyAuthApplication.class, args);
}
}
###.提供获取token相关的配置文件
ly:
jwt:
pubKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key.pub # 公钥地址
priKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key # 私钥地址
cookie:
expire: 30 #过期时间设置 单位分钟
refreshTime: 15 #过期时间设置 单位分钟
cookieName: LY_TOKEN # cookie名称
cookieDomain: leyou.com # cookie的域
app:
expire: 1500 #过期时间设置 单位分钟
id: 10
serviceName: auth-service
修改解析配置文件的配置类
package com.leyou.auth.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
……
private AppTokenPojo app = new AppTokenPojo();
@Data
public class AppTokenPojo{
private Integer expire;
private Long id;
private String serviceName;
}
……
}
编写定时任务获取服务的认证token
package com.leyou.auth.scheduled;
import com.leyou.auth.config.JwtProperties;
import com.leyou.auth.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AppTokenScheduled {
/**
* token刷新间隔
*/
private static final long TOKEN_REFRESH_INTERVAL = 86400000L;
/**
* token获取失败后重试的间隔
*/
private static final long TOKEN_RETRY_INTERVAL = 10000L;
/**
* 提供一个存储token的属性
*/
private String token;
/**
* 获取token相关配置信息
*/
@Autowired
private JwtProperties jwtProp;
/**
* 注入业务代码
*/
@Autowired
private AuthService authService;
/**
* 获取token定时任务,每24小时执行一次
*/
@Scheduled(fixedDelay = TOKEN_REFRESH_INTERVAL)
public void autoAppAuth(){
while (true){
try {
String token = authService.authorize(jwtProp.getApp().getId(), jwtProp.getApp().getServiceName());
this.token = token;
//控制台日志
log.info("【{}微服务自动获取token】成功!",jwtProp.getApp().getServiceName());
break;
}catch (Exception e){
log.error("【{}微服务自动获取token】失败!十秒钟后会再次获取!",jwtProp.getApp().getServiceName());
}
try {
Thread.sleep(TOKEN_RETRY_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 提供一个获取token的方法
*/
public String getToken() {
return token;
}
}
服务鉴权:
服务请求携带token思路分析:
这里,依然要讲所有的服务分成两类:
第一:feign访问,除去网关微服务之外所有的服务。
第二:网关访问,只有网关微服务。
/所有微服务发起请求时携带的token存放的头信息的key/
修改授权中心的全局过滤器 - 服务请求携带token网关类
package com.leyou.gateway.filter;
import com.leyou.common.auth.pojo.Payload;
import com.leyou.common.auth.pojo.UserInfo;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.constant.LyConstants;
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import com.leyou.gateway.scheduled.AppTokenScheduled;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private JwtProperties jwtProp;
@Autowired
private FilterProperties filterProp;
@Autowired
private AppTokenScheduled appTokenScheduled;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
……
//修改request域,在任何网关发起的请求中添加请求头信息
// APP_TOKEN_HEADER = "APP_TOKEN_HEADER";
ServerHttpRequest newRequest = request.mutate().header(LyConstants.APP_TOKEN_HEADER, appTokenScheduled.getToken()).build();
//修改网关的上下文exchange,修改上下文中的request域
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
……
//判断当前uri是否在白名单,白名单都是不需要登录的请求
if(isAllowPath(path)){
//成功通过当前过滤器,继续执行其他过滤器
return chain.filter(newExchange);
}
……
//成功通过当前过滤器,继续执行其他过滤器
return chain.filter(newExchange);
}
……
}
feign拦截器- 服务请求携带token之feign请求携带请求头
编写feign的请求拦截器
package com.leyou.auth.feign;
import com.leyou.auth.scheduled.AppTokenScheduled;
import com.leyou.common.constant.LyConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AuthFeignInterceptor implements RequestInterceptor {
@Autowired
private AppTokenScheduled appTokenScheduled;
@Override
public void apply(RequestTemplate template) {
//在feign的请求中添加请求头信息
template.header(LyConstants.APP_TOKEN_HEADER, appTokenScheduled.getToken());
}
}
服务鉴权:每个微服务校验访问权限
思路:如果我们在每个处理器内部,对访问者进行权限校验,就会使得权限校验的代码与业务代码耦合太紧密,springmvc为我们提供了aop思想的拦截器,可以解开此耦合。
这里,我们拿 ** 用户微服务** 来演示效果。
1.yml
ly:
jwt:
pubKeyPath: D:\ly_guangzhou126\software\rsa_key\jwt_key.pub # 公钥地址
app:
id: 1
serviceName: user-service
2.配置类
package com.leyou.user.config;
import com.leyou.common.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {
private String pubKeyPath;
private PublicKey publicKey;
private AppTokenPojo app = new AppTokenPojo();
@Data
public class AppTokenPojo{
private Long id;
private String serviceName;
}
/**
* 指定初始化方法
* @throws Exception
*/
@PostConstruct
public void initMethod() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
3.拦截器
package com.leyou.user.interceptor;
import com.leyou.common.auth.pojo.AppInfo;
import com.leyou.common.auth.pojo.Payload;
import com.leyou.common.auth.utils.JwtUtils;
import com.leyou.common.constant.LyConstants;
import com.leyou.user.config.JwtProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
@Slf4j
@Component
@EnableConfigurationProperties(JwtProperties.class)
public class AppTokenInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProp;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取token
String token = request.getHeader(LyConstants.APP_TOKEN_HEADER);
if(StringUtils.isBlank(token)){
log.error("【服务鉴权】未通过!来访服务的token不存在!");
//阻止访问处理器
return false;
}
//解析token
Payload<AppInfo> payload = null;
try {
payload = JwtUtils.getInfoFromToken(token, jwtProp.getPublicKey(), AppInfo.class);
//获取当前服务的token的服务信息
AppInfo appInfo = payload.getUserInfo();
//获取服务可以访问的服务列表
List<Long> targetList = appInfo.getTargetList();
//判断当前请求是否有访问权限
if(CollectionUtils.isEmpty(targetList) || !targetList.contains(jwtProp.getApp().getId())){
log.error("【服务鉴权】未通过!来访服务没有访问权限!");
//阻止访问处理器
return false;
}
}catch (Exception e){
log.error("【服务鉴权】未通过!来访服务的token不合法!");
//阻止访问处理器
return false;
}
//有访问权限,顺利通过拦截器
return true;
}
}
package com.leyou.user.config;
import com.leyou.user.interceptor.AppTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private AppTokenInterceptor appTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(appTokenInterceptor);
}
}