SpringCloud Alibaba 微服务实战十三 - Oauth2.0 安全认证

本贴最后更新于 1721 天前,其中的信息可能已经时异事殊

导读:为了保证我们微服务的安全性,本章主要内容是使用 Oauth2.0 给我们微服务加上安全校验。

概念

为了保证服务的安全性,往往都会在接口调用时做权限校验。在分布式架构中我们会把复杂的业务拆成多个微服务,这样不得不在所有服务中都实现这样的权限校验逻辑,这样就会有很多代码和功能冗余。所以在微服务架构中一般会独立出一个单独的认证授权服务,供其他所有服务调用。

在 SpringCloud 体系中,我们只对网关层开放外网访问权限,其他后端微服务做网络隔离,所有外部请求必须要通过网关才能访问到后端服务。在网关层对请求进行转发时先校验用户权限,判断用户是否有权限访问。

我们一般使用 Oauth2.0 来实现对所有后端服务的统一认证授权。这期内容不讲 Oauth2.0 协议,只讲实现过程。如果大家对 Oauth2.0 不是很了解,可以翻看我之前的博客。

Oauth2 认证服务

建立认证服务 Auth-Service

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>com.alibaba.cloud</groupId>
		<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-oauth2</artifactId>
	</dependency>
	<!--database-->
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
	</dependency>
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-boot-starter</artifactId>
	</dependency>
	<dependency>
		<groupId>com.jianzh5.cloud</groupId>
		<artifactId>cloud-common</artifactId>
	</dependency>
</dependencies>

引入 mysql 主要是我们需要将 oauth2.0 的客户端信息以及 token 认证存入数据库。

建立相关数据表

主要是导入 oauth2 相关数据表以及用户表,在实际开发过程中用户权限这一套应该是基于 RBAC 进行设计,这里为了方便演示我就直接只做一个用户表。

image.png

(oauth2.0 相关表结构大家可以在网上找,如果找不到可以联系我)
image.png

给用户表添加数据:

INSERT INTO `user` VALUES ('1', '$2a$10$gExKdT3nkoFKfW1cFlqQUuFji3azHG.W4Pe3/WxHKANg3TpkSJRfW', 'zhangjian', 'ADMIN');

注意:在 spring-security 5.x 版本必须要注入密码实现器,我们使用了 BCryptPasswordEncoder 加密器,所以需要这里也需要对密码进行加密

添加 client 信息,使用 oauth_client_details 表:

INSERT INTO `oauth_client_details` VALUES ('app', 'app', '$2a$10$fG7ou8CNxDESVFLIM7LrneDmIpwbrxGM2W6.coGPddfQPyZxiqXE6', 'web', 'implicit,client_credentials,authorization_code,refresh_token,password', 'http://www.baidu.com', 'ROLE_USER', null, null, null, null);

同理也需要对 client_secret 字段进行加密

application.yml 配置文件

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: auth-service
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx:8848/
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://xx.xx.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.jdbc.Driver

server:
  port: 5000

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml

自定义认证服务器

只需要继承 AuthorizationServerConfigurerAdapter 并在开始处加上 @EnableAuthorizationServer 注解即可

/**
 * <p>
 * <code>AuthorizationServerConfig</code>
 * </p>
 * Description:
 * 授权/认证服务器配置
 * @author javadaily
 * @date 2020/2/26 16:26
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private UserDetailServiceImpl userDetailService;

    // 认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    /**
     * access_token存储器
     * 这里存储在数据库,大家可以结合自己的业务场景考虑将access_token存入数据库还是redis
     */
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    /**
     * 从数据库读取clientDetails相关配置
     * 有InMemoryClientDetailsService 和 JdbcClientDetailsService 两种方式选择
     */
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

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

    /**
     * 认证服务器Endpoints配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //如果需要使用refresh_token模式则需要注入userDetailService
        endpoints.userDetailsService(userDetailService);
        endpoints.authenticationManager(this.authenticationManager);
        endpoints.tokenStore(tokenStore());
    }

    /**
     * 认证服务器相关接口权限管理
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients() //如果使用表单认证则需要加上
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * client存储方式,此处使用jdbc存储
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }
}

自定义 web 安全配置类

/**
 * <p>
 * <code>WebSecurityConfig</code>
 * </p>
 * Description:
 * 自定义web安全配置类
 * @author javadaily
 * @date 2020/2/26 16:35
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailServiceImpl();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 认证管理
     * @return 认证管理对象
     * @throws Exception 认证异常信息
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());

    }

    /**
     * http安全配置
     * @param http http安全对象
     * @throws Exception http安全异常信息
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().cors()
                .and().csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/error",
                "/static/**",
                "/v2/api-docs/**",
                "/swagger-resources/**",
                "/webjars/**",
                "/favicon.ico"
            );
    }
}

自定义用户实现

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //获取本地用户
        User user = userMapper.selectByUserName(userName);
        if(user != null){
            //返回oauth2的用户
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    AuthorityUtils.createAuthorityList(user.getRole())) ;
        }else{
            throw  new UsernameNotFoundException("用户["+userName+"]不存在");
        }
    }
}

实现 UserDetailsService 接口并实现 loadUserByUsername 方法,这一部分大家根据自己的技术框架实现,Dao 层我就不贴出来了。

对外提供获取当前用户接口

@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    public UserMapper userMapper;


    @GetMapping("getByName")
    public User getByName(){
        return userMapper.selectByUserName("zhangjian");
    }

    /**
     * 获取授权的用户信息
     * @param principal 当前用户
     * @return 授权信息
     */
    @GetMapping("current/get")
    public Principal user(Principal principal){
        return principal;
    }
}

资源服务器

Oauth2.0 的认证服务器也资源服务器,我们在启动类上加入 @EnableResourceServer 注解即可

@SpringBootApplication
//对外开启暴露获取token的API接口
@EnableResourceServer
@EnableDiscoveryClient
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }
}

后端微服务改造

后端所有微服务都是资源服务器,所以我们需要对其进行改造,下面以 account-service 为例说明改造过程

  • 添加 oauth2.0 依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
  • 配置资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers(
         "/v2/api-docs/**",
                    "/swagger-resources/**",
                    "/swagger-ui.html",
                    "/webjars/**"
                    ).permitAll()
                .anyRequest().authenticated()
                .and()
                //统一自定义异常
                .exceptionHandling()
                .and()
                .csrf().disable();
    }
}
  • 修改配置文件,配置身份获取地址
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:5000/user/current/get
      id: account-service

测试

  • 我们直接访问 account-service 接口,会提示需要需要认证授权
    image.png

  • 通过密码模式从认证服务器获取 access_token
    image.png

  • 在请求头上带上 access_token 重新访问 account-service 接口,接口正常响应
    image.png

  • 通过 debug 模式发现每次访问后端服务时都会去认证资源器获取当前用户
    image.png

  • 输入错误的 access_token 进行访问,提示 access_token 失效
    image.png

总结

通过以上几步我们将后端服务加上了认证服务,必须要先进行认证才能正常访问后端服务。
整个实现过程还是比较复杂的,建议大家都实践一下,理解其中相关配置的作用,也方便更深入理解 Oauth2 协议。

SpringCloud Alibaba 系列文章

  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    944 引用 • 1459 回帖 • 17 关注
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖 • 1 关注
  • OAuth

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 9 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 19852860636

    已解决,spring.main.allow-bean-definition-overriding=true 必须加载在 bootstrap.yml 文件中

  • 其他回帖
  • 19852860636

    两个 passwordEncoder 会存在冲突,spring.main.allow-bean-definition-overriding=true 无法解决

  • Druid

    大佬能提供一下代码 Git 地址吗?

    1 回复
  • jianzh5
    作者

    抱歉,公司电脑无法放 git 哈