springboot程序员技术干货

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

2017-07-07  本文已影响1143人  彳亍路

结构基础

基础框架: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表就是与之对应。


大局观已有,废话少说,下面开始讲述相关配置

存储介质

配置信息


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权限控制[四]

上一篇 下一篇

猜你喜欢

热点阅读