侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

OAuth2.0 + Jwt 实现微服务统一认证和鉴权

再见理想
2022-05-27 / 0 评论 / 0 点赞 / 549 阅读 / 2,848 字

一,OAuth2 和 Jwt 的关系

1.1 什么是 OAuth2

OAUth2就是一套广泛流行的认证授权协议,大白话说呢OAuth2这套协议中有两个核心的角色,认证服务器和资源服务器。

OAuth2.0 认证流程

1.2 什么是 Jwt

Jwt(JSON Web Token)就是一个特殊的 token,最大的特性就是无状态,因为它本身可以携带用户的信息(用户ID、用户名、用户的角色集合等)。Jwt 字符串由 Header(头部)、Payload(负载)、Signature(签名)三部分组成。

Header: JSON对象,用来描述 Jwt 的元数据,alg 属性表示签名的算法,type 标识 token 的类型;
Payload: JSON 对象,重要部分,除了默认的字段,还可以扩展自定义字段,比如用户ID、姓名、角色等等;
Signature: 对 Header、Payload 这两部分进行签名,认证服务器使用私钥签名,然后在资源服务器使用公钥验签,防止数据被人动了手脚。

Jwt 和传统的 Cookie/Session 会话管理相比较有着多方面的优势,因为 Cookie/Session 需要在服务器 Session 存用户信息,然后拿客户端Cookie 存储的 SessionId 获取用户信息,这个过程需要消耗服务器的内存和对客户端的要求比较严格(需支持 Cookie),而 Jwt 最大的特性在于就是无状态、去中心化,所以 Jwt 更适用分布式的场景,不需要在多台服务器做会话同步这种消耗服务器性能的操作。

1.3 OAuth2 和 Jwt 的关系

  • OAuth2 是一种认证授权的协议规范。
  • Jwt 是基于 token 的安全认证协议的实现。

OAuth2 的认证服务器签发的 token 可以使用 Jwt 实现,Jwt 轻量且安全。

二,应用架构

我们理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

相关服务划分:

gateway:网关服务,负责请求转发和鉴权功能,整合 Spring Security + Oauth2;

auth:Oauth2 认证服务,负责对登录用户进行认证,整合 Spring Security + Oauth2 + Jwt;

三,认证服务 Auth

主要是对用户进行认证,认证通过后返回 token。

3.1 添加依赖

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

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>

        <!--nacos服务发现-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>

3.2 配置文件

主要添加 Nacos 相关配置;

3.3 生成 RSA 证书

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

keytool -genkey -alias 证书名 -keyalg RSA -keypass 123456 -keystore jwt.jks -storepass 123456

3.4 实现 Spring Security 的 UserDetailsService 接口

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

/**
 * 自定义用户认证和授权
 */
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserFeignService userFeignService;
    private final UmsUserFeignService umsUserFeignService;
    private final BmsStaffFeignService bmsStaffFeignService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String clientId = RequestUtils.getAuthClientId();
        HttpServletRequest request = RequestUtils.getRequest();
        String clientType = request.getParameter("clientType");
        String cropUserId = request.getParameter("cropUserId");
        User user = null;
        switch (clientId) {
            case AuthConstants.ADMIN_CLIENT_ID:
                // 后台用户
                Result<UserDTO> userRes = userFeignService.getUserByUserName(username , AuthConstants.ENTRANCE_TYPE_ADMIN, null);
                if (ResultCode.USER_NOT_EXIST.getCode().equals(userRes.getCode())) {
                    throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
                }
                UserDTO userDTO = userRes.getData();
                userDTO.setClientId(clientId);
                user = new User(userDTO);
                break;
            case AuthConstants.BUSINESS_CLIENT_ID:
                // 商家端
                user = businessUser(username, clientType, clientId, cropUserId);
                break;
            case AuthConstants.WEAPP_CLIENT_ID:
                // 小程序会员
                Result<UmsUserDTO> memberRes = umsUserFeignService.getUserByOpenid(username);
                if (ResultCode.USER_NOT_EXIST.getCode().equals(memberRes.getCode())) {
                    throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
                }
                UmsUserDTO authUserDTO = memberRes.getData();
                authUserDTO.setClientId(clientId);
                user = new User(authUserDTO);
                break;
            default:
                throw new DisabledException("登录失败,客户端授权失败!");
        }
        if (!user.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!user.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        } else if (!user.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期!");
        }
        return user;
    }
}

3.5 认证服务配置(AuthorizationServerConfig)

AuthorizationServerConfig这个配置类是整个认证服务实现的核心,通过继承 AuthorizationServerConfigurerAdapter 实现认证服务相关配置。总结下来就是两个关键点,客户端信息配置和 access_token 生成配置。

/**
 * 授权服务配置
 */
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final DataSource dataSource;
    private final AuthenticationManager authenticationManager;
    private final UserDetailsServiceImpl userDetailsService;

    /**
     * 配置客户端详情(数据库)
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        JdbcClientDetailsServiceImpl jdbcClientDetailsService = new JdbcClientDetailsServiceImpl(dataSource);
        jdbcClientDetailsService.setFindClientDetailsSql(AuthConstants.FIND_CLIENT_DETAILS_SQL);
        jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstants.SELECT_CLIENT_DETAILS_SQL);
        clients.withClientDetails(jdbcClientDetailsService);
    }

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .userDetailsService(userDetailsService)
				.reuseRefreshTokens(true);
 // refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
 // 1 重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
 // 2 非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        /*security.allowFormAuthenticationForClients();*/
        CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
        endpointFilter.afterPropertiesSet();
        endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
        security.addTokenEndpointAuthenticationFilter(endpointFilter);

        security.authenticationEntryPoint(authenticationEntryPoint())
                .tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()");
    }


    /**
     * 自定义认证异常响应数据
     * @return
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.HTTP_OK);
            response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Cache-Control", "no-cache");
            Result result = Result.failed(ResultCode.CLIENT_AUTHENTICATION_FAILED);
            response.getWriter().print(JSONUtil.toJsonStr(result));
            response.getWriter().flush();
        };
    }


    /**
     * 使用非对称加密算法对token签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 从classpath下的密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair("xxx");
        return keyPair;
    }

    /**
     * JWT内容增强
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
       return (accessToken, authentication) -> {
            Map<String, Object> map = new HashMap<>(8);
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            map.put("userId", user.getId());
            map.put("clientId", user.getClientId());
            map.put("username",user.getUsername());
            if (user.getHeadurl() != null){
                map.put("headurl",user.getHeadurl());
            }
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }
}

3.6 暴露获取 Jwt 公钥接口

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

@RestController
@RequestMapping("/auth")
public class PublicKeyController {
    private KeyPair keyPair;

    @GetMapping("/getPublicKey")
    public Map<String, Object> loadPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }
}

3.7 安全配置 WebSecurityConfig

安全配置主要是配置请求访问权限、定义认证管理器、密码加密配置。

@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
            .and()
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .csrf().disable();
    }

    /**
     * 如果不配置SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

四,资源服务器

网关这里是担任资源服务器的角色,因为网关是微服务资源访问的统一入口,所以在这里做资源访问的统一鉴权是再合适不过。

4.1 添加依赖

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

<!--OAuth2资源服务器-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
		<!--    gateway    -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
		<!--nacos服务发现-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>

4.2 修改配置

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

server:
  port: 
app:
  id: cloud-gateway
spring:
  application:
    name: cloud-gateway
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://ip/auth/getPublicKey
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: 
    gateway:
      discovery:
        locator:
          enabled: true 
      routes:
        - id: sec_routh
          uri: lb://cloud-seckill
          predicates:
            - Path=/sec/**

# 配置白名单路径
whitelist:
  urls:
    - /auth/**
    - /favicon.ico
    - /webjars/**
    - /doc.html
    - /swagger-resources/**
    - /*/v2/api-docs
    - /*/api.pub/**

4.3 鉴权管理器

@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String path = request.getMethodValue() + "_" + request.getURI().getPath();
        log.info("请求,path={}", path);
        // 对应跨域的预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // token为空拒绝访问
        String token = request.getHeaders().getFirst(AuthConstants.AUTHORIZATION_KEY);
        if (StrUtil.isEmpty(token) || (!token.startsWith(AuthConstants.AUTHORIZATION_PREFIX) && !token.startsWith(AuthConstants.BASIC_PREFIX))) {
            log.info("请求token为空拒绝访问,path={}", path);
            return Mono.just(new AuthorizationDecision(false));
        }

        // 缓存取资源权限角色关系列表
		
        // 请求路径匹配到的资源需要的角色权限集合authorities

        return Mono.just(new AuthorizationDecision(true));
    }
}

4.4 资源服务器配置

这里做的工作是将鉴权管理器 AuthorizationManager 配置到资源服务器、请求白名单放行、无权访问和无效 token 的自定义异常响应。配置白名单的资源不用再走鉴权管理器。

@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    private final AuthorizationManager authorizationManager;
    private final WhiteListConfig whiteListConfig;

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        // 自定义处理JWT请求头过期或签名错误的结果
        http.oauth2ResourceServer().authenticationEntryPoint(authenticationEntryPoint());
        http.authorizeExchange()
				//放行白名单,不用走鉴权管理器
                .pathMatchers(ArrayUtil.toArray(whiteListConfig.getUrls(), String.class)).permitAll()
                .anyExchange().access(authorizationManager)
                .and()
                .exceptionHandling()
                // 处理未授权
                .accessDeniedHandler(accessDeniedHandler())
                //处理未认证
                .authenticationEntryPoint(authenticationEntryPoint())
                .and().csrf().disable();
        return http.build();
    }

    /**
     * 未授权
     */
    @Bean
    ServerAccessDeniedHandler accessDeniedHandler() {
        return (exchange, denied) -> {
            Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
                    .flatMap(response -> WebUtils.writeFailedToResponse(response, ResultCode.ACCESS_UNAUTHORIZED));

            return mono;
        };
    }

    /**
     * token无效或者已过期自定义响应
     */
    @Bean
    ServerAuthenticationEntryPoint authenticationEntryPoint() {
        return (exchange, e) -> {
            Mono<Void> mono = Mono.defer(() -> Mono.just(exchange.getResponse()))
                    .flatMap(response -> WebUtils.writeFailedToResponse(response,ResultCode.TOKEN_INVALID_OR_EXPIRED));
            return mono;
        };
    }

    /**
     * ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication
     * 需要把jwt的Claim中的authorities加入
     * 方案:重新定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter
     */
    @Bean
    public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.JWT_AUTHORITIES_KEY);

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
}

至此,资源服务器已整合完成,可通过接口验证。

文章参考

Spring Cloud Gateway + Spring Security OAuth2 + JWT实现微服务统一认证授权鉴权
微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!

0

评论区