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