返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - 使用Spring Security实现OAuth2授权认证教程(实现token认证)

作者:hangge | 2020-01-08 08:10

一、OAuth 2 介绍

1,什么是 OAuth 2?


2,OAuth 2 角色

OAuth 2 标准中定义了以下几种角色:
注意:一般来说,授权服务器和资源服务器可以是同一台服务器。

3,OAuth 2 授权流程

下面是 OAuth 2 一个大致的授权流程图:
  • 步骤1:客户端(第三方应用)向用户请求授权。
  • 步骤2:用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
  • 步骤3:客户端拿着授权许可凭证去授权服务器申请令牌。
  • 步骤4:授权服务器验证信息无误后,发放令牌给客户端。
  • 步骤5:客户端拿着令牌去资源服务器访问资源。
  • 步骤6:资源服务器验证令牌无误后开放资源。

4,OAuth 2 授权模式

OAuth 协议的授权模式共分为 4 种,分别说明如下:

二、使用样例

    由于本样例演示的是如何在前后端分离应用(或者为移动端、微信小程序等)提供的认证服务器中如何搭建 OAuth 服务,因此主要介绍密码模式。

1,添加依赖

    由于 Spring Boot 中的 OAuth 协议是在 Spring Security 基础上完成的。因此首先编辑 pom.xml,添加 Spring Security 以及 OAuth 依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>

2,配置授权服务器

    授权服务器和资源服务器可以是同一台服务器,也可以是不同服务器,本案例中假设是同一台服务器,通过不同的配置开启授权服务器和资源服务器。
    下面是授权服务器配置代码。创建一个自定义类继承自 AuthorizationServerConfigurerAdapter,完成对授权服务器的配置,然后通过 @EnableAuthorizationServer 注解开启授权服务器:
注意authorizedGrantTypes("password", "refresh_token") 表示 OAuth 2 中的授权模式为“password”和“refresh_token”两种。在标准的 OAuth 2 协议中,授权模式并不包括“refresh_token”,但是在 Spring Security 的实现中将其归为一种,因此如果需要实现 access_token 的刷新,就需要这样一种授权模式。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到内存中
    @Autowired(required = false)
    TokenStore inMemoryTokenStore;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
            .withClient("password")
            .authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
            .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
            .resourceIds("rid") //配置资源id
            .scopes("all")
            .secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

3,配置资源服务器

    接下来配置资源服务器。自定义类继承自 ResourceServerConfigurerAdapter,并添加 @EnableResourceServer 注解开启资源服务器配置。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {

        resources.resourceId("rid") // 配置资源id,这里的资源id和授权服务器中的资源id一致
                .stateless(true); // 设置这些资源仅基于令牌认证
    }

    // 配置 URL 访问权限
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .antMatchers("/user/**").hasRole("user")
                .anyRequest().authenticated();
    }
}

4,配置 Security

这里 Spring Security 的配置与传统的 Security 大体相同,不同在于:
注意:在这个 Spring Security 配置和上面的资源服务器配置中,都涉及到了 HttpSecurity。其中 Spring Security 中的配置优先级高于资源服务器中的配置,即请求地址先经过 Spring SecurityHttpSecurity,再经过资源服务器的 HttpSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {

        return super.userDetailsService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("admin")
                .and()
                .withUser("sang")
                .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
                .roles("user");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/oauth/**").authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .and().csrf().disable();
    }
}

5,添加测试接口

接着在 Conctoller 中添加如下三个接口用于测试,它们分别需要 admin 角色、use 角色以及登录后访问。
@RestController
public class HelloController {

    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "hello user";
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

6,开始测试

(1)启动项目,首先通过 POST 请求获取 token: 
  • 请求地址oauth/token 
  • 请求参数:用户名、密码、授权模式、客户端 idscope、以及客户端密码 
  • 返回结果access_token 表示获取其它资源是要用的令牌,refresh_token 用来刷新令牌,expires_in 表示 access_token 过期时间。

(2)当 access_token 过期后,可以使用 refresh_token 重新获取新的 access_token(前提是 access_token 未过期),这里也是 POST 请求:

(3)访问资源时,我们只需要携带上 access_token 参数即可:

(4)如果非法访问一个资源,比如 admin 用户访问“user/hello”接口,结果如下:

附:将令牌保存到 Redis 缓存服务器上

    在上面的样例中,令牌(Access Token)是存储在内存中,这种方式在单服务上可以体现出很好特效(即并发量不大,并且它在失败的时候不会进行备份)
    我们也可以将令牌保存到数据库或者 Redis 缓存服务器上。使用这中方式,可以在多个服务之间实现令牌共享。下面我通过样例演示如何将令牌存储在 Redis 缓存服务器上,同时 Redis 具有过期等功能,很适合令牌的存储。

1,增加依赖 

(1)首先编辑 pom.xml 文件,在前面的基础上增加 Redis 相关依赖:
<!-- spring security OAuth2 相关  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>

<!-- redis  -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

(2)接着在 application.properties 中配置 Redis 连接信息:
# 基本连接信息配置
spring.redis.database=0
spring.redis.host=192.168.60.133
spring.redis.port=6379
spring.redis.password=123
# 连接池信息配置
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0

2,修改授权服务器配置

最后我们修改授权服务器配置 AuthorizationServerConfig 中令牌保存相关代码,将其改成保存到 Redis 上即可。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // 该对象用来支持 password 模式
    @Autowired
    AuthenticationManager authenticationManager;

    // 该对象用来将令牌信息存储到Redis中
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    // 该对象将为刷新token提供支持
    @Autowired
    UserDetailsService userDetailsService;

    // 指定密码的加密方式
    @Bean
    PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
        return new BCryptPasswordEncoder();
    }

    // 配置 password 授权模式
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()
            .withClient("password")
            .authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
            .accessTokenValiditySeconds(1800) // 配置access_token的过期时间
            .resourceIds("rid") //配置资源id
            .scopes("all")
            .secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) //配置令牌存放在Redis中
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 表示支持 client_id 和 client_secret 做登录认证
        security.allowFormAuthenticationForClients();
    }
}

3,运行测试

启动项目,再次通过 /oauth/token 接口获取令牌。然后查看下 Redis 中的数据,可以发现令牌确实以及保存在 Redis 上了:
评论

全部评论(0)

回到顶部