微服务架构-权限篇

一、涉及的技术(绿色背景)

二、基本流程

三、构建项目(基础环境搭建请参考微服务架构-简介_Morik的博客-CSDN博客)

1、构建聚合项目

1.1、打开idea新建一个springBoot项目(注意alibabaCloud、springCloud、springBoot的版本匹配)详细版本请参考官网:版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub

2、构建公共项目

2.1、idea-->new model-->maven

2.2、pom文件中可以放一些公共的包、后期可以放一些工具类在里边

3、构建资源项目(影视资讯业务)

3.2、同样的方法新建一个model、然后在pom中加入两个核心依赖包

3.3、启动类注册为资源服务器(@EnableResourceServer)

 3.4、配置yml

 3.5、编写业务接口(通过@AuthenticationPrincipal注解获取token的用户信息)

4、构建网关项目(注意:RestTemplate要手动增强,要保证在Getaway启动完成之前通过负载均衡提前把授权服务器上的公钥拿到。如果在Getaway启动完成后批量的订单请求打过来LB还没初始化完成所有请求没公钥无法鉴权全部作废)

4.1、新建model并在pom中新增核心依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>umf</artifactId>
        <groupId>cn.morik</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.morik.umf</groupId>
    <artifactId>route</artifactId>
    <description>网关(鉴权、流控)</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--添加jwt相关的包-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.5</version>
            <scope>runtime</scope>
        </dependency>
        <!--    knife4j     -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

4.2、yml新增主配置和跨域配置

 4.8、新建过滤器

package cn.morik.umf.route.filter;

import cn.morik.umf.route.exception.GateWayException;
import cn.morik.umf.route.utils.MDA;
import cn.morik.umf.route.utils.MorikRestTemplate;
import cn.morik.umf.route.utils.SystemErrorType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
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.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 认证过滤器,根据url判断用户请求是要经过认证 才能访问
 */
@Component
@Slf4j
public class AuthorizationFilter implements GlobalFilter, Ordered, InitializingBean {
    @Autowired
    private MorikRestTemplate restTemplate;
    private PublicKey publicKey;
    private AntPathMatcher matcher = new AntPathMatcher();
    /**
     * 请求各个微服务 不需要用户认证的URL
     */
    private static Set<String> shouldSkipUrl = new LinkedHashSet<>();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String reqPath = exchange.getRequest().getURI().getPath();
        log.info("网关认证开始URL->:{}", reqPath);
        //1:不需要认证的url
        if (shouldSkip(reqPath)) {
            log.info("无需认证的路径");
            return chain.filter(exchange);
        }
        //获取请求头
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        //请求头为空
        if (StringUtils.isEmpty(authHeader)) {
            log.warn("需要认证的url,请求头为空");
            throw new GateWayException(SystemErrorType.UNAUTHORIZED_HEADER_IS_EMPTY);
        }
        //交易我们的jwt 若jwt不对或者超时都会抛出异常
        Claims claims = validateJwtToken(authHeader);
        //向headers中放文件,记得build
        ServerHttpRequest request = exchange.getRequest().mutate().header("username", claims.get("user_name").toString()).build();
        //将现在的request 变成 change对象
        ServerWebExchange serverWebExchange = exchange.mutate().request(request).build();
        //从jwt中解析出权限集合进行判断
        hasPremisson(claims, reqPath);
        return chain.filter(serverWebExchange);
    }

    private Claims validateJwtToken(String authHeader) {
        String token = null;
        try {
            token = StringUtils.substringAfter(authHeader, "bearer ");
            Jwt<JwsHeader, Claims> parseClaimsJwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
            Claims claims = parseClaimsJwt.getBody();
            log.info("claims:{}", claims);
            return claims;
        } catch (Exception e) {
            log.error("校验token异常:{},异常信息:{}", token, e.getMessage());
            throw new GateWayException(SystemErrorType.INVALID_TOKEN);
        }
    }

    private boolean hasPremisson(Claims claims, String currentUrl) {
        boolean hasPremisson = false;
        //登陆用户的权限集合判断
        List<String> premessionList = claims.get("authorities", List.class);
        for (String url : premessionList) {
            if (matcher.match(url, currentUrl)) {
                hasPremisson = true;
                break;
            }
        }
        if (!hasPremisson) {
            log.warn("权限不足");
            throw new GateWayException(SystemErrorType.FORBIDDEN);
        }
        return hasPremisson;
    }

    private boolean shouldSkip(String reqPath) {
        for (String skipPath : shouldSkipUrl) {
            if (matcher.match(skipPath, reqPath)) {
                return true;
            }
        }
        return false;
    }
    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        /**
         *实际上,这边需要通过去数据库读取 不需要认证的URL,不需要认证的URL是各个微服务
         * 开发模块的人员提供出来的. 我在这里没有去查询数据库了,直接模拟写死
         */
        shouldSkipUrl.add("/oauth/token");
        shouldSkipUrl.add("/oauth/check_token");
        shouldSkipUrl.add("/user/getCurrentUser");
//        shouldSkipUrl.add("/film/api/info");
        //初始化公钥
        this.publicKey = genPublicKeyByTokenKey();
    }

    /**
     * 方法实现说明:去认证服务器上获取tokenKey
     */
    private String getTokenKey() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(MDA.clientId, MDA.clientSecret);
        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(null, headers);
        try {
            ResponseEntity<Map> response = restTemplate.exchange(MDA.getTokenKey, HttpMethod.GET, entity, Map.class);
            String tokenKey = response.getBody().get("value").toString();
            log.info("去认证服务器获取TokenKey:{}", tokenKey);
            return tokenKey;
        } catch (Exception e) {
            log.error("远程调用认证服务器获取tokenKey失败:{}", e.getMessage());
            throw new GateWayException(SystemErrorType.GET_TOKEN_KEY_ERROR);
        }
    }

    private PublicKey genPublicKeyByTokenKey() {
        try {
            String tokenKey = getTokenKey();
            String dealTokenKey = tokenKey.replaceAll("\\-*BEGIN PUBLIC KEY\\-*", "").replaceAll("\\-*END PUBLIC KEY\\-*", "").trim();
            java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
            X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(dealTokenKey));
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
            log.info("生成公钥:{}", publicKey);
            return publicKey;
        } catch (Exception e) {
            log.info("生成公钥异常:{}", e.getMessage());
            throw new GateWayException(SystemErrorType.GET_TOKEN_KEY_ERROR);
        }
    }
}

5、构建授权中心

5.1、新建model并导入核心pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>umf</artifactId>
        <groupId>cn.morik</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.morik.umf</groupId>
    <artifactId>oauth</artifactId>
    <description>授权中心</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--nacos 注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--Oauth2的包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!--        </dependency>-->


<!--        <dependency>-->
<!--            <groupId>org.springframework.session</groupId>-->
<!--            <artifactId>spring-session-data-redis</artifactId>-->
<!--        </dependency>-->


        <!--添加A.CTable框架	-->
        <!--        <dependency>-->
        <!--            <groupId>com.gitee.sunchenbin.mybatis.actable</groupId>-->
        <!--            <artifactId>mybatis-enhance-actable</artifactId>-->
        <!--            <version>1.4.9.RELEASE</version>-->
        <!--        </dependency>-->
        <!--mybatis驱动-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!--德鲁伊数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.8</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

5.2、yml配置

server:
  port: 8888
spring:
  application:
    name: oauth-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.9:8848
  datasource:
    username: root
    password: Morik1234567890.
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.1.9:3306/morik-user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
    type: com.alibaba.druid.pool.DruidDataSource

#  redis:
#    host: 192.168.1.9
#    port: 6379
  #    password: root
#  session:
#    store-type: redis
#    timeout: 1800
logging:
  level:
    cn:
      morik:
        umf:
          oauth:
            config:
              role:
                mapper: debug

##A.CTable配置
#mybatis:
#  #自动更新表
#  table:
#    auto: update
#  #实体类扫描地址
#  model:
#    pack: cn.morik.umf.oauth.config.role.entity
#  #数据库类型
#  database:
#    type: mysql


5.3、授权配置

package cn.morik.umf.oauth.indb;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import javax.sql.DataSource;
import java.security.KeyPair;
import java.util.Arrays;

@Configuration
@EnableAuthorizationServer
public class AuthServerInDbConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UMFUserDetailService userDetailsService;


    /**
     * 方法实现说明: 使用jwt存储token,我们创建jwtTOkenStore的时候 需要一个组件
     * jwtAccessTokenConverter  所以我们 可以通过@Bean的形式 创建一个该组件.
     */
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    /**
     * 这个组件 用于jwt basecode 字符串和 安全认证对象的信息转化
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //jwt的密钥(用来保证jwt 字符串的安全性  jwt可以防止篡改  但是不能防窃听  所以jwt不要 放敏感信息)
        converter.setKeyPair(keyPair());
        //converter.setSigningKey("123456");
        return converter;
    }

    /**
     * KeyPair是 非对称加密的公钥和私钥的保存者
     * @return
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }


    /**
     * 该组件就是用来给jwt令牌中添加额外信息的 来增强我们的jwt的令牌信息
     * @return
     */
    @Bean
    public UMFTokenEnhancer umfTokenEnhancer() {
        return new UMFTokenEnhancer();
    }



    /**
     * 方法实现说明:认证服务器能够给哪些 客户端颁发token  我们需要把客户端的配置 存储到
     * 数据库中 可以基于内存存储和db存储
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }

    /**
     * 方法实现说明:用于查找我们第三方客户端的组件 主要用于查找 数据库表 oauth_client_details
     */
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 方法实现说明:授权服务器的配置的配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        /*
          增加我们的令牌信息
         */
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(umfTokenEnhancer(),jwtAccessTokenConverter()));

        endpoints.tokenStore(tokenStore()) //授权服务器颁发的token 怎么存储的
                .tokenEnhancer(tokenEnhancerChain)
                .userDetailsService(userDetailsService) //用户来获取token的时候需要 进行账号密码
                .authenticationManager(authenticationManager);
    }


    /**
     * 方法实现说明:授权服务器安全配置
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //第三方客户端校验token需要带入 clientId 和clientSecret来校验
        security .checkTokenAccess("isAuthenticated()")
                 .tokenKeyAccess("isAuthenticated()");//来获取我们的tokenKey需要带入clientId,clientSecret

        security.allowFormAuthenticationForClients();
    }

}

四、效果展示

1、获取token

 2、通过Getaway获取影视资讯服务信息

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值