SpringCloud(七):搭建OAuth2认证授权服务

一、概念部分

1.为什么需要做一个单独的认证授权服务?

为了保证服务对外的安全性,往往都会在服务接口采用权限校验机制,为了防止客户端在发起请求中途被篡改数据等安全方面的考虑,还会有一些签名校验的机制。
在分布式微服务架构的系统中,我们把原本复杂的系统业务拆分成了若干个独立的微服务应用,我们不得不在每个微服务中都实现这样一套校验逻辑,这样就会有很多的代码和功能冗余,随着服务的扩大和业务需求的复杂度不断变化,修改校验逻辑变得相当麻烦,一处改,处处改。所以我们需要把认证授权服务单独出来,做成一个服务进行调用。

2.授权服务的使用场景有哪些?

授权服务并不是每个应用的接口直接去调用,判断哪些用户有权限访问接口。 而是通过API网关进行统一调用。用户所有的请求都必须先通过API网关,API网关在进行路由转发之前对该请求进行前置校验,我们可以方便的使用OAuth2认证授权服务来做单点登录等操作。
可以使用OAuth2来实现对多个服务的统一认证授权。

3.本项目的主要操作流程

关于OAuth2的协议此处不再介绍,本项目演示主要的认证和授权步骤,简单来说就是客户端根据约定的ClientID、ClientSecret、Scope来从Access Token URL地址获取AccessToken,并经过AuthURL认证,用得到的AccessToken来访问其他资源接口。

项目结构如下,其中核心部分为core包下的config包中的三个配置类
从上往下分别是是授权服务配置,资源服务配置和安全配置


二、代码示例

1.新建一个SpringBoot项目 auth-server,在项目中添加依赖
build.gradle

dependencies {
   compile('org.springframework.cloud:spring-cloud-starter-oauth2')
   compile('org.springframework.cloud:spring-cloud-starter-security')
   compile('org.springframework.boot:spring-boot-starter-data-mongodb')
   compile('org.springframework.boot:spring-boot-starter-data-redis')
   compile('org.springframework.cloud:spring-cloud-starter-eureka')
   testCompile('org.springframework.boot:spring-boot-starter-test')
}

2.新建配置文件 application.yml

#服务器配置
server:
  #端口
  port: 8020

#服务器发现注册配置
eureka:
  client:
    serviceUrl:
      #配置服务中心(可配置多个,用逗号隔开)
      defaultZone: https://www.apiboot.cn/eureka

#spring配置
spring:
  #应用配置
  application:
    #名称: OAuth2认证授权服务
    name: auth-server
  #数据库配置
  data:
    mongodb:
      port: 27017
      database: auth_server

#安全配置
security:
  #oauth2配置
  oauth2:
    resource:
      filter-order: 3

3.应用启动类添加注解

/**
* OAuth2认证授权服务
* @ EnableDiscoveryClient 启用服务注册发现
*/
@SpringBootApplication
@EnableDiscoveryClient
public class AuthServerApplication {

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

}

4.新建账户实体类 Account.java

/**
* 账户实体类
*/
public class Account {
    @Id
    private String id;          // 主键
    private String userName;    // 用户名
    private String passWord;    // 密码
    private String[] roles;     // 角色

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String[] getRoles() {
        return roles;
    }

    public void setRoles(String[] roles) {
        this.roles = roles;
    }
}

5.新建账户表数据操作类,MongoDB操作接口 AccountRepository.java

/**
* 账户数据库操作类
* MongoDB操作接口
*/
@Component
public interface AccountRepository extends MongoRepository<Account, String> {

    /**
     * 根据用户名查找账户信息
     * @param username 用户名
     * @return 账户信息
     */
    Account findByUserName(String username);
}

6.新建用户信息控制器 UserController.java

/**
*  用户信息控制器
*/
@RestController
public class UserController {

    @Autowired
    private AccountRepository accountRepository;    // 账户数据操作

    /**
     * 初始化用户数据
     */
    @Autowired
    public void init(){

        // 为了方便测试,这里添加了两个不同角色的账户
        accountRepository.deleteAll();

        Account accountA = new Account();
        accountA.setUserName("admin");
        accountA.setPassWord("admin");
        accountA.setRoles(new String[]{"ROLE_ADMIN","ROLE_USER"});
        accountRepository.save(accountA);

        Account accountB = new Account();
        accountB.setUserName("guest");
        accountB.setPassWord("pass123");
        accountB.setRoles(new String[]{"ROLE_GUEST"});
        accountRepository.save(accountB);
    }

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

7.新建用户信息服务类,实现 Spring Security的UserDetailsService接口方法,用于身份认证 DomainUserDetailsService.java

/**
* 用户信息服务
* 实现 Spring Security的UserDetailsService接口方法,用于身份认证
*/
@Service
public class DomainUserDetailsService implements UserDetailsService {

    @Autowired
    private AccountRepository accountRepository;    // 账户数据操作接口

    /**
     * 根据用户名查找账户信息并返回用户信息实体
     * @param username 用户名
     * @return 用于身份认证的 UserDetails 用户信息实体
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUserName(username);
        if (account!=null){
            return new User(account.getUserName(),account.getPassWord(), AuthorityUtils.createAuthorityList(account.getRoles()));
        }else {
            throw  new UsernameNotFoundException("用户["+username+"]不存在");
        }
    }
}

8.新建授权服务配置类,继承AuthorizationServerConfigurerAdapter
AuthorizationServerConfig.java

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

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

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;  // redis连接工厂

    /**
     * 令牌存储
     * @return redis令牌存储对象
     */
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(this.authenticationManager);
        endpoints.tokenStore(tokenStore());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("android")
                .scopes("xx")
                .secret("android")
                .authorizedGrantTypes("password", "authorization_code", "refresh_token")
                .and()
                .withClient("webapp")
                .scopes("xx")
                .authorizedGrantTypes("implicit");
    }

}

9.新建资源服务配置类,继承ResourceServerConfigurerAdapter
ResourceServerConfig.java

/**
* 资源服务配置
* @ EnableResourceServer 启用资源服务
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatcher(new OAuth2RequestedMatcher())
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }

    /**
     * 定义OAuth2请求匹配器
     */
    private static class OAuth2RequestedMatcher implements RequestMatcher {
        @Override
        public boolean matches(HttpServletRequest request) {
            String auth = request.getHeader("Authorization");
            //判断来源请求是否包含oauth2授权信息,这里授权信息来源可能是头部的Authorization值以Bearer开头,或者是请求参数中包含access_token参数,满足其中一个则匹配成功
            boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
            boolean haveAccessToken = request.getParameter("access_token")!=null;
            return haveOauth2Token || haveAccessToken;
        }
    }
   
}

10.新建安全配置类,继承WebSecurityConfigurerAdapter
SecurityConfig.java

/**
* 安全配置
* @ EnableWebSecurity 启用web安全配置
* @ EnableGlobalMethodSecurity 启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注入用户信息服务
     * @return 用户信息服务对象
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return new DomainUserDetailsService();
    }

    /**
     * 全局用户信息
     * @param auth 认证管理
     * @throws Exception 用户认证异常信息
     */
    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
    }

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

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

}

三、演示

假设我们现在要直接访问 /user接口,我们打开PoatMan直接请求该接口



发现返回了401状态码,并且报了Unauthorized 未经授权的错误,提示信息显示 访问此资源需要完全身份验证。 添加了Spring Security+OAuth2后,所有的资源访问都需要通过token

1.获取access_token

启动服务,打开PostMan切换到Authorization页卡,Type类型选择Basic Auth,Username和Password填写授权服务配置中对应的withClient和secret的值,这里都写android

点击Update Request,切换到Headers页卡,发现请求头里多了个Authorization参数,参数值就是根据Authorization页卡填写的授权信息生成的,要获取token必须有该参数值


使用post方法访问授权服务的 /oauth/token地址,post参数需要填写grant_type、username、password。点击send请求,将会返回如下access_token信息。



拿到access_token后,就可以在请求其他资源接口的时候携带上该token参数值获取该角色可获取的资源,打开浏览器访问 /user接口并携带上 access_token参数值

2.使用正确的姿势获取access_token,并根据access_token获取资源

切换到Authorization页卡,选择OAuth2.0,点击Get New Access Token


TokenName可以随意填写,其他信息根据实际情况填写。点击Request Token后,将会跳出输入用户名和密码的页面(这个操作其实就是根据用户名和密码登录并获取AccessToken)



登录成功后,我们看到左侧有个我们刚刚新建的auth,点击auth,右侧会显示该请求获取到的AccessToken信息。点击UseToken


点击Use Token后,发现请求头Headers页卡里添加了Authorization的参数值



点击Send,请求/user接口,正常返回用户授权信息


到此 OAuth2认证授权服务已经搭建完毕了,关于api-gateway和OAuth2认证授权服务的整合调用,会在下一篇文章中写到,敬请期待。
项目地址:https://github.com/lanshiqin/cloud-project
欢迎点赞

推荐阅读更多精彩内容