一、描述
单点登录(Single Sign-On ,SSO)可以直接通过一个统一的登录认证服务器实现系统中全部用户登录的操作控制,而基于SSO机制有多种不同的实现方案、在当今互联网应用中,OAuth2协议使用的更加广泛。
OAuth协议是一个关于用户资源授权访问的开放网络标准,具有较高的安全性和简易型,在全世界范围被广泛使用,目前最新版是2.0版。OAuth协议处理中不会使第三方触及用户账户信息,无需用户名和密码获取账户相关资源授权
在OAuth2之中存在有四个身份:
1、资源拥有者(Resource Owner):就是指要进行登录认证的用户(或者直接简称为“用户”),是登录数据资料(用户名&密码、手机号&短信验证码)的提供者
2、客户端(Client):也被称为第三方应用,用户最终要访问的应用资源;
3、认证(或授权)服务器(Authorization Server):用户提供认证服务,包括系统接入处理、用户登录表单、资源服务信息、以及客户端Token数据管理、这样客户端就可以通过Token获取用户授权的访问资源;
4、资源服务器(Resource Server):利用得到的Token获取相关用户资源数据;
二、搭建OAuth2
1、引入相关依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<springboot.version>2.4.2</springboot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
2、springSecurity在实现认证会提供一个完整的登录表单,但是OAuth2可不是那种RESTful结构返回的操作形式,它是需要的独立的WEB程序的,也就是说此时需要用户自己填写表单,修改SpringSecurity配置类
@Configuration
@Order(20)//让其执行顺序靠后
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired//用户认证业务接口
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);//配置用户认证服务
}
@Bean // 如果想使用密码,则必须配置有一个密码的编码器
public PasswordEncoder getPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();// 定义密码加密器
}
@Override
protected void configure(HttpSecurity http) throws Exception {//进行访问配置
http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
}
}
3、如果客户端要想使用OAuth2进行登录处理,则需要进行接入注册,那么这个信息就称为Client信息,而这个信息需要进行额外配置
需要注意的是“AuthorizationServerConfigurerAdapter”父类,会出现不推荐使用的信息,但是又可以发现这个依赖库还在同步保持更新,最重要的是,没有给出任何的替代品,SpringSecurity的意思,OAuth2的支持已经被单独处理了,我们不在直接提供OAuth2的处理,有其他开发者负责维护,但是已经不属于Spring原生支持
@Configuration
@EnableAuthorizationServer//启用授权服务
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override//客户端配置
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client_ran")// 注册的客户端id
.secret("123456")// 注册的密码
.authorizedGrantTypes("authorization_code")// 响应的类型
.redirectUris("127.0.0.1")// 返回的路径
.scopes("webapp");// 授权的范围
}
}
4、浏览器向OAuth2服务端发送一个单点登录请求;在路径上传入client_id(注册ID信息)、response_type(数据响应类型)、redirect_uri(生成authcode后的返回路径),这些信息都需要第三方客户端在注册时提供
admin:123456@localhost/oauth/authorize?client_id=client_ran&response_type=code&redirect_uri=127.0.0.1
5、执行结果:http://localhost/oauth/127.0.0.1?code=Dcphky
此时得到了一个授权码,有了这个授权码才可以进行后续的客户端认证
三、ClientDetailsService
1、在SpringSecurity之中UserDetailsService是实现用户认证与授权处理的,而现在一旦引入了OAuth2服务之后,就必须考虑使用ClientDetailsService来实现客户端信息的存储。
package org.springframework.security.oauth2.provider;
/** @deprecated */
@Deprecated //该注解就当放了个"PI",不用特别在意
public interface ClientDetailsService {
ClientDetails loadClientByClientId(String var1) throws ClientRegistrationException;
}
2、ClientDetails接口提供的方法
@Deprecated
public interface ClientDetails extends Serializable {
String getClientId();//获取客户端注册id
Set<String> getResourceIds();//获取客户端可以访问的资源,为空则忽略
boolean isSecretRequired();//验证此客户端是否需要密钥
String getClientSecret();//获取客户端密钥
boolean isScoped();//该客户端是否限定范围
Set<String> getScope();//获取客户端范围
Set<String> getAuthorizedGrantTypes();// 获取该客户端授权类型
Set<String> getRegisteredRedirectUri();// 获取注册的放回地址
Collection<GrantedAuthority> getAuthorities();// 获取该客户端对应的所有角色
Integer getAccessTokenValiditySeconds(); // Token有效时间(单位:s)默认12h
Integer getRefreshTokenValiditySeconds(); // Token刷新的有效时间,默认30天
boolean isAutoApprove(String var1);// 是否自动批准
Map<String, Object> getAdditionalInformation();// 获取一些客户端的附加信息
}
3、创建ClientDetailsService接口子类
@Service
public class ClientDetailsServiceImpl implements ClientDetailsService {
@Override
public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
BaseClientDetails clientDetails = new BaseClientDetails();//系统默认实现的子类
clientDetails.setClientId("client_ran");//可以随机生成
clientDetails.setClientSecret("123456");// 密码可以进行加密处理
clientDetails.setAuthorizedGrantTypes(Arrays.asList("authorization_code"));//授权类型
clientDetails.setScope(Arrays.asList("webapp"));
clientDetails.setAccessTokenValiditySeconds(30);
clientDetails.setAutoApproveScopes(clientDetails.getScope());//自动处理
Set<String> redirectSet = new HashSet<>();
redirectSet.addAll(Arrays.asList("http://127.0.0.1/message/show"));
clientDetails.setRegisteredRedirectUri(redirectSet); //如果没有配置正确的路径是无法authcode获取的的
return clientDetails;
}
}
4、修改AuthorizationServiceConfig配置类,通过ClientDetailsService实现客户端检测。
@Configuration
@EnableAuthorizationServer//启用授权服务
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
// @Qualifier(value = "ClientDetailsServiceImpl")//标注Bean名称
private ClientDetailsServiceImpl clientDetailsService;
@Override//客户端配置
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(this.clientDetailsService);
}
}
此时用户只要输入了正确的用户名和密码就可以直接返回指定路径上,同时放回授权码
四、使用数据库存储Client信息
1、为了方便第三方客户端进行OAuth2认证服务的接入管理,在实际的项目运行中,往往会设计一套专属的客户端数据库,接入的客户端按照指定要求填写相关数据并审核通过后就可以使用OAuth2服务。
创建client数据表,并为其添加数据
use springsecurity;
create table client(
cid varchar(50) not null,
secret varchar(50),
grants varchar(50),
url varchar(50),
constraint pk_mid primary key(cid)
)engine='innodb';
insert into client values ('client_ran','{bcrypt}$2a$10$zAzb4c4zv4SJ.U4vh2VB5.pgr5oXtzMaOi/ISrvvMsmOgK4vvRXcS','webapp','authorization_code','http://localhost/message/show')
insert into client values ('client_zong','{bcrypt}$2a$10$zAzb4c4zv4SJ.U4vh2VB5.pgr5oXtzMaOi/ISrvvMsmOgK4vvRXcS','webapp','authorization_code','http://baidu.com')
2、创建实体类
@Data
@Entity
@Table
public class Client {
@Id
private String cid;
private String secret; // 客户端密钥
private String scope; // 授权范围
private String grants; // 授权类型
private String url;
}
3、创建数据层接口
public interface IClientDAO extends JpaRepository<Client,String> { }
4、修改ClientdetailsServiceImpl实现类,通过IClientDao接口
@Service
public class ClientDetailsServiceImpl implements ClientDetailsService {
@Autowired
private IClientDAO clientDAO;
@Override
public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
Optional<Client> optional = this.clientDAO.findById(s);
if (!optional.isPresent()) throw new ClientRegistrationException("客户端信息不存在!");
Client client = optional.get();
BaseClientDetails clientDetails = new BaseClientDetails();//系统默认实现的子类
clientDetails.setClientId(s);//可以随机生成
clientDetails.setClientSecret(client.getSecret());// 密码可以进行加密处理
clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getGrants()));//授权类型
clientDetails.setScope(Arrays.asList(client.getScope()));
clientDetails.setAccessTokenValiditySeconds(30);
clientDetails.setAutoApproveScopes(clientDetails.getScope());//自动处理
Set<String> redirectSet = new HashSet<>();
redirectSet.addAll(Arrays.asList(client.getUrl()));
clientDetails.setRegisteredRedirectUri(redirectSet); //如果没有配置正确的路径是无法authcode获取的的
return clientDetails;
}
}
五、使用Redis保存令牌
OAuth2认证处理流程中,用户相关资料获取需要通过token来完成,而Token的生成需要通过客户端注册信息以及临时授权码来完成。最佳做法将token保存到redis中,实现token分布式存储,方便获取用户信息
1、引入redis相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
2、在application.yml文件中进行redis配置
spring:
redis:
host: localhost
port: 6379
database: 0 #redis索引
timeout: 200ms
lettuce:
pool: #配置连接池
max-active: 100 #最大连接数
max-idle: 29 #连接池中的最大空闲连接
min-idle: 10 #连接池中的最小空闲连接
max-wait: 1000 #连接池最大阻塞等待时间
time-between-eviction-runs: 2000 #每2s回收一次空闲连接
3、修改AuthorizationServerConfig配置类
@Configuration
@EnableAuthorizationServer//启用授权服务
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
// @Qualifier(value = "ClientDetailsServiceImpl")//标注Bean名称
private ClientDetailsServiceImpl clientDetailsService;
@Override//客户端配置
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(this.clientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//通过Redis保存Token
endpoints.tokenStore(new RedisTokenStore(this.redisConnectionFactory));
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许通过FROM表单实现客户端认证,客户端传递client_id和client_secret获取Token数据
security.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()")//通过验证返回Token信息
.tokenKeyAccess("permitAll()") //获取Token请求不进行拦截
.passwordEncoder(this.passwordEncoder); //密码编辑器
}
}
记住此时使用的是RedisTokenStore实现了Redis数据存储,这个数据存储的时候就可以直接观察里面对应的常量的信息有很多都将作为最终存储在Redis中的标记出现。
4、【浏览器】获取Token,则首先需要获得授权码,采用与前面相同的访问路径实现
admin:123456@localhost/oauth/authorize?client_id=client_ran&response_type=code&redirect_uri=http://localhost/message/show
执行结果:http://localhost/message/show?code=N8ni_v
5、【curl命令】post请求传递client_id、client_secret、code等核心参数。authcode检测正确后将获得并在Redis中保存Token数据
curl -X POST -d “client_id=client_ran&client_secret=123456&grant_type=authorization_code&code=N8ni_v&redirect_uri=http://localhost/message/show” “http://localhost/oauth/token”
执行结果:
6、为了方便访问,curl命令获取token数据,当传递参数正确时,程序会向redis中保存相关的Token数据,同时也会返回给客户端使用。
五、OAuth2资源服务
资源服务是OAuth2客户端获取数据的唯一途径,在进行资源获取时,客户端需要传递合法token到资源服务器中
1、创建一个获取资源的Action程序类,直接返回用户认证数据
@RestController
public class ResourceAction {
@RequestMapping("/resource")
public Principal resource(Principal user){
return user;//获取用户详情
}
}
2、创建Resource服务配置类
@Configuration
@EnableResourceServer //启用资源服务
@Order(50)
@Slf4j //日志注解
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.stateless(true); // 无状态存储
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用CSRF校验
.exceptionHandling() // 异常处理
.authenticationEntryPoint((request,response,authenticationException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED))//状态码
.and().authorizeRequests().anyRequest().authenticated(); //认证访问
}
}
3、【资源验证】浏览器直接访问路径:
http://localhost/resource
结果:
4、【正常访问】通过OAuth2服务端获取authcode,随后再获取Token,而后再通过Token访问Resoure资源。
根据获取到的token获取相关资源,使用curl命令访问
curl -X GET “http://localhost/resource?access_token=Ngd1x9-D1TLRio0tPKl8yNiFoss”
六、OAuth2客户端访问
OAuth2服务搭建完成就可以对提供统一认证服务处理,如果第三方客户端使用Spring Boot 框架开发,并且需要接入OAuth2实现统一认证,只需要导入“spring-boot-starter-security” 和 “spring-security-oauth2-autoconfigure”两个依赖即可实现整合。而整合处理过程中只需要在客户端项目的application.yml配置文件中配置相关数据获取地址即可自动实现授权码,token以及资源处理
1、【本地系统】为了模拟出OAuth2客户端和服务端的操作流程,本次将hosts注册两台新的主机
127.0.0.1 oauth-server | oauth2认证主机,在之前搭建完成,占用80端口 |
---|---|
127.0.0.1 oauth-client | oauth2第三方应用接入主机,占用8888端口,需要在client表中注册此地址 |
2、【数据库】需要进行客户端接入处理,需要提供一个相应的客户端注册信息,在client数据表中添加以下数据
insert into client values ('client_happy','{bcrypt}$2a$10$zAzb4c4zv4SJ.U4vh2VB5.pgr5oXtzMaOi/ISrvvMsmOgK4vvRXcS','webapp','authorization_code','http://oauth-client:8888/login');
3、【项目】添加需要的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
4、创建application文件,对oauth2进行配置
server:
port: 8888
security:
oauth2:
client:
client-id: client_happy #注册id
client-secret: 123456 #注册密钥
user-authorization-uri: http://localhost/oauth/authorize
access-token-uri: http://oauth-server/oauth/token
authentication-scheme: query #认证模式
client-authentication-scheme: form #client信息提交模式
scope: webapp # 应用范围
authorized-grant-types: code #授权类型
registered-redirect-uri: http://oauth-client:8888/login #返回地址
resource:
user-info-uri: http://oauth-server/resource #资源地址
5、创建客户端安全访问配置类
@Configuration
@EnableOAuth2Sso // 启用单点登录
public class ClientWebSecurityConfigureAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();
}
}
6、创建ClientAction类返回资源数据
@RestController
public class ClientAction {
@GetMapping("/client")
public Object client(){
return SecurityContextHolder.getContext().getAuthentication();// 认证数据
}
}
7、【测试程序】分别启动服务端和客户端两个子模块内容,随后访问路径:
oauth-client:8888/client
由于SSO作用所以自动跳转到了"oauth-server"路径之中,当认证完成之后又跳回来“oauth-client:8888/client”路径,从而获取用户信息