概   览

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

注:本文代码已开源在此



代码地址:github.com/hansonwang99/Spring-Boot-In-Action

需要请自取


理论知识

在此之前需要学习和了解一些前置知识包括:

  • Spring Security:基于 Spring实现的 Web系统的认证和权限模块

  • OAuth2:一个关于授权( authorization)的开放网络标准

  • 单点登录 (SSO):在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

  • JWT:在网络应用间传递信息的一种基于 JSON的开放标准(( RFC 7519),用于作为 JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息


要完成的目标

  • 目标1:设计并实现一个第三方授权中心服务( Server),用于完成用户登录,认证和权限处理

  • 目标2:可以在授权中心下挂载任意多个客户端应用( Client

  • 目标3:当用户访问客户端应用的安全页面时,会重定向到授权中心进行身份验证,认证完成后方可访问客户端应用的服务,且多个客户端应用只需要登录一次即可(谓之 “单点登录 SSO”)

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

  • 一个授权服务中心( codesheep-server

  • 客户端应用1( codesheep-client1

  • 客户端应用2( codesheep-client2


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

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


  1. <dependencies>


  2.    <dependency>

  3.        <groupId>org.springframework.boot</groupId>

  4.        <artifactId>spring-boot-dependencies</artifactId>

  5.        <version>2.0.8.RELEASE</version>

  6.        <type>pom</type>

  7.        <scope>import</scope>

  8.    </dependency>


  9.    <dependency>

  10.        <groupId>io.spring.platform</groupId>

  11.        <artifactId>platform-bom</artifactId>

  12.        <version>Cairo-RELEASE</version>

  13.        <type>pom</type>

  14.        <scope>import</scope>

  15.    </dependency>


  16.    <dependency>

  17.        <groupId>org.springframework.cloud</groupId>

  18.        <artifactId>spring-cloud-dependencies</artifactId>

  19.        <version>Finchley.SR2</version>

  20.        <type>pom</type>

  21.        <scope>import</scope>

  22.    </dependency>


  23. </dependencies>

项目结构如下:


授权认证中心搭建

授权认证中心本质就是一个 SpringBoot应用,因此需要完成几个大步骤:

  • 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端口之上

  • 创建一个带指定权限的模拟用户


  1. @Component

  2. public class SheepUserDetailsService implements UserDetailsService {


  3.    @Autowired

  4.    private PasswordEncoder passwordEncoder;


  5.    @Override

  6.    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {


  7.        if( !"codesheep".equals(s) )

  8.            throw new UsernameNotFoundException("用户" + s + "不存在" );


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

  10.    }

  11. }

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

  • 认证服务器配置 AuthorizationServerConfig


  1. @Configuration

  2. @EnableAuthorizationServer

  3. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


  4.    @Override

  5.    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {


  6.        // 定义了两个客户端应用的通行证

  7.        clients.inMemory()

  8.                .withClient("sheep1")

  9.                .secret(new BCryptPasswordEncoder().encode("123456"))

  10.                .authorizedGrantTypes("authorization_code", "refresh_token")

  11.                .scopes("all")

  12.                .autoApprove(false)

  13.                .and()

  14.                .withClient("sheep2")

  15.                .secret(new BCryptPasswordEncoder().encode("123456"))

  16.                .authorizedGrantTypes("authorization_code", "refresh_token")

  17.                .scopes("all")

  18.                .autoApprove(false);

  19.    }


  20.    @Override

  21.    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {


  22.        endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());

  23.        DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();

  24.        tokenServices.setTokenStore(endpoints.getTokenStore());

  25.        tokenServices.setSupportRefreshToken(true);

  26.        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());

  27.        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());

  28.        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期

  29.        endpoints.tokenServices(tokenServices);

  30.    }


  31.    @Override

  32.    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

  33.        security.tokenKeyAccess("isAuthenticated()");

  34.    }


  35.    @Bean

  36.    public TokenStore jwtTokenStore() {

  37.        return new JwtTokenStore(jwtAccessTokenConverter());

  38.    }


  39.    @Bean

  40.    public JwtAccessTokenConverter jwtAccessTokenConverter(){

  41.        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

  42.        converter.setSigningKey("testKey");

  43.        return converter;

  44.    }


  45. }

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

  • Spring Security安全配置 SpringSecurityConfig


  1. @Configuration

  2. public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


  3.    @Override

  4.    @Bean

  5.    public AuthenticationManager authenticationManager() throws Exception {

  6.        return super.authenticationManager();

  7.    }


  8.    @Autowired

  9.    private UserDetailsService userDetailsService;


  10.    @Bean

  11.    public PasswordEncoder passwordEncoder() {

  12.        return new BCryptPasswordEncoder();

  13.    }


  14.    @Bean

  15.    public DaoAuthenticationProvider authenticationProvider() {

  16.        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();

  17.        authenticationProvider.setUserDetailsService(userDetailsService);

  18.        authenticationProvider.setPasswordEncoder(passwordEncoder());

  19.        authenticationProvider.setHideUserNotFoundExceptions(false);

  20.        return authenticationProvider;

  21.    }


  22.    @Override

  23.    protected void configure(HttpSecurity http) throws Exception {


  24.        http

  25.                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")

  26.                .and()

  27.                .authorizeRequests()

  28.                .antMatchers("/oauth/**").authenticated()

  29.                .and()

  30.                .formLogin().permitAll();

  31.    }


  32.    @Override

  33.    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

  34.        auth.authenticationProvider(authenticationProvider());

  35.    }


  36. }


客户端应用创建和配置

本文创建两个客户端应用: codesheep-client1  codesheep-client2,由于两者类似,因此只以其一为例进行讲解

  • SSO客户端应用配置类 ClientWebsecurityConfigurer


  1. @Configuration

  2. @EnableWebSecurity

  3. @EnableGlobalMethodSecurity(prePostEnabled = true)

  4. @EnableOAuth2Sso

  5. public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {


  6.    @Override

  7.    public void configure(HttpSecurity http) throws Exception {

  8.        http.antMatcher("/**").authorizeRequests()

  9.                .anyRequest().authenticated();

  10.    }

  11. }

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

  • application.yml配置


  1. auth-server: http://localhost:8085/uac

  2. server:

  3.  port: 8086


  4. security:

  5.  oauth2:

  6.    client:

  7.      client-id: sheep1

  8.      client-secret: 123456

  9.      user-authorization-uri: ${auth-server}/oauth/authorize

  10.      access-token-uri: ${auth-server}/oauth/token

  11.    resource:

  12.      jwt:

  13.        key-uri: ${auth-server}/oauth/token_key

这里几项配置都非常重要,都是需要和前面搭建的授权中心进行通信的

  • 创建测试控制器 TestController


  1. @RestController

  2. public class TestController {


  3.    @GetMapping("/normal")

  4.    @PreAuthorize("hasAuthority('ROLE_NORMAL')")

  5.    public String normal( ) {

  6.        return "normal permission test success !!!";

  7.    }


  8.    @GetMapping("/medium")

  9.    @PreAuthorize("hasAuthority('ROLE_MEDIUM')")

  10.    public String medium() {

  11.        return "medium permission test success !!!";

  12.    }


  13.    @GetMapping("/admin")

  14.    @PreAuthorize("hasAuthority('ROLE_ADMIN')")

  15.    public String admin() {

  16.        return "admin permission test success !!!";

  17.    }

  18. }

此测试控制器包含三个接口,分别需要三种权限( ROLE_NORMAL ROLE_MEDIUM ROLE_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/normal  localhost:8086/medium要求的接口权限,用户 codesheep均具备,所以能顺利访问,接下来再访问一下更高权限的接口: localhost:8086/admin

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

授权完成之后就可以顺利访问客户端2 ( codesheep-client2) 的接口:

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


未完待续

受篇幅所限,本文应该说实践了一下精简流程的: SSO单点登录和 JWT权限控制,还有很多可以复杂和具化的东西可以实现,比如:

  • 客户端 client凭据 和 用户 user的凭据可以用数据库进行统一管理

  • 认证 token也可以用数据库或缓存进行统一管理

  • 授权认证中心的统一登录页面可以自定义成需要的样子

  • 认证中心的授权页也可以自定义,甚至可以去掉

  • 包括一些异常提示也可以自定义

总之,尽情地折腾去吧!


后   记

由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!

个人网站:www.codesheep.cn (程序羊)

更多热文在此:


  ●  Spring Boot 系列实战文章合集(源码已开源)

  ●  程序员写简历时必须注意的技术词汇拼写

  ●  异步编程的几种方式

  ●  从一份配置清单详解Nginx服务器配置

  ●  一文上手 Elasticsearch常用可视化管理工具

  ●  Docker容器可视化监控中心搭建

  ●  利用ELK搭建Docker容器化应用日志中心

  ●  RPC框架实践之:Google gRPC

  ●  一文详解 Linux系统常用监控工具


作者更多 务实、能看懂、可复现的 技术文章尽在公众号 CodeSheep,欢迎扫码订阅,第一时间获取更新 ⬇️⬇️⬇️


©著作权归作者所有:来自51CTO博客作者mb5fdcae83766b7的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. 使用Python开发客户端和服务端程序 理解应用层协议和表示层
  2. 客户端用不着的数据结构之并查集
  3. 1.5 万 Star!界面酷炫、简单易用的数据库开源客户端
  4. 使用PHP来获取客户端和服务端IP
  5. php单点登录实现原理实例详解
  6. php实时推送系统消息给客户端的原理及详解
  7. 干掉Navicat!MySQL官方客户端到底行不行?
  8. Github标星 8K+,免费又好用的Redis客户端工具!

随机推荐

  1. 音视频转码合成
  2. Android常见的崩溃日志关键字分析 ( Andro
  3. ok6410 android driver(3)
  4. 修改xutils框架以支持fragment
  5. 浅析:为何到现在高端Android集成解决方案
  6. Android之如何学习
  7. 深入Android 【一】 —— 序及开篇
  8. TextView 在xml文件中的解释 android
  9. android service 生命周期
  10. Android M 新的运行时权限开发者需要知道