授权中心的操作

授权中心的主要职责:

  • 用户登录鉴权:
    • 接收用户的登录请求,
    • 通过用户中心的接口校验用户名密码
    • 使用私钥生成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);
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
课程介绍 【完善体系+精品资料】本课程总计115课时,打造全网最全的微服务体系课程;从微服务是什么、能够做什么开始讲起,绝对零基础入门到精通类型。课程整体脉络十分清晰,每个章节一个知识点,画图+源码+运行讲解,不信你学不会。1、课程先讲解了什么是单体架构、什么是微服务架构、他们之间有什么区别和联系,各自有什么优缺点。2、从本质入手,使用最简单的Spring Boot搭建微服务,让你认清微服务是一种思想和解决问题的手段,而不是新兴技术。3、讲解Spring Boot 与 Spring Cloud 微服务架构之间的联系,原生的RestTemplate工具,以及Actuator监控端点的使用。4、带着微服务所带来的各种优缺点,为大家引入服务发现与注册的概念和原理,从而引入我们的第一个注册中心服务Eureka。5、引入负载均衡的理念,区分什么是服务端负载均衡,什么是客户端负载均衡,进而引入Ribbon负载均衡组件的详细使用。6、为了解决微服务之间复杂的调用,降低代码的复杂度,我们引入了Feign声明式客户端,让你几行代码学习服务的远程调用。7、为了解决服务之间的稳定性,避免发生雪崩问题,我们引入了Hystrix断路器,服务降级和熔断机制。8、微服务集群十分庞大,监控起来是十分困难的,尤其是对每一个接口的熔断情况进行监控,因此我们引入了Turbine微服务监控。9、微服务的调用是杂乱无章的,可以网状调用,怎么做到统一的入口出口,统一的授权、加密、解密、日志过滤,我们引入了第一代网关Zuul。10、微服务的配置分散,每次要修改配置都要重启服务,因此我们引入了Config配置中心。11、跟上主流,Consul是当前主流的服务注册与发现、配置中心一体化的解决方案。12、阿里的Nacos服务注册与发现、配置中心在国内炙手可热,Nacos 经历过双十一的微服务中间件。13、Turbin做微服务监控还是太弱,我们需要更强大,可视化,操作性更强的监控系统,因此我引入了Spring Boot Admin体系。14、Zuul已经停止更新支持,Spring Cloud官方推荐的二代网关Spring Cloud Gateway更加强大。15、微服务的安全架构体系虽然复杂,但是是有学习条例的,什么是认证授权、什么是OAuth2.0的原理、 JWT、怎么样去开发实现。 课程资料 【独家资料】1、课程附带全部63个项目源码,其中Hoxton版本项目源码37个,Edgware版本项目26个,2、230页高清PDF正版课件。3、附带nacos、consul、cmder等视频配套软件。学习方法1、每一节课程均有代码,较好的方式为一边听我的讲解,一边使用我提供的项目代码进行观察和运行。2、课程体系庞大,但是并不杂乱,每个章节只针对一个知识点,减轻学习压力。3、坚持每天学习1~2个章节,可以在地铁、公交上用手机学习。【完善知识体系图】

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LC超人在良家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值