介绍:Spring Security Oath2是Oath2标准的一种实现,是基于Spring Security框架的封装。
模块一 前提知识:
- Oath2.0标准
Oath是一种标准,它规定了第三方应用访问本应用资源的协议方法,目前版本为2.0。
Oath2 大致流程为:客户端访问应用资源,需先在资源提供方进行认证得到令牌,之后持令牌范文资源提供方的资源信息。认证服务和资源服务可为同一个服务,也可为不同独立服务。
Oath2 提供了四种获取令牌的模式:授权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner password credentials)、客户端模式(client credentials)
具体理解Oath2.0原理可参考阮一峰大神博客 - Spring Security应用
SpringSecurity是基于Filter和AOP来对Spring应用进行保护的一套框架,它可对请求级别和方法级别进行安全保护。
其应用及分析可参考另一篇博客,SpringSecurity初级应用及分析
模块二 组件使用:
- 认证服务器
a 内存保存访问令牌
i. 引入Oath2的依赖,SpringBoot版本为最新Greenwich
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
II. 配置访问服务的认证,即配置Spring Security(这里登录账号密码为从数据库加载)
/**
* 根据用户名加载登陆账号信息
*/
@Service
public class SysUserService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userDao.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (!StringUtils.isEmpty(user.getRoles())) {
String[] roles = user.getRoles().split(",");
for (String role : roles) {
if (!StringUtils.isEmpty(role)) {
authorities.add(new SimpleGrantedAuthority(role.trim()));
}
}
}
//这里可直接让SysUser继承实现UserDetails接口,或者使用SpringSecurity提供的User类
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
/**
* 配置Spring Security
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserService sysUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(sysUserService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/check_token");
}
@Bean
public BCryptPasswordEncoder createPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
III. 配置访问令牌的认证,即配置AuthorizationServerConfigurerAdapter
/**
* 配置内存存储Token令牌
*/
@Configuration
@EnableAuthorizationServer
public class MemoryAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final String clientId = "sysclientId";
private final String secretId = "syssecretId";
private final String authorizedGrantTypes = "authorization_code";
private final String scopes = "applist";
private final String redirectUri = "http://localhost:9001/hello";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(createPasswordEncoder().encode(secretId))
.authorizedGrantTypes(authorizedGrantTypes)
.scopes(scopes) //申请的权限列表,会直接展示在申请页面上
.redirectUris(redirectUri);
}
/**
* 必须显示定义加密编码器
* @return
*/
@Bean
public BCryptPasswordEncoder createPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
iv. 启动应用程序,spring security oath2 会开放一些endpoints,如下:
/oauth/authorize:授权
/oauth/token:令牌
/oauth/error:授权服务错误信息
/oauth/check_token:令牌解析校验
首先访问授权endpoint去获取授权码,注意sysclientid替换成自己配置的clientid
http://localhost:8080/oauth/authorize?client_id=sysclientid&response_type=code
程序默认生成如下授权页面,可以自定义,其中的applist就是上面定义的scopes授权信息。
点击Approve授权并Authorize之后,会重定向到定义的redirectUri并在请求参数上新增了参数code=3kTpl3,如下图:
v. 接着Post方式访问令牌接口/oauth/token 请求令牌,请求参数为grant_type和code,这里使用Postman模拟请求,地址为如下,注意替换自己的clientId和secretId
http://sysclientId:syssecretId@localhost:8081/oauth/token
结果如下,可以获取到bearer类型的Token:
b 数据库保存访问令牌
I. 由于令牌为数据库保存令牌,所以和内存保存令牌流程区别为配置数据库相关,首先额外引入数据源依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
II. 配置访问服务的认证,配置同上。
III. 配置访问令牌的认证,即配置AuthorizationServerConfigurerAdapter,这里需引入数据源,使用的是Hikari数据源。
/**
* 配置数据库存储令牌
*/
@Configuration
@EnableAuthorizationServer
public class JDBCAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
/**
* 配置获取Token令牌为数据库获取
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* 配置获取Client客户端信息为数据库获取
* @return
*/
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore());
}
}
IV. 配置数据源信息,注意配置时区信息,mysql 6.0以上都需要配置,这里配置的为北京(东八区)时间。
spring:
application:
name: authorization-module
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://IP:端口号/数据库?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
username: ******
password: ******
# Hikari数据源配置
hikari.maximum-pool-size: 20
hikari.minimum-idle: 5
server:
port: 8081
logging:
level=Debug
V. 新建存储Token相关的数据库,参考官方推荐建表,可直接执行以下SQL:
-- 参考官方推荐建表 https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
-- 注意替换原来HSQL的LONGVARBINARY类型为MySQL的BLOB类型(注意不是MYSQL的TEXT类型!)
-- used in tests that use HSQL
drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
drop table if exists oauth_client_token;
create table oauth_client_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
drop table if exists oauth_access_token;
create table oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
drop table if exists oauth_code;
create table oauth_code (
code VARCHAR(256), authentication BLOB
);
drop table if exists oauth_approvals;
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
drop table if exists ClientDetails;
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
-- 插入数据
INSERT INTO `oauth_client_details` VALUES ('sysclientid', NULL, '$2a$10$tFPqxfJVvGSR/gVm5KDUJeRGswbiE9HSVNMO8NcG5rni5a8fmG6qm', 'UserInfo', 'authorization_code', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL)
VI. 启动程序,访问授权码接口获取授权码,然后根据授权码访问Token接口请求Token,可参考上面【内存保存访问令牌】步骤。访问的clientId和secretId信息存储在oauth_client_details表中,其中的secretId信息是经过BCryptPasswordEncoder加密的,可自定插入数据进行测试。
2. 资源服务器
a 配置资源服务
/**
* 配置此服务为资源服务器
*/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}
b. 配置提供的资源,即相应的Http服务,这里可使用注解@PreAuthorize对方法级别进行权限校验。之后使用Token进行访问时会校验Token对应用户的权限。
@RestController()
@RequestMapping("/api")
public class HelloController {
@GetMapping("/")
public String hello() {
return "Hello, I am /";
}
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_SYSTEM')")
public String getHello(String access_token) {
return "Hello, I am /hello";
}
}
c. 配置认证服务器配置
# 认证服务器配置
security:
oauth2:
client:
client-id: sysclientid
client-secret: syssecretid #加密前的
access-token-uri: http://localhost:8081/oauth/token
user-authorization-uri: http://localhost:8081/oauth/authorize
resource:
token-info-uri: http://localhost:8081/oauth/check_token
d. 进行Http资源访问,请求是需携带参数Token信息,可直接在请求地址后携带 ?access_token= 参数,也可放到Header中携带。
I. 使用直接地址携带参数格式例如:
http://localhost:8080/api/?access_token=3cd5e11f-76b1-4ae6-8fb6-38e67da29452
http://localhost:8080/api/hello?access_token=3cd5e11f-76b1-4ae6-8fb6-38e67da29452
使用Header携带参数格式如下,注意Bearer后有空格分隔!
II. 下面使用使用Postman工具演示各种场景:不使用token或者错误token直接访问,结果如下:
III. 使用正确token访问,token获取参考上面基于内存或者基于数据库存储令牌,结果如下:
IV. 访问权限不够的资源,会返回不允许访问信息,结果如下: