正在向icntv服务器认证授权信息,Spring-Security-OAuth2服务器之搭建认证授权服务器[一]...

结构基础

基础框架: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对象。

734348fb6cbb?utm_campaign=haruki&utm_content=note&utm_medium=reader_share&utm_source=weixin

惊不惊喜,意不意外

数据存储配置

@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请求如下:

734348fb6cbb?utm_campaign=haruki&utm_content=note&utm_medium=reader_share&utm_source=weixin

客户端模式

请求的报文信息如下:

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请求如下:

734348fb6cbb?utm_campaign=haruki&utm_content=note&utm_medium=reader_share&utm_source=weixin

密码模式

请求的报文信息如下:

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资源服务器,具体使用,稍后再续。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值