在进行Spring Security oauth2快速入门之前,我们先来了解一些基本的概念
OAuth是一种开放协议, 允许用户让第三方应用以安全且标准的方式获取该用户在某一网站,移动或者桌面应用上存储的秘密的资源(如用户个人信息,照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth协议(RFC5849)作为一个指导性文档发布,是一个小社区的工作成果。
OAuth2.0在OAuth的部署经验之上构建,也包括其他使用案例以及从更广泛的IETF社区收集到的可扩展性需求。
OAuth 2.0协议不向后兼容OAuth 1.0。这两个版本可以在网络上共存,实现者可以选择同时支持他们。
OAuth2.0在安全性方面做了比较大的提高,简单来说OAuth2.0就是一种授权协议,可以用来授权,随意点个网站,如下图这些网站,用户不想注册,就可以用微信、支付宝登录,就是场景是很常见的,也是OAuth2.0的应用。
OAuth2.0是一种授权机制,正常情况,不使用OAuth2.0等授权机制的系统,客户端是可以直接访问资源服务器的资源的,为了用户安全访问数据,在访问中间添加了Access Token机制。客户端需要携带Access Token去访问受到保护的资源。所以OAuth2.0确保了资源不被恶意客户端访问,从而提高了系统的安全性。
a.授权服务器(AuthorizationServer): 在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器,使得授权客户端应用能够访问资源拥有者所拥有的资源。
b.资源服务器(ResourceServer):托管受保护资源的服务器,能够接收和响应使用访问令牌对受保护资源的请求。需要注意的是授权服务器和资源服务器可以是同一个服务器,也可以不是同一个服务器。
c.资源所有者(ResourceOwner):指拥有共享数据的人或应用。比如微信的用户或者淘宝的用户就是是资源拥有者,他们拥有的资源就是他们的数据。需要注意的是资源拥有者也可以是一个应用,不一定必须是人。
d.客户端应用(client):指请求访问存储在资源服务器的资源的应用。
如,你公司的OA系统需要使用微信进行授权登录,那么在这个过程中这四个角色分别对应
授权服务器:微信授权下发token
客户端应用:OA系统
资源服务器:微信给OA系统返回用户信息
资源所有者:微信用户
当OA系统要进行登录的时候,微信进行认证授权,微信下发token信息后,用户在OA系统中需要查看该微信用户的信息,因此OA系统携带token信息向微信发送请求,然后微信返回信息给OA系统。
client_id:客户端应用的ID
client_secret:客户端秘钥
response_type:响应类型
grant_type:授权模式
scope:授权范围
redirect_uri:回调地址
参数:client_id、client_secret、response_type、grant_type、redirect_uri
http://<认证服务地址>?client_id=client_hutao&client_secret=secret_hutao&response_type=code&redirect_uri=http://<客户端(web)地址>
http://<认证服务地址>??code=504dbO&grant_type=authorization_code&client_id=client_hutao&client_secret=secret_hutao
调用上述接口后,我们就会获取到token信息。
http://<资源服务器地址>??token=XXXXXXXXXXXXXXXXXXXXXXXX
参数:client_id、response_type、redirect_uri
和授权码模式不同的是,用户进行登录操作后,直接携带token信息重定向客户端应用页面。
不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此称简化模式。response_type的值为token说明该接口直接返回的信息就是token。
http://<认证服务地址>?client_id=client_hutao&client_secret=secret_hutao&response_type=token&redirect_uri=http://<客户端(web)地址>
和授权码以及简化模式最大区别是,不需要重定向页面,直接通过接口返回token信息,并且在这个过程中,不需要用户参与。response_type为
参数:client_id、client_secret、response_type
和客户端模式一样,直接通过接口返回token信息,但是需要在接口中传用户名和密码。
参数:client_id、client_secret、username、password
如果你使用idea或者sts等工具,可以创建SpringBoot项目的时候选择相关依赖或者版本,则不用在自行添加依赖。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.10.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>usermanager_serviceartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>usermanager_servicename>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR10spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<includeSystemScope>trueincludeSystemScope>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
plugins>
build>
project>
接下来我们将按照四个角色进行开始搭建并且演示。
a.授权服务器(AuthorizationServer): 在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器,使得授权客户端应用能够访问资源拥有者所拥有的资源。
b.资源服务器(ResourceServer):托管受保护资源的服务器,能够接收和响应使用访问令牌对受保护资源的请求。需要注意的是授权服务器和资源服务器可以是同一个服务器,也可以不是同一个服务器。
c.资源所有者(ResourceOwner):指拥有共享数据的人或应用。比如微信的用户或者淘宝的用户就是是资源拥有者,他们拥有的资源就是他们的数据。需要注意的是资源拥有者也可以是一个应用,不一定必须是人。
d.客户端应用(client):指请求访问存储在资源服务器的资源的应用。
定义授权服务器,用注解@EnableAuthorizationServer
;
在这里我们暂时使用java对象的方式构建了一个客户端信息。注意,后续这块数据应该从数据库读取
@Data
public class ClientInfo {
/**客户端ID*/
private String clientId = "client_hutao";
/**客户端秘钥*/
private String clientSecret = "secret_hutao";
/**授权范围*/
private String scop = "all";
/**token有效期*/
private int tokenValid = 60*30*4;
/**flush_token有效期*/
private int flushTokenValid = 60*30*4;
/**授权模式:授权码模式,简化模式,密码模式,客户端模式*/
private String [] grantTypes= {"authorization_code","implicit","password","client_credentials"};
}
/**
* @Description:认证服务器配置
* 在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器,使得授权客户端应用能够访问资源拥有者所拥有的资源
* @author:hutao
* @mail:[email protected]
* @date:2021年4月21日
*/
@SpringBootConfiguration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
@Autowired
private AuthenticationManager authenticationManager;
/**
* @Description:客户端配置:这里我们先使用内存的方式构造死数据进行入门,后续我们需要从数据库中读取客户端信息
* @author:hutao
* @mail:[email protected]
* @date:2021年4月21日
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
//使用我们构建的客户端信息,将其写入到内存中
ClientInfo clientInfo = new ClientInfo();
clients
// 使用内存存储
.inMemory()
//标记客户端id
.withClient(clientInfo.getClientId())
//客户端安全码 需要注意这里存储的数据是通过BCryptPasswordEncoder进行加密后的数据
.secret("{noop}"+clientInfo.getClientSecret())
//为true 直接自动授权成功返回code
.autoApprove(true)
.redirectUris("https://www.baidu.com/") //重定向uri
//允许授权范围
.scopes(clientInfo.getScop())
//token 时间秒
.accessTokenValiditySeconds(clientInfo.getTokenValid())
//刷新token 时间 秒
.refreshTokenValiditySeconds(clientInfo.getFlushTokenValid())
//允许授权类型
.authorizedGrantTypes(clientInfo.getGrantTypes());
}
/**
* @Description:配置令牌访问端点:定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的token,这里目前我们简单意见,使用内存的方式
* @author hutao
* @date 2021年4月21日
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//
endpoints
//密码模式需要配置注入的authenticationManager,后续深入在解释为什么需要注入这个
.authenticationManager(authenticationManager)
.tokenStore(new InMemoryTokenStore());
}
/**
* @Description:.令牌端点的安全约束:endpoint可以定义一些安全上的约束等
* @author:hutao
* @mail:[email protected]
* @date:2021年4月25日
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{
security
//开启/oauth/token_key验证端口认证权限访问
.tokenKeyAccess("isAuthenticated()")
//开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()")
//允许表单认证
.allowFormAuthenticationForClients();
}
资源所有者就是用户,因此我们同样用java对象的方式构建一个用户信息。注意后续应该从数据库读取。
@Data
public class UserInfo {
/**登录用户名*/
private String userName = "hutao";
/**登录密码*/
private String passWord = "123456";
/**用户权限*/
private String role = "admin";
}
@SpringBootConfiguration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* @Description:认证服务器中注入的authenticationManager就是从这里来的
* @author:hutao
* @mail:[email protected]
* @date:2021年4月30日
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
UserInfo userInfo = new UserInfo();
auth.inMemoryAuthentication()
.withUser(userInfo.getUserName())
.password("{noop}"+userInfo.getPassWord())
.roles(userInfo.getRole());
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
}
/**
* @Description:配置不拦截"/oauth/**",否则在申请access_token会要求先认证
* @author:hutao
* @mail:[email protected]
* @date:2021年4月30日
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页并允许访问
.formLogin().permitAll()
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
.and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**").permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 关闭跨域保护;
.and().csrf().disable();
}
}
public class ClientInfo {
/**客户端ID*/
private String clientId = "client_hutao";
/**客户端秘钥*/
private String clientSecret = "secret_hutao";
/**授权范围*/
private String scop = "all";
/**为true 直接自动授权成功返回code*/
private boolean autoApprove = true;
/**重定向uri*/
private String redirectUris = "https://www.baidu.com/";
/**token有效期*/
private int tokenValid = 60*30*4;
/**flush_token有效期*/
private int flushTokenValid = 60*30*4;
/**授权模式:授权码模式,简化模式,密码模式,客户端模式*/
private String [] grantTypes= {"authorization_code","implicit","password","client_credentials"};
}
public class UserInfo {
/**登录用户名*/
private String userName = "hutao";
/**登录密码*/
private String passWord = "123456";
/**用户权限*/
private String role = "admin";
}
注意:由于我们这里没有使用数据库,可能某些版本会要求配置sql,因此这里我们设置不需要进行sql配置,在启动类里面配置
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
步骤1:获取授权码code
http://127.0.0.1:8080/oauth/authorize?client_id=client_hutao&client_secret=secret_hutao&response_type=code
请求示例:通过浏览器访上述地址,会被重定向到一个登录页面,输入用户名密码(hutao/123456),会携带code码回调到www.baidu.com页面。
步骤2:授权码code换取token
现在入门阶段,我们暂时使用postman来进行调试
设置基本认证信息(Basic Auth)
username填clientId ,password填写clientSecret
设置Params参数
http://127.0.0.1:8080/oauth/token?code=j51uBH&grant_type=authorization_code
除了上述这种,在认证参数中,username和password里面,client_id和client_secret,也可以像下面这种方式,直接当作请求参数传递也可以。
TIP:多次测试,记得清楚浏览器缓存。
简化模式与授权码模式一样,直接使用浏览器访问,会被重定向到登录页面,需要输入用户名密码,但是省去了用code换token,这种方式会直接将token直接返回。
请求示例
http://127.0.0.1:8080/oauth/authorize?client_id=client_hutao&response_type=token&scope=all
请求示例
http://127.0.0.1:8080/oauth/token?client_id=client_hutao&client_secret=secret_hutao&grant_type=client_credentials
http://127.0.0.1:8080/oauth/token?client_id=client_hutao&client_secret=secret_hutao&username=hutao&password=123456&grant_type=password