结构基础
基础框架:Spring Boot + Spring-Security-OAuth2
存储介质:Mysql + Redis
持久化方式:Spring-data-jpa
测试工具:Postman
大局观:
1、OAuth2服务器分为两部分组成:认证授权服务器和资源服务器。闻名知意,不解释。本文只讲认证授权服务器的搭建,资源服务器部分后续。
2、认证授权服务器分为两大步骤,一是认证,二是授权。而认证则主要由Spring-Security负责,而授权则有Oauth2负责。
3、本项目有2个存储介质,Mysql和Redis。Mysql的作用是用来存储认证数据,而Redis用作缓存和存储授权信息及AccessToken的。其实,Mysql同事可以用来存储认证数据和存储授权信息以及AccessToken的,而且Spring-Security-OAuth2也提供了存储基础。那么问题来了,为什么不用Mysql呢?考虑原因:AccessToken是有时效性的,也就是说,存储一段时间后,将会失效,也许是一天或者一个月。在单体应用情况下,当业务比较多、访问频率大的时候,如果使用mysql,那么有可能导致响应速度降低,基于性能的考虑,减小数据库的压力,所以将其改良为使用Redis存储授权信息和AccessToken。而Redis性能十分优越,同时还能作为缓存认证信息使用,一举两得,何乐而不为呢?
学习基础
认证方式
Oauth2授权有多种方式,此处将使用grant_type为client_secret和password两种方式。
1、客户端授权(Client Credentials Grant)
POST /oauth2-server/oauth/token?grant_type=client_credentials HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
请求信息如上。注意事项如下:
1、在mysql中建立基础表:oauth_client_details,查看建表以及初始化。其中client_id=client_auth_mode,client_secret的原始值为123456,数据库中存储的是加密后的值,加密方式为BCrypt。
2、请求头:key=Authorization;value=Basic+空格+Base64(username:password)
3、Basic后面的信息由[username:password]内的字符Base64加密而成
4、此中的username和password分别为oauth_client_details表中的client_id和client_secret,也就是客户端模式下的标识客户端的凭证(用以区别是哪种受信任的客户端),对应OAuth2映射为ClientDetails对象。
2、密码授权
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
请求信息如上。注意事项如下:
1、在mysql中建立基础表:oauth_client_details和ux_member,查看建表以及初始化。其中oauth_client_details表中client_id=password_auth_mode,client_secret的原始值为123456,数据库中存储的是加密后的值,加密方式为BCrypt。ux_member表中,username=member_name,password=123456,加密方式MD5。
1、请求头:key=Authorization;value=Basic+空格+Base64(username:password)
2、Basic后面的信息由[username:password]内的字符Base64加密而成
3、此中的username和password依旧为oauth_client_details表中的client_id和client_secret,也就是客户端模式下的标识客户端的凭证(用以区别是哪种受信任的客户端),对应OAuth2映射为DetailDetails对象。
4、由上至少可看出二者在传参时的表面上的区别,只是密码授权模式,多了2个参数:username和password,以及grant_type的值不一样。而里层的区别,在于密码模式下,Spring-Security-Oauth2中,有个叫做UserDetails的对象,而刚好ux_member表就是与之对应。
大局观已有,废话少说,下面开始讲述相关配置
存储介质
Mysql
a、作用:存储认证管理信息和业务数据。那么问题来了,什么称之为认证信息呢?我的理解为能标识用户主体是谁的唯一性的信息,这里的主体可能为客户端也可能为某个PC或者移动端的某个人。
b、设计:在本项目中,所谓的认证信息有2个,oauth_client_details与ux_member表。与之对应的也就是ClientDetails和UserDetails对象。这两个都是待认证的主体,也就是说在客户端模式下,需要对ClientDetails对象进行认证;而在密码模式下,则既需要对ClientDetails对象认证,也需要对UserDetails对象认证。
Redis
a、存储授权信息以及AccessToken
b、缓存密码模式下的认证信息(UserDetails对象,以username为key)
配置信息
security:
basic:
enabled: false # 是否开启基本的鉴权,默认为true。 true:所有的接口默认都需要被验证,将导致 拦截器[对于 excludePathPatterns()方法失效]
server:
context-path: /oauth2-server
port: 8050
---
spring:
application:
name: oauth2-server
redis:
database: 4
host: 127.0.0.1
password: root123456
port: 6379
pool:
max-active: 8
max-wait: 8
min-idle: 0
max-idle: 8
datasource:
# dataSourceClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/redis-oauth2?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
database: MYSQL
openInView: true
show_sql: true
generate-ddl: true #(false)
hibernate:
ddl-auto: update #(none)
在resources文件夹下建立一个application.yml文件,然后把上述信息拷贝进去,即可。
因为本项目是基于Spring Boot的开发,Spring Boot其中一个好处就是能够根据你的配置信息自动生成相关的Bean对象,如数据源DataSource、缓存工厂类RedisConnectionFactory、缓存RedisCache等Bean对象。
惊不惊喜,意不意外
数据存储配置
@Configuration
public class DataStoreConfig {
public static final String REDIS_CACHE_NAME="redis_cache_name";//不为null即可
public static final String REDIS_PREFIX ="redis_cache_prefix";//不为null即可
public static final Long EXPIRE =60*60L;//缓存有效时间
/**
* 配置用以存储用户认证信息的缓存
*/
@Bean
RedisCache redisCache(RedisTemplate redisTemplate){
RedisCache redisCache = new RedisCache(REDIS_CACHE_NAME,REDIS_PREFIX.getBytes(),redisTemplate,EXPIRE);
return redisCache;
}
/**
*
* 创建UserDetails存储服务的Bean:使用Redis作为缓存介质
* UserDetails user = this.userCache.getUserFromCache(username)
*/
@Bean
public UserCache userCache(RedisCache redisCache) throws Exception {
UserCache userCache = new SpringCacheBasedUserCache(redisCache);
return userCache;
}
/**
* 配置AccessToken的存储方式:此处使用Redis存储
* Token的可选存储方式
* 1、InMemoryTokenStore
* 2、JdbcTokenStore
* 3、JwtTokenStore
* 4、RedisTokenStore
* 5、JwkTokenStore
*/
@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
return new RedisTokenStore(redisConnectionFactory);
}
}
Domain层简述
@Entity
@Table(name = "ux_member")
public class Member implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
public Member(Member member){
super();
this.username = member.getUsername();
this.password = member.getPassword();
}
public Member() {
}
//略过getter和setter
}
//默认角色
public class Role implements GrantedAuthority {
private static final long serialVersionUID = -2633659220734280260L;
private Set roles = new HashSet();
@Override
public String getAuthority() {
return "USER";
}
public Set getRoles() {
return roles;
}
public void setRoles(Set roles) {
this.roles = roles;
}
}
Dao层
@Component("memberRepository")
public interface MemberRepository extends JpaRepository {
Member findOneByUsername(String username);
}
Service层
@Service
public class CustomUserDetailsService implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findOneByUsername(username);
if (member == null) {
log.error("用户不存在");
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(member);
}
/**
* 注意该类的层次结构,继承了Member并实现了UserDetails接口,继承是为了使用Member的username和password信息
*/
private final static class UserRepositoryUserDetails extends Member implements UserDetails {
private static final long serialVersionUID = 1L;
private UserRepositoryUserDetails(Member member) {
super(member);
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Role role = new Role();
return role.getRoles();
}
@Override
public String getUsername() {
return super.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
自定义认证服务器类:用来对UserDetails信息进行认证,CustomUserDetailsService类实现了UserDetailsService接口,而UserDetailsService则是用来对UserDetails进行认证检查的,该项目是基于SpringBoot的,所以,该Bean对象将会注入依赖该Bean的其他的Bean对象中,如DaoAuthenticationProvider、DefaultTokenServices等,并在相关的认证流程中对UserDetails进行检查。
认证授权配置
1、Spring-Security-OAuth2对于认证信息的存储提供了如下方案:数据库和内存。而此处将使用Mysql存储。
2、认证管理信息的配置主要是针对ClientDetails和UserDetails对象的检查,客户端模式针对ClientDetails检查,而密码模式则先检查ClientDetails后检查UserDetails对象。
认证授权配置如下
@Configuration
@EnableAuthorizationServer//开启配置 OAuth 2.0 认证授权服务
public class AuthAuthorizeConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* 配置 oauth_client_details【client_id和client_secret等】信息的认证【检查ClientDetails的合法性】服务
* 设置 认证信息的来源:数据库 (可选项:数据库和内存,使用内存一般用来作测试)
* 自动注入:ClientDetailsService的实现类 JdbcClientDetailsService (检查 ClientDetails 对象)
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
/**
* 密码模式下配置认证管理器 AuthenticationManager,并且设置 AccessToken的存储介质tokenStore,如果不设置,则会默认使用内存当做存储介质。
* 而该AuthenticationManager将会注入 2个Bean对象用以检查(认证)
* 1、ClientDetailsService的实现类 JdbcClientDetailsService (检查 ClientDetails 对象)
* 2、UserDetailsService的实现类 CustomUserDetailsService (检查 UserDetails 对象)
*
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore).userDetailsService(userDetailsService);
}
/**
* 配置:安全检查流程
* 默认过滤器:BasicAuthenticationFilter
* 1、oauth_client_details表中clientSecret字段加密【ClientDetails属性secret】
* 2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤,默认值:denyAll()
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();//允许客户表单认证
security.passwordEncoder(new BCryptPasswordEncoder());//设置oauth_client_details中的密码编码器
security.checkTokenAccess("permitAll()");//对于CheckEndpoint控制器[框架自带的校验]的/oauth/check端点允许所有客户端发送器请求而不会被Spring-security拦截
}
}
启动服务器
@SpringBootApplication
public class Oauth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2ServerApplication.class, args);
}
}
Postman测试
客户端授权模式获取AccessToken请求如下:
客户端模式
请求的报文信息如下:
POST /oauth2-server/oauth/token?grant_type=client_credentials HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
Cache-Control: no-cache
Postman-Token: e5d3ea12-af31-d344-8804-f92db46112a3
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
返回结果如:
{
"access_token": "afef641c-62de-4f5d-a5b8-7864ac2b7127",
"token_type": "bearer",
"expires_in": 3463,
"scope": "read write"
}
密码授权模式获取AccessToken请求如下:
密码模式
请求的报文信息如下:
POST /oauth2-server/oauth/token?username=member_name&password=e10adc3949ba59abbe56e057f20f883e&grant_type=password&client_id=password_auth_mode&client_secret=123456 HTTP/1.1
Host: 127.0.0.1:8050
Authorization: Basic cGFzc3dvcmRfYXV0aF9tb2RlOjEyMzQ1Ng==
Cache-Control: no-cache
Postman-Token: 0ccf7ea9-c2ac-10bc-a9da-3d15de82840b
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
返回结果如:
{
"access_token": "a83ba33f-9f1a-4f9a-ba65-99e7fc905ba2",
"token_type": "bearer",
"refresh_token": "89f724d6-8553-4838-b4ff-7f6c8fb4d88b",
"expires_in": 3378,
"scope": "read write"
}
结果对比
差异:客户端授权返回结果比密码模式返回结果少了一个refresh_token,因为客户模式不支持refresh_token认证。
原因:client_credentials是受信任的认证模式,也就意味着你对于此种信息都是信任的,即可以设置为永久性的AccessToken,而不需要刷新重新获取AccessToken。
总结
对于Spring-Security-Oauth2的学习和研究,陆陆续续地持续了不少时间,零零散散地也做了不少的笔记,踩了不少的坑,不奇怪,Spring-Security-OAuth2都没个官方文档。写文章的时候,也是一边敲着代码,一边优化着,去除了不少无用的代码,也理清了头绪。如有错误,还请大牛们指出。
源代码地址:oauth2-redis-mysql[提醒,直接导入我的项目前,需要启动redis服务,并修改相关的redis配置和数据库配置,如果未启动redis服务,程序运行成功,但是spring boot默认将TokenStore设置为InMemoryStore,获取AccessToken也将失败!]
话外篇
oauth2-redis-mysql项目中的oauth2-server模块项目仅在OAuth2服务器中充当认证授权的角色,而一个完整的OAuth2服务,则由资源服务器和认证授权服务器组成,这两个可以合二为一,也可以分开。后续我将抽空,编写OAuth2资源服务器的搭建,在上述链接中已经有个名为oauth2-client的模块项目,也就是OAuth2资源服务器,具体使用,稍后再续。