微服务解决方案

2021-12-17 22:56:15 浏览数 (1)

架构

通过认证服务(oauth2-auth)进行统一认证,然后通过网关(oauth2-gateway)来统一校验认证和鉴权。采用Nacos作为注册中心,Gateway作为网关,使用nimbus-

jose-jwtJWT库操作JWT令牌。

  • oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security Oauth2
  • ouath2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security Oauth2
  • oauth2-resource:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security Oauth2

具体实现

一、认证服务oauth2-auth

1、首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它,在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖

代码语言:txt复制
<dependencies>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.boot</groupId>
代码语言:txt复制
        <artifactId>spring-boot-starter-web</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.boot</groupId>
代码语言:txt复制
        <artifactId>spring-boot-starter-security</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.cloud</groupId>
代码语言:txt复制
        <artifactId>spring-cloud-starter-oauth2</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>com.nimbusds</groupId>
代码语言:txt复制
        <artifactId>nimbus-jose-jwt</artifactId>
代码语言:txt复制
        <version>8.16</version>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <!-- redis -->
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.boot</groupId>
代码语言:txt复制
        <artifactId>spring-boot-starter-data-redis</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
</dependencies>

2、在application.yml中添加相关配置,主要是Nacos和Redis相关配置

代码语言:txt复制
server:
代码语言:txt复制
  port: 9401
代码语言:txt复制
spring:
代码语言:txt复制
  profiles:
代码语言:txt复制
    active: dev
代码语言:txt复制
  application:
代码语言:txt复制
    name: oauth2-auth
代码语言:txt复制
  cloud:
代码语言:txt复制
    nacos:
代码语言:txt复制
      discovery:
代码语言:txt复制
        server-addr: localhost:8848
代码语言:txt复制
  jackson:
代码语言:txt复制
    date-format: yyyy-MM-dd HH:mm:ss
代码语言:txt复制
  redis:
代码语言:txt复制
    database: 0
代码语言:txt复制
    port: 6379
代码语言:txt复制
    host: localhost
代码语言:txt复制
    password:
代码语言:txt复制
management:
代码语言:txt复制
  endpoints:
代码语言:txt复制
    web:
代码语言:txt复制
      exposure:
代码语言:txt复制
        include: "*"

3、使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可

代码语言:txt复制
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

4、创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息

代码语言:txt复制
package cn.gathub.auth.service.impl;
代码语言:txt复制
import org.springframework.security.authentication.AccountExpiredException;
代码语言:txt复制
import org.springframework.security.authentication.CredentialsExpiredException;
代码语言:txt复制
import org.springframework.security.authentication.DisabledException;
代码语言:txt复制
import org.springframework.security.authentication.LockedException;
代码语言:txt复制
import org.springframework.security.core.userdetails.UserDetails;
代码语言:txt复制
import org.springframework.security.core.userdetails.UsernameNotFoundException;
代码语言:txt复制
import org.springframework.security.crypto.password.PasswordEncoder;
代码语言:txt复制
import org.springframework.stereotype.Service;
代码语言:txt复制
import java.util.ArrayList;
代码语言:txt复制
import java.util.List;
代码语言:txt复制
import java.util.stream.Collectors;
代码语言:txt复制
import javax.annotation.PostConstruct;
代码语言:txt复制
import cn.gathub.auth.constant.MessageConstant;
代码语言:txt复制
import cn.gathub.auth.domain.entity.User;
代码语言:txt复制
import cn.gathub.auth.service.UserService;
代码语言:txt复制
import cn.gathub.auth.service.principal.UserPrincipal;
代码语言:txt复制
import cn.hutool.core.collection.CollUtil;
代码语言:txt复制
/**
代码语言:txt复制
 * 用户管理业务类
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Service
public class UserServiceImpl implements UserService {
代码语言:txt复制
  private List<User> userList;
代码语言:txt复制
  private final PasswordEncoder passwordEncoder;
代码语言:txt复制
  public UserServiceImpl(PasswordEncoder passwordEncoder) {
代码语言:txt复制
    this.passwordEncoder = passwordEncoder;
代码语言:txt复制
  }
代码语言:txt复制
  @PostConstruct
代码语言:txt复制
  public void initData() {
代码语言:txt复制
    String password = passwordEncoder.encode("123456");
代码语言:txt复制
    userList = new ArrayList<>();
代码语言:txt复制
    userList.add(new User(1L, "admin", password, 1, CollUtil.toList("ADMIN")));
代码语言:txt复制
    userList.add(new User(2L, "user", password, 1, CollUtil.toList("USER")));
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
代码语言:txt复制
    List<User> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
代码语言:txt复制
    if (CollUtil.isEmpty(findUserList)) {
代码语言:txt复制
      throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
代码语言:txt复制
    }
代码语言:txt复制
    UserPrincipal userPrincipal = new UserPrincipal(findUserList.get(0));
代码语言:txt复制
    if (!userPrincipal.isEnabled()) {
代码语言:txt复制
      throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
代码语言:txt复制
    } else if (!userPrincipal.isAccountNonLocked()) {
代码语言:txt复制
      throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
代码语言:txt复制
    } else if (!userPrincipal.isAccountNonExpired()) {
代码语言:txt复制
      throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
代码语言:txt复制
    } else if (!userPrincipal.isCredentialsNonExpired()) {
代码语言:txt复制
      throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
代码语言:txt复制
    }
代码语言:txt复制
    return userPrincipal;
代码语言:txt复制
  }
代码语言:txt复制
}

5、创建ClientServiceImpl类实现Spring Security的ClientDetailsService接口,用于加载客户端信息

代码语言:txt复制
package cn.gathub.auth.service.impl;
代码语言:txt复制
import org.springframework.http.HttpStatus;
代码语言:txt复制
import org.springframework.security.crypto.password.PasswordEncoder;
代码语言:txt复制
import org.springframework.security.oauth2.provider.ClientDetails;
代码语言:txt复制
import org.springframework.security.oauth2.provider.ClientRegistrationException;
代码语言:txt复制
import org.springframework.stereotype.Service;
代码语言:txt复制
import org.springframework.web.server.ResponseStatusException;
代码语言:txt复制
import java.util.ArrayList;
代码语言:txt复制
import java.util.List;
代码语言:txt复制
import java.util.stream.Collectors;
代码语言:txt复制
import javax.annotation.PostConstruct;
代码语言:txt复制
import cn.gathub.auth.constant.MessageConstant;
代码语言:txt复制
import cn.gathub.auth.domain.entity.Client;
代码语言:txt复制
import cn.gathub.auth.service.ClientService;
代码语言:txt复制
import cn.gathub.auth.service.principal.ClientPrincipal;
代码语言:txt复制
import cn.hutool.core.collection.CollUtil;
代码语言:txt复制
/**
代码语言:txt复制
 * 客户端管理业务类
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/18
 */
@Service
public class ClientServiceImpl implements ClientService {
代码语言:txt复制
  private List<Client> clientList;
代码语言:txt复制
  private final PasswordEncoder passwordEncoder;
代码语言:txt复制
  public ClientServiceImpl(PasswordEncoder passwordEncoder) {
代码语言:txt复制
    this.passwordEncoder = passwordEncoder;
代码语言:txt复制
  }
代码语言:txt复制
  @PostConstruct
代码语言:txt复制
  public void initData() {
代码语言:txt复制
    String clientSecret = passwordEncoder.encode("123456");
代码语言:txt复制
    clientList = new ArrayList<>();
代码语言:txt复制
    // 1、密码模式
代码语言:txt复制
    clientList.add(Client.builder()
代码语言:txt复制
        .clientId("client-app")
代码语言:txt复制
        .resourceIds("oauth2-resource")
代码语言:txt复制
        .secretRequire(false)
代码语言:txt复制
        .clientSecret(clientSecret)
代码语言:txt复制
        .scopeRequire(false)
代码语言:txt复制
        .scope("all")
代码语言:txt复制
        .authorizedGrantTypes("password,refresh_token")
代码语言:txt复制
        .authorities("ADMIN,USER")
代码语言:txt复制
        .accessTokenValidity(3600)
代码语言:txt复制
        .refreshTokenValidity(86400).build());
代码语言:txt复制
    // 2、授权码模式
代码语言:txt复制
    clientList.add(Client.builder()
代码语言:txt复制
        .clientId("client-app-2")
代码语言:txt复制
        .resourceIds("oauth2-resource2")
代码语言:txt复制
        .secretRequire(false)
代码语言:txt复制
        .clientSecret(clientSecret)
代码语言:txt复制
        .scopeRequire(false)
代码语言:txt复制
        .scope("all")
代码语言:txt复制
        .authorizedGrantTypes("authorization_code,refresh_token")
代码语言:txt复制
        .webServerRedirectUri("https://www.gathub.cn,https://www.baidu.com")
代码语言:txt复制
        .authorities("USER")
代码语言:txt复制
        .accessTokenValidity(3600)
代码语言:txt复制
        .refreshTokenValidity(86400).build());
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
代码语言:txt复制
    List<Client> findClientList = clientList.stream().filter(item -> item.getClientId().equals(clientId)).collect(Collectors.toList());
代码语言:txt复制
    if (CollUtil.isEmpty(findClientList)) {
代码语言:txt复制
      throw new ResponseStatusException(HttpStatus.NOT_FOUND, MessageConstant.NOT_FOUND_CLIENT);
代码语言:txt复制
    }
代码语言:txt复制
    return new ClientPrincipal(findClientList.get(0));
代码语言:txt复制
  }
代码语言:txt复制
}

6、添加认证服务相关配置Oauth2ServerConfig,需要配置加载用户信息的服务UserServiceImpl和加载客户端信息的服务ClientServiceImpl及RSA的钥匙对KeyPair

代码语言:txt复制
package cn.gathub.auth.config;
代码语言:txt复制
import org.springframework.context.annotation.Bean;
代码语言:txt复制
import org.springframework.context.annotation.Configuration;
代码语言:txt复制
import org.springframework.core.io.ClassPathResource;
代码语言:txt复制
import org.springframework.security.authentication.AuthenticationManager;
代码语言:txt复制
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
代码语言:txt复制
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
代码语言:txt复制
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
代码语言:txt复制
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
代码语言:txt复制
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
代码语言:txt复制
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
代码语言:txt复制
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
代码语言:txt复制
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
代码语言:txt复制
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
代码语言:txt复制
import java.security.KeyPair;
代码语言:txt复制
import java.util.ArrayList;
代码语言:txt复制
import java.util.List;
代码语言:txt复制
import cn.gathub.auth.component.JwtTokenEnhancer;
代码语言:txt复制
import cn.gathub.auth.service.ClientService;
代码语言:txt复制
import cn.gathub.auth.service.UserService;
代码语言:txt复制
import lombok.AllArgsConstructor;
代码语言:txt复制
/**
代码语言:txt复制
 * 认证服务器配置
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
代码语言:txt复制
  private final UserService userService;
代码语言:txt复制
  private final ClientService clientService;
代码语言:txt复制
  private final AuthenticationManager authenticationManager;
代码语言:txt复制
  private final JwtTokenEnhancer jwtTokenEnhancer;
代码语言:txt复制
  @Override
代码语言:txt复制
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
代码语言:txt复制
//    clients.inMemory()
代码语言:txt复制
//        // 1、密码模式
代码语言:txt复制
//        .withClient("client-app")
代码语言:txt复制
//        .secret(passwordEncoder.encode("123456"))
代码语言:txt复制
//        .scopes("read,write")
代码语言:txt复制
//        .authorizedGrantTypes("password", "refresh_token")
代码语言:txt复制
//        .accessTokenValiditySeconds(3600)
代码语言:txt复制
//        .refreshTokenValiditySeconds(86400)
代码语言:txt复制
//        .and()
代码语言:txt复制
//        // 2、授权码授权
代码语言:txt复制
//        .withClient("client-app-2")
代码语言:txt复制
//        .secret(passwordEncoder.encode("123456"))
代码语言:txt复制
//        .scopes("read")
代码语言:txt复制
//        .authorizedGrantTypes("authorization_code", "refresh_token")
代码语言:txt复制
//        .accessTokenValiditySeconds(3600)
代码语言:txt复制
//        .refreshTokenValiditySeconds(86400)
代码语言:txt复制
//        .redirectUris("https://www.gathub.cn", "https://www.baidu.com");
代码语言:txt复制
    clients.withClientDetails(clientService);
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
代码语言:txt复制
    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
代码语言:txt复制
    List<TokenEnhancer> delegates = new ArrayList<>();
代码语言:txt复制
    delegates.add(jwtTokenEnhancer);
代码语言:txt复制
    delegates.add(accessTokenConverter());
代码语言:txt复制
    enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
代码语言:txt复制
    endpoints.authenticationManager(authenticationManager)
代码语言:txt复制
        .userDetailsService(userService) //配置加载用户信息的服务
代码语言:txt复制
        .accessTokenConverter(accessTokenConverter())
代码语言:txt复制
        .tokenEnhancer(enhancerChain);
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public void configure(AuthorizationServerSecurityConfigurer security) {
代码语言:txt复制
    security.allowFormAuthenticationForClients();
代码语言:txt复制
  }
代码语言:txt复制
  @Bean
代码语言:txt复制
  public JwtAccessTokenConverter accessTokenConverter() {
代码语言:txt复制
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
代码语言:txt复制
    jwtAccessTokenConverter.setKeyPair(keyPair());
代码语言:txt复制
    return jwtAccessTokenConverter;
代码语言:txt复制
  }
代码语言:txt复制
  @Bean
代码语言:txt复制
  public KeyPair keyPair() {
代码语言:txt复制
    // 从classpath下的证书中获取秘钥对
代码语言:txt复制
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray());
代码语言:txt复制
    return keyStoreKeyFactory.getKeyPair("jwt", "654321".toCharArray());
代码语言:txt复制
  }
代码语言:txt复制
}

7、如果你想往JWT中添加自定义信息的话,比如说登录用户的ID,可以自己实现TokenEnhancer接口

代码语言:txt复制
package cn.gathub.auth.component;
代码语言:txt复制
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
代码语言:txt复制
import org.springframework.security.oauth2.common.OAuth2AccessToken;
代码语言:txt复制
import org.springframework.security.oauth2.provider.OAuth2Authentication;
代码语言:txt复制
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
代码语言:txt复制
import org.springframework.stereotype.Component;
代码语言:txt复制
import java.util.HashMap;
代码语言:txt复制
import java.util.Map;
代码语言:txt复制
import cn.gathub.auth.service.principal.UserPrincipal;
代码语言:txt复制
/**
代码语言:txt复制
 * JWT内容增强器
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
    Map<String, Object> info = new HashMap<>();
    // 把用户ID设置到JWT中
    info.put("id", userPrincipal.getId());
    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
    return accessToken;
  }
}

8、由于我们的网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来

代码语言:txt复制
package cn.gathub.auth.controller;
代码语言:txt复制
import com.nimbusds.jose.jwk.JWKSet;
代码语言:txt复制
import com.nimbusds.jose.jwk.RSAKey;
代码语言:txt复制
import org.springframework.web.bind.annotation.GetMapping;
代码语言:txt复制
import org.springframework.web.bind.annotation.RestController;
代码语言:txt复制
import java.security.KeyPair;
代码语言:txt复制
import java.security.interfaces.RSAPublicKey;
代码语言:txt复制
import java.util.Map;
代码语言:txt复制
/**
代码语言:txt复制
 * 获取RSA公钥接口
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@RestController
public class KeyPairController {
代码语言:txt复制
  private final KeyPair keyPair;
代码语言:txt复制
  public KeyPairController(KeyPair keyPair) {
代码语言:txt复制
    this.keyPair = keyPair;
代码语言:txt复制
  }
代码语言:txt复制
  @GetMapping("/rsa/publicKey")
代码语言:txt复制
  public Map<String, Object> getKey() {
代码语言:txt复制
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
代码语言:txt复制
    RSAKey key = new RSAKey.Builder(publicKey).build();
代码语言:txt复制
    return new JWKSet(key).toJSONObject();
代码语言:txt复制
  }
代码语言:txt复制
}

9、还需要配置Spring Security,允许获取公钥接口的访问

代码语言:txt复制
package cn.gathub.auth.config;
代码语言:txt复制
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
代码语言:txt复制
import org.springframework.context.annotation.Bean;
代码语言:txt复制
import org.springframework.context.annotation.Configuration;
代码语言:txt复制
import org.springframework.security.authentication.AuthenticationManager;
代码语言:txt复制
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
代码语言:txt复制
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
代码语言:txt复制
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
代码语言:txt复制
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
代码语言:txt复制
import org.springframework.security.crypto.password.PasswordEncoder;
代码语言:txt复制
/**
代码语言:txt复制
 * SpringSecurity配置
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
代码语言:txt复制
  @Override
代码语言:txt复制
  protected void configure(HttpSecurity http) throws Exception {
代码语言:txt复制
    http.authorizeRequests()
代码语言:txt复制
        .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
代码语言:txt复制
        .antMatchers("/rsa/publicKey").permitAll()
代码语言:txt复制
        .anyRequest().authenticated();
代码语言:txt复制
  }
代码语言:txt复制
  @Bean
代码语言:txt复制
  @Override
代码语言:txt复制
  public AuthenticationManager authenticationManagerBean() throws Exception {
代码语言:txt复制
    return super.authenticationManagerBean();
代码语言:txt复制
  }
代码语言:txt复制
  @Bean
代码语言:txt复制
  public PasswordEncoder passwordEncoder() {
代码语言:txt复制
    return new BCryptPasswordEncoder();
代码语言:txt复制
  }
代码语言:txt复制
}

10、创建一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取

代码语言:txt复制
package cn.gathub.auth.service;
代码语言:txt复制
import org.springframework.data.redis.core.RedisTemplate;
代码语言:txt复制
import org.springframework.stereotype.Service;
代码语言:txt复制
import java.util.List;
代码语言:txt复制
import java.util.Map;
代码语言:txt复制
import java.util.TreeMap;
代码语言:txt复制
import javax.annotation.PostConstruct;
代码语言:txt复制
import cn.gathub.auth.constant.RedisConstant;
代码语言:txt复制
import cn.hutool.core.collection.CollUtil;
代码语言:txt复制
/**
代码语言:txt复制
 * 资源与角色匹配关系管理业务类
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Service
public class ResourceServiceImpl {
代码语言:txt复制
  private final RedisTemplate<String, Object> redisTemplate;
代码语言:txt复制
  public ResourceServiceImpl(RedisTemplate<String, Object> redisTemplate) {
代码语言:txt复制
    this.redisTemplate = redisTemplate;
代码语言:txt复制
  }
代码语言:txt复制
  @PostConstruct
代码语言:txt复制
  public void initData() {
代码语言:txt复制
    Map<String, List<String>> resourceRolesMap = new TreeMap<>();
代码语言:txt复制
    resourceRolesMap.put("/resource/hello", CollUtil.toList("ADMIN"));
代码语言:txt复制
    resourceRolesMap.put("/resource/user/currentUser", CollUtil.toList("ADMIN", "USER"));
代码语言:txt复制
    redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);
代码语言:txt复制
  }
代码语言:txt复制
}

二、网关服务oauth2-gateway

接下来搭建网关服务,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作

1、在pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖

代码语言:txt复制
<dependencies>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.boot</groupId>
代码语言:txt复制
        <artifactId>spring-boot-starter-webflux</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.cloud</groupId>
代码语言:txt复制
        <artifactId>spring-cloud-starter-gateway</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.security</groupId>
代码语言:txt复制
        <artifactId>spring-security-config</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.security</groupId>
代码语言:txt复制
        <artifactId>spring-security-oauth2-resource-server</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.security</groupId>
代码语言:txt复制
        <artifactId>spring-security-oauth2-client</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.security</groupId>
代码语言:txt复制
        <artifactId>spring-security-oauth2-jose</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>com.nimbusds</groupId>
代码语言:txt复制
        <artifactId>nimbus-jose-jwt</artifactId>
代码语言:txt复制
        <version>8.16</version>
代码语言:txt复制
    </dependency>
代码语言:txt复制
</dependencies>

2、在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置

代码语言:txt复制
server:
代码语言:txt复制
  port: 9201
代码语言:txt复制
spring:
代码语言:txt复制
  profiles:
代码语言:txt复制
    active: dev
代码语言:txt复制
  application:
代码语言:txt复制
    name: oauth2-gateway
代码语言:txt复制
  cloud:
代码语言:txt复制
    nacos:
代码语言:txt复制
      discovery:
代码语言:txt复制
        server-addr: localhost:8848
代码语言:txt复制
    gateway:
代码语言:txt复制
      routes: # 配置路由路径
代码语言:txt复制
        - id: oauth2-resource-route
          uri: lb://oauth2-resource
          predicates:
            - Path=/resource/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-route
          uri: lb://oauth2-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-login
          uri: lb://oauth2-auth
          predicates:
            - Path=/login
          filters:
            - PreserveHostHeader
        - id: oauth2-auth-token
          uri: lb://oauth2-auth
          predicates:
            - Path=/oauth/token
          filters:
            - PreserveHostHeader
        - id: oauth2-auth-authorize
          uri: lb://oauth2-auth
          predicates:
            - Path=/oauth/authorize
          filters:
            - PreserveHostHeader
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能
          lower-case-service-id: true # 使用小写服务名,默认是大写
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:9401/rsa/publicKey' # 配置RSA的公钥访问地址
  redis:
    database: 0
    port: 6379
    host: localhost
    password:
secure:
  ignore:
    urls: # 配置白名单路径
      - "/actuator/**"
      - "/oauth/token"
      - "/oauth/authorize"
      - "/login"

3、对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启

代码语言:txt复制
package cn.gathub.gateway.config;
代码语言:txt复制
import org.springframework.context.annotation.Bean;
代码语言:txt复制
import org.springframework.context.annotation.Configuration;
代码语言:txt复制
import org.springframework.core.convert.converter.Converter;
代码语言:txt复制
import org.springframework.security.authentication.AbstractAuthenticationToken;
代码语言:txt复制
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
代码语言:txt复制
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
代码语言:txt复制
import org.springframework.security.config.web.server.ServerHttpSecurity;
代码语言:txt复制
import org.springframework.security.oauth2.jwt.Jwt;
代码语言:txt复制
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
代码语言:txt复制
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
代码语言:txt复制
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
代码语言:txt复制
import org.springframework.security.web.server.SecurityWebFilterChain;
代码语言:txt复制
import cn.gathub.gateway.authorization.AuthorizationManager;
代码语言:txt复制
import cn.gathub.gateway.component.RestAuthenticationEntryPoint;
代码语言:txt复制
import cn.gathub.gateway.component.RestfulAccessDeniedHandler;
代码语言:txt复制
import cn.gathub.gateway.constant.AuthConstant;
代码语言:txt复制
import cn.gathub.gateway.filter.IgnoreUrlsRemoveJwtFilter;
代码语言:txt复制
import cn.hutool.core.util.ArrayUtil;
代码语言:txt复制
import lombok.AllArgsConstructor;
代码语言:txt复制
import reactor.core.publisher.Mono;
代码语言:txt复制
/**
代码语言:txt复制
 * 资源服务器配置
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
  private final AuthorizationManager authorizationManager;
  private final IgnoreUrlsConfig ignoreUrlsConfig;
  private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
  private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
  private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
代码语言:txt复制
  @Bean
代码语言:txt复制
  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
代码语言:txt复制
    http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
代码语言:txt复制
    // 1、自定义处理JWT请求头过期或签名错误的结果
代码语言:txt复制
    http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
代码语言:txt复制
    // 2、对白名单路径,直接移除JWT请求头
代码语言:txt复制
    http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
代码语言:txt复制
    http.authorizeExchange()
代码语言:txt复制
        .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll() // 白名单配置
代码语言:txt复制
        .anyExchange().access(authorizationManager) // 鉴权管理器配置
代码语言:txt复制
        .and().exceptionHandling()
代码语言:txt复制
        .accessDeniedHandler(restfulAccessDeniedHandler) // 处理未授权
代码语言:txt复制
        .authenticationEntryPoint(restAuthenticationEntryPoint) // 处理未认证
代码语言:txt复制
        .and().csrf().disable();
代码语言:txt复制
    return http.build();
代码语言:txt复制
  }
代码语言:txt复制
  @Bean
代码语言:txt复制
  public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
代码语言:txt复制
    JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
代码语言:txt复制
    jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
代码语言:txt复制
    jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
代码语言:txt复制
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
代码语言:txt复制
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
代码语言:txt复制
    return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
代码语言:txt复制
  }
代码语言:txt复制
}

4、在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口

代码语言:txt复制
package cn.gathub.gateway.authorization;
代码语言:txt复制
import org.springframework.data.redis.core.RedisTemplate;
代码语言:txt复制
import org.springframework.security.authorization.AuthorizationDecision;
代码语言:txt复制
import org.springframework.security.authorization.ReactiveAuthorizationManager;
代码语言:txt复制
import org.springframework.security.core.Authentication;
代码语言:txt复制
import org.springframework.security.core.GrantedAuthority;
代码语言:txt复制
import org.springframework.security.web.server.authorization.AuthorizationContext;
代码语言:txt复制
import org.springframework.stereotype.Component;
代码语言:txt复制
import java.net.URI;
代码语言:txt复制
import java.util.List;
代码语言:txt复制
import java.util.stream.Collectors;
代码语言:txt复制
import cn.gathub.gateway.constant.AuthConstant;
代码语言:txt复制
import cn.gathub.gateway.constant.RedisConstant;
代码语言:txt复制
import cn.hutool.core.convert.Convert;
代码语言:txt复制
import reactor.core.publisher.Mono;
代码语言:txt复制
/**
代码语言:txt复制
 * 鉴权管理器,用于判断是否有资源的访问权限
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
  private final RedisTemplate<String, Object> redisTemplate;
代码语言:txt复制
  public AuthorizationManager(RedisTemplate<String, Object> redisTemplate) {
代码语言:txt复制
    this.redisTemplate = redisTemplate;
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
代码语言:txt复制
    // 1、从Redis中获取当前路径可访问角色列表
代码语言:txt复制
    URI uri = authorizationContext.getExchange().getRequest().getURI();
代码语言:txt复制
    Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
代码语言:txt复制
    List<String> authorities = Convert.toList(String.class, obj);
代码语言:txt复制
    authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX   i).collect(Collectors.toList());
代码语言:txt复制
    // 2、认证通过且角色匹配的用户可访问当前路径
代码语言:txt复制
    return mono
代码语言:txt复制
        .filter(Authentication::isAuthenticated)
代码语言:txt复制
        .flatMapIterable(Authentication::getAuthorities)
代码语言:txt复制
        .map(GrantedAuthority::getAuthority)
代码语言:txt复制
        .any(authorities::contains)
代码语言:txt复制
        .map(AuthorizationDecision::new)
代码语言:txt复制
        .defaultIfEmpty(new AuthorizationDecision(false));
代码语言:txt复制
  }
代码语言:txt复制
}

5、这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息

代码语言:txt复制
package cn.gathub.gateway.filter;
代码语言:txt复制
import com.nimbusds.jose.JWSObject;
代码语言:txt复制
import org.slf4j.Logger;
代码语言:txt复制
import org.slf4j.LoggerFactory;
代码语言:txt复制
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
代码语言:txt复制
import org.springframework.cloud.gateway.filter.GlobalFilter;
代码语言:txt复制
import org.springframework.core.Ordered;
代码语言:txt复制
import org.springframework.http.server.reactive.ServerHttpRequest;
代码语言:txt复制
import org.springframework.stereotype.Component;
代码语言:txt复制
import org.springframework.web.server.ServerWebExchange;
代码语言:txt复制
import java.text.ParseException;
代码语言:txt复制
import cn.hutool.core.util.StrUtil;
代码语言:txt复制
import reactor.core.publisher.Mono;
代码语言:txt复制
/**
代码语言:txt复制
 * 将登录用户的JWT转化成用户信息的全局过滤器
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
代码语言:txt复制
  private final static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
代码语言:txt复制
  @Override
代码语言:txt复制
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
代码语言:txt复制
    String token = exchange.getRequest().getHeaders().getFirst("Authorization");
代码语言:txt复制
    if (StrUtil.isEmpty(token)) {
代码语言:txt复制
      return chain.filter(exchange);
代码语言:txt复制
    }
代码语言:txt复制
    try {
代码语言:txt复制
      // 从token中解析用户信息并设置到Header中去
代码语言:txt复制
      String realToken = token.replace("Bearer ", "");
代码语言:txt复制
      JWSObject jwsObject = JWSObject.parse(realToken);
代码语言:txt复制
      String userStr = jwsObject.getPayload().toString();
代码语言:txt复制
      LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr);
代码语言:txt复制
      ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
代码语言:txt复制
      exchange = exchange.mutate().request(request).build();
代码语言:txt复制
    } catch (ParseException e) {
代码语言:txt复制
      e.printStackTrace();
代码语言:txt复制
    }
代码语言:txt复制
    return chain.filter(exchange);
代码语言:txt复制
  }
代码语言:txt复制
  @Override
代码语言:txt复制
  public int getOrder() {
代码语言:txt复制
    return 0;
代码语言:txt复制
  }
代码语言:txt复制
}

三、资源服务(API服务)oauth2-resource

最后我们搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它

1、在pom.xml中添加相关依赖,就添加了一个web依赖

代码语言:txt复制
<dependencies>
代码语言:txt复制
    <dependency>
代码语言:txt复制
        <groupId>org.springframework.boot</groupId>
代码语言:txt复制
        <artifactId>spring-boot-starter-web</artifactId>
代码语言:txt复制
    </dependency>
代码语言:txt复制
</dependencies>

2、在application.yml添加相关配置,很常规的配置

代码语言:txt复制
server:
代码语言:txt复制
  port: 9501
代码语言:txt复制
spring:
代码语言:txt复制
  profiles:
代码语言:txt复制
    active: dev
代码语言:txt复制
  application:
代码语言:txt复制
    name: oauth2-resource
代码语言:txt复制
  cloud:
代码语言:txt复制
    nacos:
代码语言:txt复制
      discovery:
代码语言:txt复制
        server-addr: localhost:8848
代码语言:txt复制
management:
代码语言:txt复制
  endpoints:
代码语言:txt复制
    web:
代码语言:txt复制
      exposure:
代码语言:txt复制
        include: "*"

3、创建一个测试接口,网关验证通过即可访问

代码语言:txt复制
package cn.gathub.resource.controller;
代码语言:txt复制
import org.springframework.web.bind.annotation.GetMapping;
代码语言:txt复制
import org.springframework.web.bind.annotation.RestController;
代码语言:txt复制
/**
代码语言:txt复制
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@RestController
public class HelloController {
代码语言:txt复制
  @GetMapping("/hello")
代码语言:txt复制
  public String hello() {
代码语言:txt复制
    return "Hello World !";
代码语言:txt复制
  }
代码语言:txt复制
}

4、创建一个获取登录中的用户信息的接口,用于从请求的Header中直接获取登录用户信息

代码语言:txt复制
package cn.gathub.resource.controller;
代码语言:txt复制
import org.springframework.web.bind.annotation.GetMapping;
代码语言:txt复制
import org.springframework.web.bind.annotation.RequestMapping;
代码语言:txt复制
import org.springframework.web.bind.annotation.RestController;
代码语言:txt复制
import javax.servlet.http.HttpServletRequest;
代码语言:txt复制
import cn.gathub.resource.domain.User;
代码语言:txt复制
import cn.hutool.core.convert.Convert;
代码语言:txt复制
import cn.hutool.json.JSONObject;
代码语言:txt复制
/**
代码语言:txt复制
 * 获取登录用户信息接口
 *
 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
 */
@RestController
@RequestMapping("/user")
public class UserController {
代码语言:txt复制
  @GetMapping("/currentUser")
代码语言:txt复制
  public User currentUser(HttpServletRequest request) {
代码语言:txt复制
    // 从Header中获取用户信息
代码语言:txt复制
    String userStr = request.getHeader("user");
代码语言:txt复制
    JSONObject userJsonObject = new JSONObject(userStr);
代码语言:txt复制
    return User.builder()
代码语言:txt复制
        .username(userJsonObject.getStr("user_name"))
代码语言:txt复制
        .id(Convert.toLong(userJsonObject.get("id")))
代码语言:txt复制
        .roles(Convert.toList(String.class, userJsonObject.get("authorities"))).build();
代码语言:txt复制
  }
代码语言:txt复制
}

功能演示

在此之前先启动我们的 Nacos 和 Redis

服务,然后依次启动oauth2-authoauth2-gatewayoauth2-api服务

我这里测试使用的 Docker 跑的单机版的 Nacos

代码语言:txt复制
docker pull nacos/nacos-server
代码语言:txt复制
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server

1、使用密码模式获取JWT令牌

在这里插入图片描述

2、使用获取到的JWT令牌访问需要权限的接口

在这里插入图片描述

3、使用获取到的JWT令牌访问获取当前登录用户信息的接口,访问地址

在这里插入图片描述

4、当token不存在时

image

5、当JWT令牌过期时,使用refresh_token获取新的JWT令牌

在这里插入图片描述

6、使用授码模式登录时,先访问地址获取授权码:undefined localhost:9201/oauth/authorize?response_type=code&client_id=client- app-2&redirect_uri=重定向地址

7、访问地址,跳转登录页面

8、登录成功,进入授权页面

9、通过授权,拿到授权码

10、拿到授权码,登录

11、使用没有访问权限的user账号登录,访问接口时会返回如下信息

0 人点赞