基于 Spring Security Oauth2 的 SSO 单点登录+JWT 权限控制实践 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hansonwang99
V2EX    Blogger

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

  •  
  •   hansonwang99 2019-05-07 07:37:51 +08:00 8060 次点击
    这是一个创建于 2416 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Profile


    概 述

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

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


    理论知识

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

    • 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中需要加入相关依赖如下:

    <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 即无需登录_而是直接跳到授权页]( https://raw.githubusercontent.com/hansonwang99/pic/master/springbt_sso_jwt/由于用户已通过客户端 1 登录过_因此再访问客户端 2 即无需登录_而是直接跳到授权页.png)

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

    ![顺利访问客户端 2 的接口]( https://raw.githubusercontent.com/hansonwang99/pic/master/springbt_sso_jwt/顺利访问客户端 2 的接口.png)

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


    未完待续

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

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

    总之,尽情地折腾去吧!

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


    写在最后

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

    2 条回复    2019-09-12 09:07:43 +08:00
    bxb100
        1
    bxb100  
       2019-05-07 09:19:04 +08:00 via Android
    正好在看 spring oa,mark 一下~
    chexie
        2
    chexie  
       2019-09-12 09:07:43 +08:00
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     909 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 21:05 PVG 05:05 LAX 13:05 JFK 16:05
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86