美文网首页spring.io
基于Spring Security Oauth2的SSO单点登录

基于Spring Security Oauth2的SSO单点登录

作者: Kyriez7 | 来源:发表于2022-10-24 10:39 被阅读0次

    理论知识

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

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

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><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>
    </pre>

    项目结构如下:

    image.png

    项目结构


    授权认证中心搭建

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

    • pom中添加依赖

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"><dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    </dependencies>
    </pre>

    • 项目 yml配置文件:

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">server:
    port: 8085
    servlet:
    context-path: /uac
    </pre>

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

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

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@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"));
    }
    }
    </pre>

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

    • 认证服务器配置 AuthorizationServerConfig

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@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;
    }
    }
    </pre>

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

    • Spring Security安全配置 SpringSecurityConfig

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@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());
    }
    }
    </pre>


    客户端应用创建和配置

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

    • SSO客户端应用配置类 ClientWebsecurityConfigurer

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @EnableOAuth2Sso
    public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/**").authorizeRequests()
    .anyRequest().authenticated();
    }
    }
    </pre>

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

    • application.yml配置

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">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
    </pre>

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

    • 创建测试控制器 TestController

    <pre style="box-sizing: border-box; font-family: monospace; font-size: 18px; margin: 20px 0px; padding: 15px; border: 0px; background-color: rgb(244, 245, 246); white-space: pre-wrap; word-break: break-all; color: rgb(34, 34, 34); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@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 !!!";
    }
    }
    </pre>

    此测试控制器包含三个接口,分别需要三种权限(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

    image.png

    自动跳转到授权中心统一登录页面

    输入用户名 codesheep,密码 123456,即可登录认证,并进入授权页面:

    image.png

    授权页面

    同意授权后,会自动返回之前客户端的测试接口:

    image.png

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

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

    image.png

    直接访问

    由于 localhost:8086/normal 和 localhost:8086/medium要求的接口权限,用户codesheep均具备,所以能顺利访问,接下来再访问一下更高权限的接口:localhost:8086/admin:

    image.png

    无权限访问

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

    image.png

    由于用户已通过客户端1登录过因此再访问客户端2即无需登录而是直接跳到授权页

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

    image.png

    顺利访问客户端2的接口

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

    相关文章

      网友评论

        本文标题:基于Spring Security Oauth2的SSO单点登录

        本文链接:https://www.haomeiwen.com/subject/kqxgzrtx.html