SpringCloud微服务安全网关安全 3-3 搭建OAuth2 认证服务器

Posted 鮀城小帅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud微服务安全网关安全 3-3 搭建OAuth2 认证服务器相关的知识,希望对你有一定的参考价值。

1. OAuth2 的角色和流程

2. OAuth2 认证服务器搭建

2.1 创建认证服务器工程

(1)创建is-server-auth 认证服务器工程

(2)pom.xml 依赖

以下是使用 OAuth2 的主要依赖配置

                <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
			<version>2.2.4.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
			<version>2.2.4.RELEASE</version>
		</dependency>
                <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-dependencies</artifactId>
			<version>2.3.4.RELEASE</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
                <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>Greenwich.SR2</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>

2.2 创建 OAuth2AuthServerConfig.java 继承 AuthorizationServer实现自定义配置

我们需要新建一个认证服务器配置类   OAuth2AuthServerConfig继承 AuthorizationServerConfigurerAdapter ,AuthorizationServerConfigurerAdapter 是认证服务器适配器,我们看一下的源码:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

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

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }

}

里面有三个方法,这三个方法,正对应上图中箭头所指的三个问题,我们需要重写这三个方法,实现自己的配置。

(1)配置Client信息    

  从图中可以看出,认证服务器要配置两个Client,一个是【客户端应用】,他需要来认证服务器申请令牌,一个是 【订单服务】,他要来认证服务器验令牌

       重写AuthorizationServerConfigurerAdapter 的   configure(ClientDetailsServiceConfigurer clients) throws Exception 方法      

package com.imooc.security.server.auth;

import jdk.nashorn.internal.ir.annotations.Reference;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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;

/**
 * @ClassName OAuth2AuthServerConfig
 * @Description TODO 认证服务器
 * @Author wushaopei
 * @Date 2021/5/2 21:44
 * @Version 1.0
 */
@Configuration  //这是一个配置类
@EnableAuthorizationServer  // 当前应用是一个认证服务器
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter  {
    //Spring 对密码加密的封装,自己配置下
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    /***
     * @Description 配置客户端应用的信息,让认证服务器知道有哪些客户端应用来申请令牌。
     * @param clients 客户端的详情服务的配置
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()  //添加客户端应用,配置在内存里,后面修改为数据库里
                .withClient("orderApp")// 指定client的id,应用的用户名,这里添加的是客户端应用
                .secret(passwordEncoder.encode("123456")) // 应用的密码
                .scopes("read", "write") // 应用的权限
                .accessTokenValiditySeconds(3600) // 令牌的有效期,单位为秒s
                .resourceIds("order-server")  // 资源服务器的id。指:我发给orderApp的token可以访问哪些资源服务器
                .authorizedGrantTypes("password") // 授权方式,指可以用哪种方式去实现
                .and()
                .withClient("orderService")// 指定client的id,应用的用户名,这里添加的是订单服务。微服务中,订单服务应该具备访问其他服务的权利,同样需要获取令牌
                .secret(passwordEncoder.encode("12345")) // 应用的密码
                .scopes("read") // 应用的权限
                .accessTokenValiditySeconds(3600) // 令牌的有效期,单位为秒s
                .resourceIds("order-server")  // 资源服务器的id。指:我发给orderApp的token可以访问哪些资源服务器
                .authorizedGrantTypes("password"); // 授权方式,指可以用哪种方式去实现
    }
}

 (2)配置用户 信息

 告诉认证服务器,有哪些用户可以来访问认证服务器

    重写AuthorizationServerConfigurerAdapter 的   configure(AuthorizationServerEndpointsConfigurer endpoints)   方法

    /**
     * @Description TODO 配置用戶信息
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 传给他一个authenticationManager用来校验传过来的用户信息是不是合法的,注进来一个,自己实现
        endpoints.authenticationManager(authenticationManager);
    }


    /**
     * @Description TODO 配置资源服务器过来验token的规则
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        /**
         * 过来验令牌有效性的请求,不是谁都能验的,必须要是经过身份认证的。
         * 所谓身份认证就是,必须携带clientId,clientSecret,否则随便一请求过来验token是不验的
         */
        security.checkTokenAccess("isAuthenticated()");
    }

上边的 加密解密类 PasswordEncoder  和  配置用户信息的 AuthenticationManager 还没有实例的来源,下边配置这俩类。

(3)新建配置类 OAuth2WebSecurityConfig 继承 WebSecurityConfigurerAdapter

package com.imooc.security.server.auth;

import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @ClassName OAuth2WebSecurityConfig
 * @Description TODO 用于声明客户端用户的信息
 * @Author wushaopei
 * @Date 2021/5/3 10:29
 * @Version 1.0
 */
@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    public PasswordEncoder passwordEncoder;
    /**
     * @Description TODO AuthenticationManagerBuilder 是用来构建  AuthenticationManager(处理登录操作)的
     *              TODO 需要两个东西:userDetailsService  、passwordEncoder
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    /**
     * @Description TODO  把AuthenticationManager暴露为bean
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

 passwordEncoder 本应该配置在上述类里,但是配置后报错循环依赖,暂时将其写在启动类里,不报错

(4)UserDetailsService的实现

分析:UserDetailsService接口,只有一个方法,返回UserDetails 接口:

public interface UserDetailsService {
   
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername,这里不用比对密码,比对密码是在AuthenticationManager里做的。UserDetails接口如下,提供了一些见名知意的方法,我们需要自定义自己的UserDetails实现类,比如你的User类实现这个接口 :
public interface UserDetails extends Serializable {
    // ~ Methods
    // ========================================================================================================

    Collection<? extends GrantedAuthority> getAuthorities();
   
    String getPassword();
 
    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

自定义UserDetailsService 实现类:

/**
 * @ClassName UserDetailsServiceImpl
 * @Description TODO
 * @Author wushaopei
 * @Date 2021/5/3 10:44
 * @Version 1.0
 */
@Component
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername(username)
                .password(passwordEncoder.encode("123456"))
                .authorities("ROLE_ADMIN")
                .build();
    }
}

AuthenticationManager 接口 也只有一个方法,实现类一般不需要自己实现。入参 是一个 Authentication 接口的实现,其中封装了认证的信息,不同的认证发的信息不一样,如用户名/密码 登录需要用户名密码,OAuth2则需要appId,appSecret,redirectURI等,不同的认证方式传过来的实现不同, authenticate 方法验证完了后将其中的信息更新调,返回。

public interface AuthenticationManager {
   
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

2.3 获取令牌

所有的准备工作都做好了,下面启动应用,来申请一个OAuth2令牌

postman请求http://localhost:9090/oauth/token ,HttpBasic传入客户端id和客户端密码。

用图片生动解释了 OAuth2 中的角色和Spring接口 AuthorizationServerConfigurerAdapter 三个方法的关系,方便记忆

实现了OAuth2的密码模式,来申请token

存在问题:passwordEncoder 如果放在了 OAuth2WebSecurityConfig配置类里面,就会报循环依赖错误,有待解决

以上是关于SpringCloud微服务安全网关安全 3-3 搭建OAuth2 认证服务器的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud微服务安全网关安全 3-1 概述

SpringCloud微服务安全网关安全 3-2 常见的微服务安全整体架构

重学SpringCloud系列八之微服务网关安全认证-JWT篇

SpringCloud微服务安全网关安全 3-5 重构代码以使用真实环境

SpringCloud微服务安全网关安全 3-8 用开源项目spring-cloud-zuul-ratelimit 做网关上的限流

SpringCloud微服务(05):Zuul组件,实现路由网关控制