基于Spring Security Oauth2的SSO单点登陆+JWT权限控制实践

Profile


概 述

在前文《基于Spring Security和 JWT的权限系统设计》之中已经讨论过基于 Spring SecurityJWT的权限系统用法和实践,本文则进一步实践一下基于 Spring Security Oauth2实现的多系统单点登陆(SSO)和 JWT权限控制功能,毕竟这个需求也仍是蛮广泛的。html

代码已开源,放在文尾,须要自取

理论知识

在此以前须要学习和了解一些前置知识包括:git

  • Spring Security:基于 Spring实现的 Web系统的认证和权限模块
  • OAuth2:一个关于受权(authorization)的开放网络标准
  • 单点登陆 (SSO):在多个应用系统中,用户只须要登陆一次就能够访问全部相互信任的应用系统
  • JWT:在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于做为JSON对象在不一样系统之间进行安全地信息传输。主要使用场景通常是用来在 身份提供者和服务提供者间传递被认证的用户身份信息

要完成的目标

  • 目标1:设计并实现一个第三方受权中心服务(Server),用于完成用户登陆,认证和权限处理
  • 目标2:能够在受权中心下挂载任意多个客户端应用(Client
  • 目标3:当用户访问客户端应用的安全页面时,会重定向到受权中心进行身份验证,认证完成后方可访问客户端应用的服务,且多个客户端应用只须要登陆一次便可(谓之 “单点登陆 SSO”)

基于此目标驱动,本文设计三个独立服务,分别是:github

  • 一个受权服务中心(codesheep-server
  • 客户端应用1(codesheep-client1
  • 客户端应用2(codesheep-client2

多模块(Multi-Module)项目搭建

三个应用经过一个多模块的 Maven项目进行组织,其中项目父 pom中须要加入相关依赖以下:spring

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.0.8.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

    <dependency>
        <groupId>io.spring.platform</groupId>
        <artifactId>platform-bom</artifactId>
        <version>Cairo-RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Finchley.SR2</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

</dependencies>

项目结构以下:数据库

项目结构


受权认证中心搭建

受权认证中心本质就是一个 Spring Boot应用,所以须要完成几个大步骤:浏览器

  • pom中添加依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
</dependencies>
  • 项目 yml配置文件:
server:
  port: 8085
  servlet:
    context-path: /uac

即让受权中心服务启动在本地的 8085端口之上缓存

  • 建立一个带指定权限的模拟用户
@Component
public class SheepUserDetailsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        if( !"codesheep".equals(s) )
            throw new UsernameNotFoundException("用户" + s + "不存在" );

        return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
    }
}

这里建立了一个用户名为codesheep,密码 123456的模拟用户,而且赋予了 普通权限ROLE_NORMAL)和 中等权限ROLE_MEDIUM安全

  • 认证服务器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        // 定义了两个客户端应用的通行证
        clients.inMemory()
                .withClient("sheep1")
                .secret(new BCryptPasswordEncoder().encode("123456"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .autoApprove(false)
                .and()
                .withClient("sheep2")
                .secret(new BCryptPasswordEncoder().encode("123456"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("all")
                .autoApprove(false);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
        DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()");
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("testKey");
        return converter;
    }

}

这里作的最重要的两件事:一是 定义了两个客户端应用的通行证(sheep1sheep2);二是 配置 token的具体实现方式为 JWT Token服务器

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

}

客户端应用建立和配置

本文建立两个客户端应用:codesheep-client1codesheep-client2,因为二者相似,所以只以其一为例进行讲解网络

  • SSO客户端应用配置类 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}

复杂的东西都交给注解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
  port: 8086

security:
  oauth2:
    client:
      client-id: sheep1
      client-secret: 123456
      user-authorization-uri: ${auth-server}/oauth/authorize
      access-token-uri: ${auth-server}/oauth/token
    resource:
      jwt:
        key-uri: ${auth-server}/oauth/token_key

这里几项配置都很是重要,都是须要和前面搭建的受权中心进行通讯的

  • 建立测试控制器 TestController
@RestController
public class TestController {

    @GetMapping("/normal")
    @PreAuthorize("hasAuthority('ROLE_NORMAL')")
    public String normal( ) {
        return "normal permission test success !!!";
    }

    @GetMapping("/medium")
    @PreAuthorize("hasAuthority('ROLE_MEDIUM')")
    public String medium() {
        return "medium permission test success !!!";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String admin() {
        return "admin permission test success !!!";
    }
}

此测试控制器包含三个接口,分别须要三种权限(ROLE_NORMALROLE_MEDIUMROLE_ADMIN),待会后文会一一测试看效果


实验验证

  • 启动受权认证中心 codesheep-server(启动于本地8085端口)
  • 启动客户端应用 codesheep-client1 (启动于本地8086端口)
  • 启动客户端应用 codesheep-client2 (启动于本地8087端口)

首先用浏览器访问客户端1 (codesheep-client1) 的测试接口:localhost:8086/normal,因为此时并无过用户登陆认证,所以会自动跳转到受权中心的登陆认证页面:http://localhost:8085/uac/login

自动跳转到受权中心统一登陆页面

输入用户名 codesheep,密码 123456,便可登陆认证,并进入受权页面:

受权页面

赞成受权后,会自动返回以前客户端的测试接口:

自动返回客户端接口并调用成功

此时咱们再继续访问客户端1 (codesheep-client1) 的测试接口:localhost:8086/medium,发现已经直接能够调用而无需认证了:

直接访问

因为 localhost:8086/normallocalhost:8086/medium要求的接口权限,用户codesheep均具有,因此能顺利访问,接下来再访问一下更高权限的接口:localhost:8086/admin

无权限访问

好了,访问客户端1 (codesheep-client1) 的测试接口到此为止,接下来访问外挂的客户端2 (codesheep-client2) 的测试接口:localhost:8087/normal,会发现此时会自动跳到受权页:

因为用户已经过客户端1登陆过_所以再访问客户端2即无需登陆_而是直接跳到受权页

受权完成以后就能够顺利访问客户端2 (codesheep-client2) 的接口:

顺利访问客户端2的接口

这就验证了单点登陆SSO的功能了!


未完待续

受篇幅所限,本文应该说实践了一下精简流程的:SSO单点登陆和JWT权限控制,还有不少能够复杂和具化的东西能够实现,好比:

  • 客户端 client凭据 和 用户 user的凭据能够用数据库进行统一管理
  • 认证 token也能够用数据库或缓存进行统一管理
  • 受权认证中心的统一登陆页面能够自定义成须要的样子
  • 认证中心的受权页也能够自定义,甚至能够去掉
  • 包括一些异常提示也能够自定义

总之,尽情地折腾去吧!

本文开源代码地址在此:Spring-Boot-In-Action,须要自取

写在最后

因为能力有限,如有错误或者不当之处,还请你们批评指正,一块儿学习交流!
相关文章
相关标签/搜索