看到我的文章,是你的幸运。 刚搭建完oauth2框架,使用密码模式。这篇文章是实战。 踩了不少坑,终于把oauth2的概念理解透,实战框架搭建起来,最下面是有github上的demo例子。 跟着我的教程,你能迅速搭建一个支持oauth2的spring-boot应用。
oauth2概念介绍
0Auth, 开放授权(open Authorization)是一种资源提供商用于授权第三方应用代表资源所有者获取有限访问权限的授权机制。整个授权期间,第三方应用都碰不到用户的密码,所以OAuth是安全的。
那么,它是怎么实现的呢?
先介绍oauth2的包含的角色:
- resource owner 资源所有者,通常指用户
- resource server 资源服务器。想要访问的东东。比如网页所在的服务器
- Client 客户端,第三方应用,用户直接访问的网站。比如csdn
- Authorization Server 授权服务器 在这里获取token。比如QQ的登录页面
举一个例子:
你(资源所有者)用浏览器,打开csdn首页(客户端);点击登录,选择qq方式登录;跳转到
qq登录页面(授权服务器);qq登录页面需要你输入短信验证码;输入完毕,授权完成,
跳转到csdn(客户端)的服务器指定url(原因是qq授权服务器和csdn签了合约,授权完成后回到csdn网站),此时授权服务器会把token和refresh_token都带给csdn(客户端);
csdn使用token,去qq的个人信息服务器(资源服务器),获取用户信息;
从而csdn就获取到用户信息,和自己的db库数据比较,就知道是否是注册用户。
上面的例子,是授权码模式。是最全面,最复杂的方式。为什么叫授权码模式,想想我们平时用到的短信验证码,微信公众号码。
总共有4种模式
有4种授权模式
- 授权码模式(例子是我们常见的短信验证码)
- 隐式授权模式 (较少使用,一般是客户端非常信任的情况下使用,比如移动qq客户端)
- 密码模式(客户端 比较信任,一般属于同一个公司的内网)
- 客户端授权模式(这种模式没有用户,非常信任客户端)
4种授权码模式介绍
这是大佬的博客。要仔细看,理解oauth2的4种模式,也就理解这个框架,到底干什么的?spring security做了什么事情。
接下来实战:
先下载我的github项目。oauthDemo
搭建一个spring-boot项目。官方链接
maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
oauth2配置类AuthorizationServerConfigurerAdapter
我的实现类是AuthorizationServerConfig
需要开启@EnableAuthorizationServer注解
这里配置开启验证,看注释吧
/**
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// super.configure(security);
// 开启该配置,才可以使用oauth2认证,否则模式默认是HTTP的基本认证
security.allowFormAuthenticationForClients();
// 这两个配置,目的是开启两个端点url,默认服务器是关闭的 /oauth/token_key /oauth/check_token
security.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()");
}
这里是配置token的参数。inMemory是一种简单的使用模式。大规模使用,肯定要研究一下源码。提供了复杂的使用。一般来说,框架是提供了关于jdbc相关的类,来做类似的事情。
参数解析自行百度关键字。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//client_id
.withClient("client-for-server")
.secret(passwordEncoder.encode("client"))
// .secret("client")
.authorizedGrantTypes("authorization_code", "implicit", "password", "refresh_token")
.accessTokenValiditySeconds(60 * 6 * 2)
.refreshTokenValiditySeconds(60 * 60 * 2)
// .redirectUris("http://localhost:8080/login/oauth2/code/authorizationserver")
.redirectUris("http://www.baidu.com")
.additionalInformation()
//这里有疑问,资源id
// .resourceIds(ResourceServerConfig.RESOURCE_ID)
// .authorities("ROLE_CLIENT")
.scopes("any");
// .autoApprove("profile");
}
这里是配置token的类型,密码模式
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore()).userDetailsService(userDetailsService);
endpoints.authenticationManager(authenticationManager).allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
// endpoints.tokenStore(memoryTokenStore());
endpoints.tokenStore(tokenStore());
endpoints.accessTokenConverter(accessTokenConverter());
// endpoints.userDetailsService(userDetailsService);
}
配置oauth2的链路Filter ResourceServerConfigurerAdapter
我的实现类是ResourceServerConfig
开启@EnableResourceServer
最终要是覆盖这个方法。requestMatchers().antMatchers("/resources/*")是
1,筛选/resources下的path
2,这个path下的anyRequest都要验证
提示:这是配置那些url要做oauth验证的,需要仔细理解。尤其是antMatchers和authorizeRequests间的前后关系。可以自行关键字搜索***
@Override
public void configure(HttpSecurity http) throws Exception {
logger.info("ResourceServerConfig中配置HttpSecurity对象执行");
//只有/me端点作为资源服务器的资源
// http.requestMatchers().antMatchers("/")
// .and()
// .authorizeRequests()
// .antMatchers("/")
// .permitAll()
// .anyRequest()
// .authenticated();
http.requestMatchers().antMatchers("/resources/*").and().authorizeRequests().anyRequest().authenticated();
}
一般spring Security web Filter配置 WebSecurityConfigurerAdapter
没有oauth2框架下,spring security是使用WebSecurityConfigurerAdapter来做filter配置的,我的实现类是SecurityConfiguration
需要开启注解@EnableWebSecurity
关键配置,是过滤那些url需要验证。模式是需要HTTP BASIC认证
@Override
protected void configure(HttpSecurity http) throws Exception {
logger.info("SecurityConfiguration中配置HttpSecurity对象执行");
// http.authorizeRequests().antMatchers("/admin").permitAll().anyRequest().hasAnyRole("USER", "ADMIN").and().formLogin();;
http.authorizeRequests().antMatchers("/").permitAll().anyRequest().hasAnyRole("USER", "ADMIN")
.and().formLogin();
}
关键配置,是配置用户的账号密码。看到inMemory,就知道它仅仅存在于内存中。所以实际生产,是需要使用jdbc相关的接口的,获取真实的数据
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService());
auth.inMemoryAuthentication().withUser("user").password(passwordEncoder().encode("user")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN");
}
介绍antMatchers()和authorizeRequests().antMatchers()区别
首先要介绍ResourceServerConfigurerAdapter和WebSecurityConfigurerAdapter间的区别
区别介绍
这篇文章,其中介绍了antMatchers()和authorizeRequests().antMatchers()的区别
下面是一个例子:
http.requestMatchers().antMatchers("/resources/*").and().authorizeRequests().antMatchers("/resources/phone").permitAll().anyRequest().authenticated();
第一步:筛选url是否前缀是/resources,不是则代表oauth2无需处理这类请求;是则下一步
第二部:查看path是否/resources/phone,是则无需授权认证,直接通过;不是则下一步
第三步:其他任何请求anyRequest,都要授权认证
第一步解析,假如不归oauth2认证,那么请求就会跑到WebSecurityConfigurerAdapter上认证。
这样就实现了既有一部分是oauth2认证;剩下的,是session认证。
至此,oauth2的配置已经完成。
假如只需要oauth2框架,其实SecurityConfiguration是不需要配置的,因为它默认是用session的会话认证。
而oauth2框架的目的是使用token认证。token认证(使用jwt)是服务端无状态的认证;而session认证是有状态认证。对于一个固定的url请求,spring框架里面,授权认证两者只能2选1。
好,那为什么要配置SecurityConfiguration,原因就在这里。
很多时候,一个服务器,一部分请求要走oauth2认证,一部分要走session认证。那么这里就凸显出链路过滤的配置。请看configure(HttpSecurity http),理解透它。
至此,oauth2已经配置完毕。
实践oauth2授权认证
端点(endPoint),在源码里面,可以经常看到这个单词。可以理解为授权场景的url吧。
ResourceServerConfigurerAdapter 确定了需要oauth2认证的endPoint。
这是我的demo例子
ResourceController
@RestController
@RequestMapping("/resources")
public class ResourceController {
private static final Logger logger = LoggerFactory.getLogger(ResourceController.class);
/**
* 需要认证
*
* @param access_token
* @param principal
* @return
*/
@RequestMapping("/me")
public Principal me(String access_token, Principal principal) {
logger.info(principal.toString());
logger.info(access_token);
return principal;
}
/**
* antMatchers("/resources/phone").permitAll()
* 无需认证
*
* @return
*/
@RequestMapping("/phone")
public String phone() {
return "phone: 1234567890";
}
}
调用/resources/phone 直接通过。
没有token直接调用,会返回401错误。/resources/me
oauth2获取token的方式
spring security oauth2 中的 endpoint
- /oauth/authorize(授权端,授权码模式使用)
- /oauth/token(令牌端,获取 token)
- /oauth/check_token(资源服务器用来校验token)
- /oauth/confirm_access(用户发送确认授权)
- /oauth/error(认证失败)
- /oauth/token_key(如果使用JWT,可以获的公钥用于 token 的验签)
本文介绍密码模式,只需要用到/oauth/token和/oauth/check_token
密码模式获取token
参数介绍,直接看源码
org.springframework.security.oauth2.provider.OAuth2Request
public class OAuth2Request extends BaseRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Resolved resource IDs. This set may change during request processing.
*/
private Set<String> resourceIds = new HashSet<String>();
/**
* Resolved granted authorities for this request. May change during request processing.
*/
private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
/**
* Whether the request has been approved by the end user (or other process). This will be altered by the User
* Approval Endpoint and/or the UserApprovalHandler as appropriate.
*/
private boolean approved = false;
/**
* Will be non-null if the request is for a token to be refreshed (the original grant type might still be available
* via {@link #getGrantType()}).
*/
private TokenRequest refresh = null;
/**
* The resolved redirect URI of this request. A URI may be present in the original request, in the
* authorizationParameters, or it may not be provided, in which case it will be defaulted (by processing classes) to
* the Client's default registered value.
*/
private String redirectUri;
/**
* Resolved requested response types initialized (by the OAuth2RequestFactory) with the response types originally
* requested.
*/
private Set<String> responseTypes = new HashSet<String>();
/**
* Extension point for custom processing classes which may wish to store additional information about the OAuth2
* request. Since this class is serializable, all members of this map must also be serializable.
*/
private Map<String, Serializable> extensions = new HashMap<String, Serializable>();
验证token有效性
token过期后提示
业务请求带上token访问
至此。已经试验完毕。
补充使用jwt
JSON Web Token - 在Web应用间安全地传递信息
理解JWT的使用场景和优劣
我的demo使用jwt的方式:
// 使用最基本的InMemoryTokenStore生成token
@Bean
public TokenStore memoryTokenStore() {
return new InMemoryTokenStore();
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* 指定使用token的类型,默认的Opaque格式和jwt格式
* @return
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//这里使用堆成加密,123456789是work key
converter.setSigningKey("123456789");
// KeyStoreKeyFactory keyStoreKeyFactory =
// new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
// converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
// converter.setAccessTokenConverter(new CustomerAccessTokenConverter());
return converter;
}
后记
这个demo,resource server 资源服务器和Authorization Server 授权服务器是部署在一起的。可以搭建两个spring-boot,拆分功能。
我的GitHub下载:hjq2016 oauth2Demo