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性能十分优越,同时还能作为缓存认证信息使用,一举两得,何乐而不为呢?

学习基础

参考理解OAuth 2.0 - 阮一峰的网络日志

认证方式

Oauth2授权有多种方式,此处将使用grant_typeclient_secretpassword两种方式。


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<Role> roles = new HashSet<Role>();

    @Override
    public String getAuthority() {
        return "USER";
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

}

Dao层

@Component("memberRepository")
public interface MemberRepository extends JpaRepository<Member, Long> {
    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资源服务器,具体使用,稍后再续。

Spring-Security-OAuth2服务器之搭建认证授权服务器[一]

Spring-Security-OAuth2服务器搭建之AccessToken的检测[二]

Spring-Security-OAuth2服务器搭建之资源服务器搭建[三]

Spring-Security-OAuth2资源服务器及SpringSecurity权限控制[四]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容