基于SpringSecurity OAuth2实现单点登录——入门示例和流程分析

1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》

一、入门示例

1、模块规划

  为了模拟单点登录,我们创建了了授权服务、资源服务、应用A、应用B四个模块,其中授权服务和资源服务在实际项目中可以考虑合并为一,这里为了学习,没有进行合并。

  • qriver-auth-server 授权服务 端口号 8080
  • qriver-user-server 资源服务 端口号 8081
  • qriver-client-app 应用A 端口号 8082
  • qriver-client-app2 应用B 端口号 8083

在这里插入图片描述

2、授权服务搭建
2.1、依赖

  在授权服务中,核心的依赖有SpringSecurity、Oauth2等,如下所示:

<dependencies>
    <!-- <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-oauth2</artifactId>
     </dependency>
 </dependencies>
2.2、TokenStore 配置

  主要用来配置AccessToken的存储方式,这里选择了内存方式,即InMemoryTokenStore,实际工作中可以选择Redis、数据库等方式。

@Configuration
public class AccessTokenConfig {
    /**
     * 配置Token存储方式
     * @return
     */
    @Bean
    TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
}
2.3、SpringSecurity 相关配置

  这里主要是进行SpringSecurity 的配置,我们配置了加密方式、创建了两个基于内存的用户。

@Configuration
public class QriverSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码加密器
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    public UserDetailsService userDetailsServiceBean() throws Exception {
        InMemoryUserDetailsManager mg = new InMemoryUserDetailsManager();
        mg.createUser(User.withUsername("user").password(passwordEncoder().encode("123456")).roles("USER").build());
        mg.createUser(User.withUsername("admin").password(passwordEncoder().encode("123456")).roles("USER","ADMIN").build());
        return mg;
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());

    }
    /**
     * 配置 过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().formLogin();
    }
}
2.4、授权服务 相关配置

  这里是配置授权服务需要的配置。首先,我们需要在配置类上添加@EnableAuthorizationServer注解,启动授权服务自动加载等配置内容。

  然后,配置AuthorizationServerTokenServices相关内容,实际上就是注入DefaultTokenServices(AuthorizationServerTokenServices的实现类)对象,并设置一些对应的参数。

  然后,配置授权码的存储方式,这里选择了基于内存的InMemoryAuthorizationCodeServices方式。

  然后,把前面配置的AuthorizationServerTokenServices、AuthorizationCodeServices配置到授权服务中,即重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法。

  再,配置令牌端点的安全约束,即通过重写configure(AuthorizationServerSecurityConfigurer security)实现。

  最后,配置客户端的相关信息,通过重写configure(ClientDetailsServiceConfigurer clients)方法实现,实际项目中该配置一般是持久化到数据库的。

注意:其实,授权服务器的配置不止这些,我们可以根据自己的需求进行配置,这里的配置主要是为了实现单点登录,且尽量简化的进行的配置。

  完整的配置如下:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 配置 Token 的一些基本信息
     * @return
     */
    @Bean
    AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService);//配置客户端校验方式
        services.setReuseRefreshToken(true);//设置Token是否支持刷新
        services.setTokenStore(tokenStore);//设置Token的存储位置
        services.setAccessTokenValiditySeconds(60 * 60 * 2);//设置Token有效期
        services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);//设置Token刷新有效期
        return services;
    }

    /**
     * 配置令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()")//Token 校验的端点,后续客户端验证Token使用
                .allowFormAuthenticationForClients()
                .passwordEncoder(passwordEncoder);
    }

    /**
     * 配置客户端的详细信息,客户端信息可以存储数据库、内存等地方
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //clientId,resourceIds,scopes,grantTypes,authorities
        clients.inMemory().withClient("client1")//配置clientId,唯一标识,表示客户端,即第三方应用
                .secret(new BCryptPasswordEncoder().encode("123456"))//客户端访问密码
                .autoApprove(true)
               // .resourceIds("res-1")//客户端所能访问的资源id集合
                //客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔
                .authorizedGrantTypes("authorization_code","refresh_token")
                //客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔
                .scopes("all")
                //客户端的重定向URI
                .redirectUris("http://localhost:8082/login")
              .and()
                .withClient("client2")
                .secret(new BCryptPasswordEncoder().encode("123456"))//客户端访问密码
                .autoApprove(true)
               // .resourceIds("res-2")//客户端所能访问的资源id集合
                //客户端支持的grant_type,可选值包括authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个grant_type用逗号(,)分隔
                .authorizedGrantTypes("authorization_code","refresh_token")
                //客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔
                .scopes("all")
                //客户端的重定向URI
                .redirectUris("http://localhost:8083/login")
                 .and()
                .withClient("resource1")
                .secret(new BCryptPasswordEncoder().encode("123456"));
    }

    /**
     * 配置令牌的访问端点和令牌服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authorizationCodeServices(authorizationCodeServices())
                .tokenServices(tokenServices());
    }

    /**
     * 配置授权码的存储
     * @return
     */
    @Bean
    AuthorizationCodeServices authorizationCodeServices(){
        return new InMemoryAuthorizationCodeServices();
    }
}
2.5、其他

  在授权服务器搭建过程中,还可以自定义统一登录界面、配置服务器的端口号等内容,我们这里准守尽量简单的原则,使用较简单的配置先实现单点登录的示例,其中也省略了启动配置类等内容,完整的代码请看文章最底部,进行下载。

3、资源服务

  资源服务器,主要为第三方应用提供了获取用户信息的接口。首先,我们需要添加资源服务器所需的依赖,如下:

<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

  然后,我们再进行资源服务配置,首先该类需要继承ResourceServerConfigurerAdapter 类,然后添加@EnableResourceServer注解启动资源服务,最后再配置一个RemoteTokenServices ,主要是因为授权服务和资源服务分离,所以需要配置,代码如下:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    /**
     * 因为资源服务器和授权服务器是分开的,所以需要配置一个验证Token的远程地址
     * @return
     */
    @Bean
    RemoteTokenServices tokenServices() {
        RemoteTokenServices services = new RemoteTokenServices();
        services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        services.setClientId("resource1");
        services.setClientSecret("123456");
        return services;
    }
}

  最后,再提供一个获取用户信息的地址,地址由用户自定义,后续在应用A、应用B中配置路径即可。

@RestController
public class UserController {
    @GetMapping("/user")
    public Principal getCurrentUser(Principal principal) {
        return principal;
    }
}

启动器类、配置文件相关代码未展示,可以下载完整代码查看。

4、应用A、应用B

  应用A、应用B代码基本一样,我们以应用A为例进行。

  首先,引入依赖,如下:

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-thymeleaf</artifactId>
 </dependency>

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-oauth2</artifactId>
 </dependency>

  然后,修改配置文件(application.properties):

#端口
server.port=8082
#配置客户端client-id,用于授权服务器端的验证
security.oauth2.client.client-id=client1
#配置客户端client-secret
security.oauth2.client.client-secret=123456
#授权服务器获取授权的地址,用于获取code
security.oauth2.client.user-authorization-uri=http://localhost:8080/oauth/authorize
#获取token
security.oauth2.client.access-token-uri=http://localhost:8080/oauth/token
#通过资源服务器,获取用户信息
security.oauth2.resource.user-info-uri=http://localhost:8081/user

server.servlet.session.cookie.name=client1

  然后,配置启动类,这里我们把单点登录的@EnableOAuth2Sso注解直接配置到了启动类上,同时提供了一个RestTemplate 实例注入到了Spring容器中,代码如下:

@SpringBootApplication
@EnableOAuth2Sso
public class QriverClientAppApplication {

    public static void main(String[] args) {
        SpringApplication.run(QriverClientAppApplication.class, args);
    }
    @Bean
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

  然后,添加了一个测试用的接口,如下:

@Controller
public class IndexController {

    @GetMapping("/index")
    public String index(Model model) {
        model.addAttribute("msg","欢迎," +
                SecurityContextHolder.getContext().getAuthentication().getName() + ",登录系统A!");
        return "index";
    }
}

  最后,实现一个简单的前端页面,我们引入了thymeleaf依赖,为了简化配置,我们按照thymeleaf默认路径添加一个html页面,即在resources目录下创建templates目录,然后把index.html放到templates目录下,这样就可以使用thymeleaf的默认配置了。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>系统A - 首页</title>
</head>
<body>
<h1>系统A</h1>
<p th:text="${msg}"></p>
<a href="http://localhost:8083/index">跳转系统B</a>
</body>
</html>
5、测试

  完成上述配置后,我们依次启动授权服务、资源服务、应用A、应用B(没有顺序要求),然后访问应用A的地址http://localhost:8082/index,然后就会跳转到登录界面,然后输入用户名密码,就可以看到了我们应用A中对应的页面,然后点击跳转链接,就可以跳转到应用B,而这时不需要在输入用户名密码。

二、流程分析

   1>、第一次访问应用A(http://localhost:8082/index),因为未登录,经过Spring Security过滤器,会重定向到应用A的登录http://localhost:8082/login。
在这里插入图片描述
   2>、因为我们启用了应用A的单点登录功能(即在启动类上增加了@EnableOAuth2Sso注解),当我们访问应用A的登录http://localhost:8082/login时,经过单点登录的过滤器,会重定向到授权服务的http://localhost:8080/oauth/authorize地址上。
在这里插入图片描述
   3>、因为访问授权服务的http://localhost:8080/oauth/authorize地址同样需要登录,所以这个时候,还是会被Spring Security过滤器拦截,并重定向到授权服务的登录地址http://localhost:8080/login上。需要注意的是,第一步中重定向到的是应用A的登录界面,这个时候重定向到了的是授权服务的登录界面。
在这里插入图片描述
   4>、然后输入用户名密码,点击"登录"按钮。这个时候,会请求http://localhost:8080/login登录接口,需要注意该接口是POST请求,即登录验证接口。如果验证通过(用户名密码正确),则会重定向到http://localhost:8080/oauth/authorize?client_id=client1&redirect_uri=http://localhost:8082/login&response_type=code&state=dGRQzM地址上。
在这里插入图片描述
   5>、访问http://localhost:8080/oauth/authorize?xxx地址时,又重定向到了应用A的登录地址http://localhost:8082/login?code=ax9jCC&state=dGRQzM,这个时候,和第一次访问应用A的登录相比,多了code参数。
在这里插入图片描述
   6>、访问应用A的登录地址http://localhost:8082/login?code=ax9jCC&state=dGRQzM,这个时候通过携带的code,换取到access_token,然后经过SpringSecurity处理,最终重定向到了应用A的访问地址http://localhost:8082/index。
在这里插入图片描述
在这里插入图片描述

写在最后

  上述示例的完整源码,请移步查看下载:https://gitee.com/hsh2015/qriver-cloud-learning/tree/master/qriver-security-oauth

  这篇内容主要旨在记录单点登录示例的搭建和从前端视角了解浏览器和服务端的交互过程,后续我们将从源码级别分析这些重定向是如何实现的,最后是如何通过授权服务、资源服务器实现授权的。

  • 18
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
Spring Security OAuth2可以实现单点登录功能。通过引入相关的jar包,可以在Spring Boot项目中使用注解来实现单点登录客户端的功能。具体步骤如下: 1. 创建一个单点登录客户端工程,并引入以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </dependency> ``` 2. 在项目中使用注解来配置单点登录客户端的相关信息,例如授权服务器的URL、客户端ID和密钥等。 需要注意的是,根据Spring Security官方的最新推荐,Spring Security OAuth2项目已经不再推荐使用,而是将OAuth2的相关功能抽取出来,集成在Spring Security中,并单独新建了spring-authorization-server项目来实现授权服务器的功能。因此,如果需要实现授权服务器的功能,可以使用spring-authorization-server项目。 总结来说,Spring Security OAuth2可以实现单点登录功能,但是根据最新的推荐,建议使用Spring Security和spring-authorization-server来实现授权服务器的功能。\[1\]\[2\] #### 引用[.reference_title] - *1* *2* [SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统](https://blog.csdn.net/wmz1932/article/details/124719588)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Spring Security OAuth2 单点登录](https://blog.csdn.net/weixin_42073629/article/details/115436378)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姠惢荇者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值