Практика аутентификации и авторизации микросервисов Spring Cloud OAuth2 (на основе JWT)

Spring Cloud

1. Введение

В предыдущей главе были представлены базовые теоретические знания о Spring Security Oauth2 и сценариях его использования.Аннотации в этой главе описывают, как реализовать унифицированную аутентификацию и авторизацию микросервисов через Oauth2 в Spring Cloud.

1.1 Решения

В этой статье в основном используются следующие решения:

  • На основе схемы аутентификации токена без сохранения состояния (jwt) серверу не нужно сохранять статус входа пользователя;
  • Основан на фреймворке безопасности spring + протокол oauth2;

Зачем использовать метод jwt? Чтобы избежать необходимости удаленно вызывать службу проверки подлинности и авторизации для каждого запроса, служба проверки подлинности и авторизации проверяет только один раз и возвращает JWT. Возвращенный JWT содержит всю информацию о пользователе, включая информацию о разрешениях.

1.2 Архитектура проектирования случая

Три проекта:

  • eureka-server: центр регистрации, порт 8888. (Он был построен в предыдущих статьях, и в этой статье процесс построения не демонстрируется)
  • auth-server: отвечает за авторизацию. Авторизация требует, чтобы пользователь предоставил clientId и пароль клиента, а также имя пользователя и пароль авторизованного пользователя. После правильной подготовки информации служба auth-service возвращает JWT, который содержит основную информацию о пользователе и информацию о точке авторизации и зашифрован с помощью RSA.
  • auth-client: клиент аутентификации, общедоступная зависимость. Представлены все остальные службы ресурсов
  • пользователь-сервер: как служба ресурсов, ее ресурсы защищены и требуют соответствующих разрешений для доступа. После того, как служба пользователь-сервер получает запрошенный пользователем JWT, она сначала расшифровывает JWT с помощью открытого ключа, получает информацию о пользователе и информацию о разрешениях пользователя, соответствующую JWT, а затем определяет, есть ли у пользователя разрешение на доступ к ресурсу.
  • сервер заказа: такой же, как указано выше

Схема инженерной архитектуры:

Инженерные зависимости:

2. Создайте проект сервера авторизации

2.1 Добавить зависимости maven

Создайте новый модуль auth-server и добавьте следующие зависимости:

<?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>spring-cloud-oauth2</artifactId>
        <groupId>com.hxmec</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>auth-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.8.5</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

2.2 Создайте таблицу данных

Таблицы, необходимые для проекта, в основном включают следующее:

Он в основном используется для хранения информации о клиенте, выданного токена доступа, токена обновления и другой информации в протоколе oauth2. ссылка на описание связанной таблицы oauth2Andrea.com/spring-OA UT…. В демонстрационном проекте токен доступа хранится в виде jwt, поэтому фактически используются только таблица oauth_client_details и таблица с информацией о пользователе sys_user. Если вместо этого вы используете хранилище jdbc, вам нужно использовать другие таблицы.

Оператор создания таблицы выглядит следующим образом:

-- 客户端应用注册详情
create table oauth_client_details (
  client_id VARCHAR(256) PRIMARY KEY, -- 客户端应用的账号
  resource_ids VARCHAR(256),		-- 客户端应用可访问的资源服务器列表,(空代表所有资源服务器都可以访问)
  client_secret VARCHAR(256),	-- 客户端应用的密码
  scope VARCHAR(256),	-- 资源服务器拥有的所有权限列表 (get add delete update)  
  authorized_grant_types VARCHAR(256), -- 客户端支持的授权码模式列表
  web_server_redirect_uri VARCHAR(256), -- 授权码模式,申请授权码后重定向的uri.
  authorities VARCHAR(256),
  access_token_validity INTEGER,   -- 设置颁发token的有效期
  refresh_token_validity INTEGER,  -- 颁发refresh_token的有效期(不设置不会同时颁发refresh_token)
  additional_information VARCHAR(4096),
  autoapprove VARCHAR(256)     -- 设置为true,授权码模式下自动授权
);

create table oauth_client_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256)
);

-- 存放颁发的token
create table oauth_access_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication_id VARCHAR(256) PRIMARY KEY,
  user_name VARCHAR(256),
  client_id VARCHAR(256),
  authentication BLOB,
  refresh_token VARCHAR(256)
);

create table oauth_refresh_token (
  token_id VARCHAR(256),
  token BLOB,
  authentication BLOB
);

-- 授权码模式下,存放颁发的授权码
create table oauth_code (
  code VARCHAR(256), authentication BLOB
);

create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt DATETIME,
	lastModifiedAt DATETIME
);

CREATE TABLE `sys_user` (
  `id` bigint(32) NOT NULL,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(200) DEFAULT NULL,
  `enable_` tinyint(1) DEFAULT NULL,
  `email` varchar(50) DEFAULT NULL,
  `mobile` varchar(20) DEFAULT NULL,
  `del_flag` tinyint(1) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `create_user` bigint(32) DEFAULT NULL,
  `modified_time` datetime DEFAULT NULL,
  `modified_user` bigint(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('app', NULL, '$2a$10$JWSnLszKQANW7OF3p2b8IuIQXTVD8OUN//Q4l/sZGmzyaLEWnC5/u', 'server', 'password,authorization_code,refresh_token,client_credentials', NULL, NULL, 60000, 300, NULL, NULL);
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('order', NULL, '$2a$10$JWSnLszKQANW7OF3p2b8IuIQXTVD8OUN//Q4l/sZGmzyaLEWnC5/u', 'server', 'password,authorization_code,refresh_token,client_credentials', NULL, NULL, 60000, 300, NULL, NULL);
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('user', NULL, '$2a$10$JWSnLszKQANW7OF3p2b8IuIQXTVD8OUN//Q4l/sZGmzyaLEWnC5/u', 'server', 'password,authorization_code,refresh_token,client_credentials', NULL, NULL, 60000, 300, NULL, NULL);

INSERT INTO `sys_user` VALUES (1282941563927805954, 'trazen', '$2a$10$JWSnLszKQANW7OF3p2b8IuIQXTVD8OUN//Q4l/sZGmzyaLEWnC5/u', NULL, 'trazen@126.com', '18559756159', 0, '2020-07-14 15:34:39', NULL, '2020-07-14 15:40:45', NULL);

2.3 Добавить конфигурацию

bootstrap.yml настроен следующим образом

server:
  port: 8889
spring:
  application:
    name: auth-server
logging:
  pattern:
    console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} - %msg%n'
  config: classpath:logback-spring.xml
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/

application.yml настраивается следующим образом:

# mybatis- plus配置
mybatis-plus:
  # xml扫描,多个目录用逗号或者分号隔开隔开
  mapper-locations: classpath:mapper/*.xml
  # 以下配置均有默认值,可以不设置
  global-config:
    db-config:
      #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: ASSIGN_ID
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏
    call-setters-on-nulls: true

spring:
  datasource:
    #driver-class-name: com.mysql.cj.jdbc.Driver
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:p6spy:mysql://192.168.29.188:3306/sc_oauth2?useUnicode=true&characterEncoding=utf-8
      username: root
      password: root
      # 初始连接数
      initial-size: 10
      # 最大连接池数量
      max-active: 100
      # 最小连接池数量
      min-idle: 10
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 打开PSCache,并且指定每个连接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: false

2.4 Настройка службы сведений о пользователе

  • Запрос клиента на выдачу токена на самом деле заключается в доступе к конечной точке токена (TokenEndPoint), которая была инкапсулирована Spring Security oauth2. Интерфейс в конечном итоге вызовет метод UserDetailsService для получения информации о пользователе в соответствии с запрошенной информацией об учетной записи. Обратите внимание, что это только для получения информации о пользователе Проверка информации об учетной записи выполняется Spring Security, поэтому этот интерфейс переписан для получения информации о пользователе из таблицы Mysql.
  • Класс UserDetailsService заключается в том, что когда пользователь запрашивает токен, информация о пользователе может быть загружена в соответствии с именем пользователя и возвращена.После операции проверки пароля учетной записи пользователя AuthenticationManager вызовет аутентификатор для проверки пароля учетной записи пользователя. .

интерфейс MyUserDetailsService.java

/**
 * 功能描述: 继承UserDetailsService接口
 * @author  Trazen
 * @date  2020/7/14 15:43
 */
public interface MyUserDetailsService extends UserDetailsService {
}

Реализовать класс MyUserDetailsServiceImpl

/**
 * 功能描述: 自定义UserDetailsService
 * @author  Trazen
 * @date  2020/7/14 15:43
 */
@Primary
@Service
@AllArgsConstructor
public class MyUserDetailsServiceImpl implements MyUserDetailsService {

    private final SysUserMapper sysUserMapper;

    @Override
    public AuthUserDetail loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(SysUser::getUsername,username);
        SysUser sysUser = sysUserMapper.selectOne(wrapper);
        if(sysUser == null){
            throw new UsernameNotFoundException("用户不存在");
        }else {
            return UserDetailConverter.convert(sysUser);
        }
    }

    public static class UserDetailConverter{
        static AuthUserDetail convert(SysUser user){
            return new AuthUserDetail(user);
        }
    }
}

2.5 Конфигурация аутентификации и авторизации

Весенняя конфигурация безопасности центра аутентификации выглядит следующим образом: Oauth2WebSecurityConfig.java

/**
 * 功能描述:  spring security 配置
 * @author  Trazen
 * @date  2020/7/14 16:00
 */
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class Oauth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final MyUserDetailsService myUserDetailsService;

    /**
     * 重新注入认证管理器
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     * 注入密码加密BCryptPasswordEncoder
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义方式加载用户信息
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().and()
                .csrf().disable()
                .httpBasic();
    }
}

2.6 Расширить энхансер токена TokenEnhancer

В предыдущей статье Spring Security Oauth2 вы могли узнать, что интерфейс по умолчанию для получения токена будет получать только accessToken, refreshToken и другую информацию. В практических сценариях приложений нам также может потребоваться добавить некоторую информацию, связанную с пользователем (например, идентификатор пользователя, имя пользователя, идентификатор организации пользователя и т. д.) В настоящее время информация, возвращаемая по умолчанию, не может удовлетворить наши потребности. Возьмите на себя и переопределите его реализацию метода повышения.

MyTokenEnhancer.java

/**
 * 功能描述: 增强颁发的token的携带信息
 * @author  Trazen
 * @date  2020/7/14 16:07
 */
public class MyTokenEnhancer implements TokenEnhancer {


    /**
     * 客户端模式
     */
    private final static String CLIENT_CREDENTIALS = "client_credentials";

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

        //客户端模式不进行增强
        if (CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {
            return accessToken;
        }
        //获取要增强携带的字段
        AuthUserDetail authUserDetail = (AuthUserDetail) authentication.getPrincipal();


        final Map<String, Object> additionalInfo = new HashMap<>(3);

        //添加token携带的字段
        additionalInfo.put("id", authUserDetail.getSysUser().getId());
        additionalInfo.put("username", authUserDetail.getSysUser().getUsername());
        additionalInfo.put("email", authUserDetail.getSysUser().getEmail());
        additionalInfo.put("mobile", authUserDetail.getSysUser().getMobile());

        DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
        token.setAdditionalInformation(additionalInfo);
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;

    }
}

2.7 Конфигурация сервера аутентификации и авторизации

Настройте компоненты oauth2 центра аутентификации, такие как настраиваемый TokenEnhancer, настраиваемый UserDetailService, перенастроенный AuthenticationManager и источник данных ранее созданной таблицы базы данных.

Код сервера аутентификации и авторизации выглядит следующим образом Oauth2AuthServerConfig.java

/**
 * 功能描述: oauth2 认证服务器配置
 * @author  Trazen
 * @date  2020/7/14 16:11
 */
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class Oauth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;

    private final DataSource dataSource;

    private final MyUserDetailsService myUserDetailsService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        MyClientDetailsService clientDetailsService = new MyClientDetailsService(dataSource);
        clients.withClientDetails(clientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 将增强的token设置到增强链中
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), jwtAccessTokenConverter()));

        endpoints.tokenStore(tokenStore())
                .authenticationManager(authenticationManager)
                // //刷新token的请求会用用到
                .userDetailsService(myUserDetailsService)
                .tokenEnhancer(enhancerChain);
    }


    /**
     * 更改存储token的策略,默认是内存策略,修改为jwt
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        //基于token认证
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jat = new JwtAccessTokenConverter();
        // jwt使用这个key来签名,验证token的服务也使用这个key来验签
        jat.setSigningKey("hxmec");
        return jat;
    }


    /**
     * 添加自定义token增强器实现颁发额外信息的token,因为默认颁发token的字段只有username和ROLE
     * @return
     */
    @Bean
    public TokenEnhancer customTokenEnhancer() {
        //自定义实现
        return new MyTokenEnhancer();
    }
}

2.8 Проверка сервера аутентификации и авторизации

Запустите проекты eureka-server и auth-server, чтобы проверить получение токена доступа. В тестовых данных используются данные, созданные в предыдущем скрипте (для шифрования пароля используется метод BCryptPasswordEncoder). Тестовые данные oauth_client_details выглядят следующим образом: client_id:приложение client_secret: 123456

Данные теста sys_user следующие: имя пользователя: тразен пароль: 123456

Конкретные этапы тестирования следующие: Адрес конечной точки для получения токена: http://{ip}:{port}/oauth/token

Возвращенный access_tokenken можно найти на официальном сайте jwt [Audition.IO/#decoded - Сегодня вечером…] для разрешения адреса.

3. Создайте проект auth-client

Этот проект в основном инкапсулирует конфигурацию, связанную с сервером ресурсов, как общую зависимость.

3.1 Добавить зависимости maven

<?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>spring-cloud-oauth2</artifactId>
        <groupId>com.hxmec</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>auth-client</artifactId>

    <dependencies>
        <!-- security ouath2 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!--导入配置文件处理器,配置文件进行绑定就会有提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.3.3.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.2 Переписать анализатор токенов

код показывает, как показано ниже: MyUserAuthenticationConverter.java

/**
 * 功能描述:  重写token解析器,根据checkToken的结果转化用户信息
 * @author  Trazen
 * @date  2020/7/14 17:16
 */
public class MyUserAuthenticationConverter implements UserAuthenticationConverter {

    private static final String N_A = "N/A";


    @Override
    public Map<String, ?> convertUserAuthentication(Authentication userAuthentication) {
        return null;
    }

    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
        if (!map.containsKey(USERNAME)){
            return null;
        }else{
            CurrentUser user = CurrentUser.builder()
                    .id((Long) map.get("id"))
                    .username((String) map.get(USERNAME))
                    .email((String) map.get("email"))
                    .mobile((String) map.get("mobile"))
                    .build();
            // 有权限信息就格式化权限信息
            if (map.containsKey("authorities") && map.get("authorities") != null){
                Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
                user.setAuthorities(authorities);
                return new UsernamePasswordAuthenticationToken(user, N_A,authorities);
            }else {

                return new UsernamePasswordAuthenticationToken(user, N_A,null);
            }
        }

    }

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
        Object authorities = map.get(AUTHORITIES);
        if (authorities instanceof String) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        }
        if (authorities instanceof Collection) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                    .collectionToCommaDelimitedString((Collection<?>) authorities));
        }else if (authorities == null){

        }
        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
    }

}

3.3 Решить проблему вызова токена, проходящего через интерфейс Feign

Основной код выглядит следующим образом (остальные коды относятся к исходному коду проекта): MyFeignClientInterceptor.java

/**
 *  功能描述: 扩展OAuth2FeignRequestInterceptor
 *  如果特殊场景比如调度任务调用Feign接口等
 *  可以通过过滤指定header头的方式,防止accessTokenContextRelay.copyToken()报错
 * @author  Trazen
 * @date  2020/7/17 21:17
 */
public class MyFeignClientInterceptor extends OAuth2FeignRequestInterceptor {

    private final OAuth2ClientContext oAuth2ClientContext;
    private final AccessTokenContextRelay accessTokenContextRelay;

    /**
     * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
     * within Authorization header
     *
     * @param oAuth2ClientContext     provided context
     * @param resource                type of resource to be accessed
     * @param accessTokenContextRelay
     */
    public MyFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext
            , OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) {
        super(oAuth2ClientContext, resource);
        this.oAuth2ClientContext = oAuth2ClientContext;
        this.accessTokenContextRelay = accessTokenContextRelay;
    }


    /**
     * Create a template with the header of provided name and extracted extract
     * 1. 如果使用 非web 请求,header 区别
     * 2. 根据authentication 还原请求token
     *
     * @param template
     */
    @Override
    public void apply(RequestTemplate template) {
        accessTokenContextRelay.copyToken();
        if (oAuth2ClientContext != null
                && oAuth2ClientContext.getAccessToken() != null) {
            super.apply(template);
        }
    }
}

3.4 Конфигурация сервера ресурсов

код показывает, как показано ниже: MyResourceServerConfig.java

/**
 * 功能描述: 资源服务器配置
 * @author  Trazen
 * @date  2020/7/14 21:37
 */
@Slf4j
@Configuration
@EnableResourceServer
@AllArgsConstructor
@ComponentScan("com.hxmec.auth")
@EnableConfigurationProperties(AuthClientProperties.class)
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {

    private final MyAuthenticationEntryPoint baseAuthenticationEntryPoint;

    private final AuthClientProperties authClientProperties;

    private final RestTemplate lbRestTemplate;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        // 放行 swagger ui (有整合swagger就放行这些请求吧)
        http.authorizeRequests().antMatchers(
                "/v2/api-docs",
                "/swagger-resources/configuration/ui",
                "/swagger-resources",
                "/swagger-resources/configuration/security",
                "/swagger-ui.html",
                "/webjars/**",
                "/api/**/v2/api-docs")
                .permitAll();

        // 根据自定义配置url放行
        if (authClientProperties.getIgnoreUrls() != null){
            for(String url: authClientProperties.getIgnoreUrls()){
                http.authorizeRequests().antMatchers(url).permitAll();
            }
        }
        // 其他请求均需要token才能访问
        http.authorizeRequests().anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        if (authClientProperties.getResourceId() != null) {
            resources.resourceId(authClientProperties.getResourceId());
        }

        // 这里的签名key 保持和认证中心一致
        if (authClientProperties.getSigningKey() == null) {
            log.info("SigningKey is null cant not decode token.......");
        }

        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());

        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //设置解析jwt的密钥
        converter.setSigningKey(authClientProperties.getSigningKey());
        converter.setVerifier(new MacSigner(authClientProperties.getSigningKey()));

        MyTokenServices tokenServices = new MyTokenServices();

        // 在CustomTokenServices注入三个依赖对象
        //设置token存储策略
        tokenServices.setTokenStore(new JwtTokenStore(converter));
        tokenServices.setJwtAccessTokenConverter(converter);
        tokenServices.setDefaultAccessTokenConverter(accessTokenConverter);
        tokenServices.setRestTemplate(lbRestTemplate);
        resources.tokenServices(tokenServices)
                .authenticationEntryPoint(baseAuthenticationEntryPoint);
    }
}

4. Создайте проект пользователя-сервера/сервера-заказа приложения сервера ресурсов.

Этапы построения приложения сервера ресурсов такие же, сервер заказов в основном демонстрирует функцию передачи токена через вызов интерфейса feign.

4.1 Добавление зависимостей maven

<?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>spring-cloud-oauth2</artifactId>
        <groupId>com.hxmec</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-server</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.hxmec</groupId>
            <artifactId>auth-client</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- SpringRetry 重试框架依赖 -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.2 Создайте класс запуска Spring Boot

UserServerApplication.java

/**
 * 功能描述: User Server启动类
 * @author  Trazen
 * @date  2020/7/15 9:55
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServerApplication.class, args);
    }
}

4.3 Добавить конфигурацию

конфигурация bootstrap.yml

server:
  port: 8890
spring:
  application:
    name: user-server
  cloud:
    loadbalancer:
      retry:
        #开启重试机制
        enabled: true
logging:
  pattern:
    console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} - %msg%n'
  config: classpath:logback-spring.xml
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/

конфигурация application.yml

hx:
  oauth2:
    client:
      # jwt的密钥
      signingKey: hxmec
      resourceId: ${spring.application.name}
      # 放行的url
      ignoreUrls:
        - /oauth/**
        - /user/**



#ribbon全局配置
ribbon:
  #处理请求的超时时间,单位ms,默认1000
  ReadTimeout: 3000
  #连接建立的超时时间,单位ms,默认1000
  ConnectTimeout: 3000

feign:
  compression:
    request:
      #是否启用请求GZIP压缩,true:启用,false:不启用
      enabled: true
      #压缩支持的MIME TYPE
      mime-types: text/xml,application/xml,application/json
      #压缩数据的最小值
      min-request-size: 2048
    response:
      #是否启用响应GZIP压缩,true:启用,false:不启用
      enabled: true
  client:
    config:
      #feign全局配置
      default:
        #指定日志级别,none:不记录任何日志,basic:仅记录请求方法、URL、响应状态代码以及执行时间(适合生产环境)
        #headers:在basic基础上,记录请求和响应的header,full:记录请求和响应的header、body和元数据,默认none
        loggerLevel: basic
      #feign指定客户端配置,即仅对指定调用的服务生效
      eureka-client:
        loggerLevel: full

4.4 Написание тестовых интерфейсов

UserController.java

/**
 * 功能描述: 
 * @author  Trazen
 * @date  2020/7/15 10:39
 */
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @GetMapping("/a")
    @PreAuthorize("isAuthenticated()")
    public String get(@AuthenticationPrincipal CurrentUser currentUser){
        return "1";
    }

    @GetMapping("/b")
    public String get02(){
        log.info("---------------->{}",SecurityUtils.getCurrentUser());
        return SecurityUtils.getCurrentUser().getUsername();
    }

    @GetMapping("/c")
    public String get03(@AuthenticationPrincipal CurrentUser currentUser){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("---------------->{}",currentUser);
        return "3";
    }

}

4.5 Разверните сервер заказов, как указано выше

Демонстрационный код вызова интерфейса Feign выглядит следующим образом:

/**
 * 功能描述:
 * @author  Trazen
 * @date  2020/7/15 17:41
 */
@FeignClient(value = "user-server")
public interface UserFeignApi {

    /**
     * user服务b接口
     * @return
     */
    @GetMapping("/user/b")
    String get02();

}
/**
 * 功能描述: 
 * @author  Trazen
 * @date  2020/7/15 16:16
 */
@RestController
@RequestMapping("/order")
@Slf4j
@AllArgsConstructor
public class OrderController {

    private final UserFeignApi userFeignApi;

    @GetMapping("/a")
    public String get02(@AuthenticationPrincipal CurrentUser currentUser){
        log.info("---------------->{}", SecurityUtils.getCurrentUser());
        return "1";
    }

    @GetMapping("/b")
    public String getFeign(){
        return userFeignApi.get02();
    }
}

4.6 Запустите проект для тестирования

Запустите следующие проекты: эврика-сервер----> сервер-аутентификации----> сервер-пользователь/сервер-заказа

  • 1. Получить Токен через интерфейс службы аутентификации и авторизации

  • 2. Принесите токен для доступа к интерфейсу в пользовательском сервере

Если токен действителен и нормально возвращает результат; Если токен недействителен, он вернет пользовательскую информацию об истечении срока авторизации.

  • 3. Посетите интерфейс сервера заказов и продемонстрируйте вызов интерфейса feign для передачи токена.

5. Адрес проекта:

GitHub.com/tianya197287300…

6. Справочные документы: