Spring Security使用OAuth2进行认证和授权开发

Spring Security使用OAuth2进行认证和授权开发

(一)Spring Security官网介绍

官网地址(https://spring.io/projects/spring-security)
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。

Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求。

特征
• 对身份验证和授权的全面且可扩展的支持
• 防御会话固定,点击劫持,跨站点请求伪造等攻击
• Servlet API集成
• 与Spring Web MVC的可选集成

了解OAuth 2.0

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0取代了在2006年创建的原始OAuth协议上所做的工作。OAuth2.0专注于简化客户端开发人员,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的授权流。该规范及其扩展正在IETF OAuth工作组内开发。

详细可以去官网地址(https://oauth.net/2/)
或者通过http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
这篇文章来了解

1.了解OAuth 2.0定义的四种授权方式

OAuth的作用是让"客户端"(一般指浏览器)安全可控地获取"用户"的授权,与"服务商提供商"进行互动。客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。

根据使用场景不同,分成了4种模式

 密码模式(以下简称password模式):认证的时候需要验证用户名和密码,适合已经拥有用户表的时候在登录时使用。
 客户端模式(以下简称client_credentials模式):是较常使用的模式,客户端在认证的时候只需要客户端id和密码就可以进行授权,获得访问资源的权限。适合在纯后端服务器之间安全的进行资源访问。
 授权码模式:因为使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
 简化模式:不常用到。

2.了解令牌刷新

如果用户访问的时候,客户端的"访问令牌"已经过期,则需要使用"更新令牌"申请一个新的访问令牌。

客户端发出更新令牌的HTTP请求,包含以下参数:
 granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
 refresh_token:表示早前收到的更新令牌,必选项。
 scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。

(三)开发

1.前言

使用oauth2保护应用,可以简单分为三个步骤
 配置资源服务器
 配置认证服务器
 配置spring security

前两点是oauth2的主体内容,而 spring security oauth2是建立在spring security基础之上的,所以有一些体系是公用的。

2.引入maven依赖

<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>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 将token存储在redis中 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

3.创建资源

@RestController
public class TestEndpoints {

    @GetMapping("/order")
    @ApiOperation(value = "受保护的接口")
    public String getOrder(@PathVariable String id) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "order id : " + id;
    }
    @GetMapping("/product")
    @ApiOperation(value = "没有保护的接口", hidden = true)
    public String getProduct(@PathVariable String id){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "product id : " + id;
    }
}

创建一个endpoint作为提供给外部的资源接口。通过认证和授权,客户端会获得能够访问受保护资源的授权(令牌)。而这是我们要访问的受保护接口。

4. 配置资源服务器

资源服务器和授权服务器是oauth2的核心配置。这里将客户端信息放到了内存中。正式开发应该存放到redis或者数据库里面。

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    private static final String DEMO_RESOURCE_ID = "order";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 配置资源id
        resources.resourceId(DEMO_RESOURCE_ID)
                // 令牌信息一般存储到redis,现在测试使用在内存中新建用户
                //.tokenStore(new RedisTokenStore(redisConnectionFactory))
                // 无状态对登录成功的用户不会创建Session
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //配置order访问控制,必须认证过后才可以访问
                .antMatchers("/order/**")
                .authenticated();
    }
}

5.配置授权服务器

token的存储一般选择使用redis,一是性能比较好,二是自动过期的机制,符合token的特性。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private UserDetailsService userService;

    private static final String DEMO_RESOURCE_ID = "order";

    @Value("${oauth2.client-id}")
    private String clientId;
    @Value("${oauth2.secret}")
    private String clientSecret;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置两个客户端,一个用于password认证一个用于client认证
        clients.inMemory().withClient("client_1")
                .resourceIds(DEMO_RESOURCE_ID)//客户端ID
                .authorizedGrantTypes("client_credentials", "refresh_token")
                .scopes("select")
                .authorities("client") //授权服务器,client_1和client_2都属于client
                .secret("123456")//设置客户端密码
                .and().withClient("client_2")
                .resourceIds(DEMO_RESOURCE_ID)
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("select")
                .authorities("client")
                .secret("123456");
        //使用bcrypt加密
        /*String finalSecret = "{bcrypt}"+passwordEncoder().encode(clientSecret);
        clients.inMemory()
                .withClient(clientId)//客户端ID
                .authorizedGrantTypes("password", "refresh_token","client_credentials")//设置验证方式
                .scopes("read", "write","trust")
                .secret(finalSecret)//设置客户端密码
                .authorities("oauth2")
                .accessTokenValiditySeconds(7200) //token过期时间
                .refreshTokenValiditySeconds(7200); //refresh过期时间*/
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //token信息保存到redis
        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
                //这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
                .authenticationManager(authenticationManager);
                // 不加报错"method_not_allowed"
                //.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                //一般会重写配置userService 这样每次认证的时候会去检验用户是否锁定,有效等.
                // 没有它,在使用refresh_token的时候会报错,现在使用内存中新建的userservice
                //.userDetailsService(userService);
    }

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

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        //允许表单认证
        oauthServer.allowFormAuthenticationForClients();
    }

}

6.配置spring security

对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑

在spring security的版本迭代中,产生了多种配置方式,建造者模式,适配器模式等等设计模式的使用,spring security内部的认证flow也是错综复杂。使用了springboot之后,spring security其实是有不少自动配置的,我们可以仅仅修改自己需要的那一部分,并且遵循一个原则,直接覆盖最需要的那一部分。

下面有2种方法替换内存中的用户

(1) 替换掉了容器中的UserDetailsService
(2) 替换AuthenticationManager

里面还自定义了资源白名单

@Configuration
@EnableWebSecurity
@Order(1000) //可以设置优先级
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    //配置在内存中的两个用户
    protected UserDetailsService userDetailsService() {
        //替换掉了容器中的UserDetailsService
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
        manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
        return manager;
    }

    @Override
    //配置在内存中的两个用户,效果跟上面一样
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //替换了AuthenticationManager效果
        auth.inMemoryAuthentication()
                .withUser("user_1")
                .password("123456")
                .authorities("USER")
                .and()
                .withUser("user_2")
                .password("123456")
                .authorities("USER");

    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        //不重写configure(AuthenticationManagerBuilder auth)注入内存中的用户会被新的管理覆盖
        return super.authenticationManagerBean();
    }
    
    //配置不需要权限验证的路径
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.requestMatchers().anyRequest()
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/*")
                .permitAll();
        // @formatter:on
    }
}

8.获取token

启动应用之后,我们可以使用http工具来访问/oauth/token来获得令牌
password模式:

http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=123456

响应如下:
{“access_token”:“950a7cc9-5a8a-42c9-a693-40e817b1a4b0”,“token_type”:“bearer”,“refresh_token”:“773a0fcd-6023-45f8-8848-e141296cb3cb”,“expires_in”:27036,“scope”:“select”}

client模式:

http://localhost:8080/oauth/token?grant_type=client_credentials&scope=select&client_id=client_1&client_secret=123456

响应如下:
{“access_token”:“56465b41-429d-436c-ad8d-613d476ff322”,“token_type”:“bearer”,“expires_in”:25074,“scope”:“select”}

在配置中,我们已经配置了对order资源的保护,如果直接访问:
http://localhost:8080/order/1
会得到这样的响应:
{“error”:“unauthorized”,“error_description”:“Full authentication is required to access this resource”}
(这样的错误响应可以通过重写配置来修改)
而对于未受保护的product资源
http://localhost:8080/product/1
则可以直接访问,得到响应
product id : 1

携带accessToken参数访问受保护的资源:
使用password模式获得的token:
http://localhost:8080/order/1?access_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0
得到了之前匿名访问无法获取的资源:
order id : 1

使用client模式获得的token:
http://localhost:8080/order/1?access_token=56465b41-429d-436c-ad8d-613d476ff322
同上的响应
order id : 1

发布了29 篇原创文章 · 获赞 0 · 访问量 391

猜你喜欢

转载自blog.csdn.net/qq_43399077/article/details/103096148