SSO单点登录系统搭建(附源码)

👨‍💻作者简介:全干的java博主

🎟️个人主页:无所谓^_^

 ps:点赞是免费的,却可以让写博客的作者开心好几天😎

目录

前言

一、项目介绍

1.项目下载:

2.系统架构:

3.直白话 OAuth 2 流程

二、创建工程

1.父工程依赖

2.认证服务器和资源服务器的依赖

三、认证服务器实现

1.启动类和application.yml配置

2.WebSecurityConfig安全配置类

3.CustomUserDetailsService

4.AuthorizationServerConfig认证服务器配置

5.TokenConfig(JWT配置类)

四、资源服务器实现

1.启动类和application.yml配置

2.ResourceServerConfig资源服务器配置类

                     3.统一结果返回类

                     4.需要授权才能访问的接口

五、测试

1.授权码方式

2.密码授权模式

小结


前言

为什么要做这个系统:单点登录(Single Sign-On,简称SSO)是一种用户认证和授权的技术,它允许用户在多个应用系统中使用同一组凭据(用户名和密码)登录,避免了用户需要多次输入凭据的麻烦,提高了用户体验。比如阿里的淘宝和天猫,显然是两个系统,但是你只要登录了淘宝,天猫就不需要登录了。

本文将介绍如何使用Spring Security和OAuth2协议搭建一个简单的单点登录系统。

项目下载

gitee:https://gitee.com/wusupweilgy/springboot-vue.git(点个star呀😎)

一、项目介绍

1.系统架构:

本系统采用OAuth2的授权码和密码模式,主要包括以下几个组件:

  • 认证服务器(Authorization Server):负责颁发访问令牌(Access Token),验证客户端身份和用户身份。
  • 资源服务器(Resource Server):提供受保护的资源,需要验证访问令牌。
  • 客户端(Client):需要访问受保护的资源,需要获取访问令牌。
  • 用户(User):系统的最终用户,需要登录并授权客户端访问受保护的资源。

在这贴上JustAuth官方的流程图,方便大家理解

2.直白话 OAuth 2 流程

以上流程理解起来可能有些难度,这儿我们给出一个白话版的流程图

首先引入三个角色:

  • 用户A:可以理解成你自己
  • 网站B:可以理解成 OSChina
  • 第三方C:可以理解成 Github

在我们搭建的系统中,Github就相当于第三方授权系统,也就是认证系统,网站B我们可以先理解成资源服务器,因为用户A要访问网站B的资源,但是网站B说必须Github登录才能访问我的资源。不知道这样说大家能否理解一点点,不理解的话,只能怪我文采不好了😶‍🌫️。

二、创建工程

1.父工程依赖

这里就不教大家怎么创建工程了,因为我默认大家不是小白了😉,所以就贴上依赖了。一定要注意,创建的工程都是maven工程,不是创建springboot工程。因为创建springboot工程的话idea不能继承,还需要手动改pom.xml文件。

2.认证服务器和资源服务器的依赖

<dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
</dependencies>

这里我们要创建两个模块,依赖都是一样的。子工程创建完后,看看自己的子工程pom.xml有没有继承父工程,父工程的pom.xml有没有添加子工程

子工程:

 <parent>
        <artifactId>security_oauth_parent</artifactId>
        <groupId>com.wusuowei</groupId>
        <version>1.0-SNAPSHOT</version>
</parent>

父工程:

<modules>
        <module>security_oauth_server</module>
        <module>security_oauth_resouce</module>
</modules>

三、认证服务器实现

1.启动类和application.yml配置

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class OauthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication.class, args);
    }

}
server:
  port: 9000
  servlet:
    context-path: /auth

2.WebSecurityConfig安全配置类

主要配置了Security的认证方式和密码编码器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

    /**
     * password密码模式需要使用此认证管理器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

3.CustomUserDetailsService

用于从数据库或其他数据源中获取用户信息,以便进行身份认证和授权。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    //这里写死的,也可以从数据库查询
    @Override
    public UserDetails loadUserByUsername(String u) throws UsernameNotFoundException {
        return new User("admin", passwordEncoder.encode("1234"),
                AuthorityUtils.commaSeparatedStringToAuthorityList("product1"));
    }
}

4.AuthorizationServerConfig认证服务器配置

每一行我都进行了注释,当然import和@Autowired没有注释哈😂。这里主要是配置了认证服务器的令牌存储方式切换为 JWT模式。

import com.wusuowei.oauthserver.service.impl.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

@Configuration
@EnableAuthorizationServer//开启认证服务器功能
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private TokenStore tokenStore;

    /**  配置被允许访问此认证服务器的客户端详情信息
     * 方式1:内存方式管理
     * 方式2:数据库管理
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用内存方式
        clients.inMemory()
                // 客户端id
                .withClient("wj-pc")
                // 客户端密码,要加密,不然一直要求登录
                .secret(passwordEncoder.encode("wj-secret"))
                // 资源id, 如商品资源
                .resourceIds("product-server")
                // 授权类型, 可同时支持多种授权类型
                .authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
                // 授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
                .scopes("all")
                // false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码,
                .autoApprove(false)
                .redirectUris("http://www.baidu.com/");// 客户端回调地址
    }

    /**
     * 重写父类的方法
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //密码模式需要设置此认证管理器
        endpoints.authenticationManager(authenticationManager);
        // 刷新令牌获取新令牌时需要
        endpoints.userDetailsService(customUserDetailsService);
        //设置token存储策略
        endpoints.tokenStore(tokenStore).accessTokenConverter(jwtAccessTokenConverter);
        //授权码管理策略,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
        //endpoints.authorizationCodeServices(authorizationCodeServices());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //所有人可以访问/oauth/token_key后面获取公钥,默认拒绝访问
        security.tokenKeyAccess("permitAll()");
        //认证后可访问/oauth/check_token,默认拒绝访问
        security.checkTokenAccess("isAuthenticated()");
    }
}

5.TokenConfig(JWT配置类)

要配置签名密钥,因为jwt要靠它进行解析,验证JWT的准确性,是否被篡改。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {

    @Bean
    public TokenStore tokenStore(){
        //jwt管理令牌
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    // JWT 签名秘钥
    private static final String SIGNING_KEY = "wj-key";

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }
}

四、资源服务器实现

  

资源服务器的搭建就比认证服务器的搭建简单许多了,但是博客还是要贴很多代码,没办法,主要是为了读者大大们都能成功搭建这个系统,所以就。。。😶

1.启动类和application.yml配置

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;

@EnableOAuth2Sso//启用单点登录(SSO)功能
@SpringBootApplication
public class OauthResourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OauthResourceApplication.class, args);
    }

}
server:
  port: 8000
  servlet:
    context-path: /product

 #oauth2的认证 appid appsecret
sso-server-url: http://localhost:9000
security:
  oauth2:
    client:
      client-id: wj-pc
      client-secret: wj-secret
      #认证服务器接受认证的接口地址,这里的地址和端口要与认证服务器中的配置一致
      #user-authorization-uri: ${sso-server-url}/oauth/authorize
      #根据授权码去请求令牌的地址
      #access-token-uri: ${sso-server-url}/oauth/token
    resource:
      jwt:
        # 从认证服务器中获取JWT的密钥地址,验证token时使用,系统启动时会初始化,不会每次验证都请求
        key-uri: ${sso-server-url}/auth/oauth/token_key

2.ResourceServerConfig资源服务器配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
// 标识为资源服务器, 所有发往当前服务的请求,都会去请求头里找token,找不到或验证不通过不允许访问
@EnableResourceServer
//开启方法级别权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //配置当前资源服务器的ID
    private static final String RESOURCE_ID = "product-server";


    /**当前资源服务器的一些配置, 如资源服务器ID **/
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 配置当前资源服务器的ID, 会在认证服务器验证(客户端表的resources配置了就可以访问这个服务)
        resources.resourceId(RESOURCE_ID);
                //TODO 可以把配置文件的security配置整个去掉,也要把启动类的@EnableOAuth2Sso注解去掉,不然会报错
                // 在本地配置一个TokenConfig,跟认证服务器一样的类,进行本地token校验
                //.tokenStore(tokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                //不创建session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //资源授权规则
                .authorizeRequests().anyRequest().permitAll();
              //  .antMatchers("/product/**").hasAuthority("product")
                //所有的请求对应访问的用户都要有all范围的权限
             //   .antMatchers("/**").access("#oauth2.hasScope('all')");
    }
}

3.统一结果返回类

import java.util.HashMap;
import java.util.Map;

/**
 * 返回数据
 *
 * @author Mark sunlightcs@gmail.com
 */
public class R extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    public R setData(Object data) {
        put("data",data);
        return this;
    }

    public R() {
        put("code", 200);
        put("msg", "success");
    }

    public static R error() {
        return error(500, "未知异常,请联系管理员");
    }

    public static R error(String msg) {
        return error(500, msg);
    }

    public static R error(int code, String msg) {
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R ok(String msg) {
        R r = new R();
        r.put("msg", msg);
        return r;
    }

    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R ok() {
        return new R();
    }

    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }
    public  Integer getCode() {

        return (Integer) this.get("code");
    }

}

4.需要授权才能访问的接口

为了后面进行测试,只有携带token令牌才能访问这个接口

import com.wusuowei.oauthresource.utils.R;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class ProductController {

    @GetMapping("/list")
    @PreAuthorize("hasAuthority('product1')")
    public R list(Authentication authentication) {
        List<String> list = new ArrayList<>();
        list.add("huawei");
        list.add("vivo");
        list.add("oppo");
        System.out.println(authentication);
        return R.ok().setData(list);
    }
}

五、测试

1.授权码方式

就好比前面打的比方,用户A想访问网站B,但是网站B说必须登录后才允许访问,所以网站B提供了一个GitHub的第三方授权登录链接,用户A点击后,跳转到GitHub的登录页,填写GitHub的账号密码,然后点击授权,GitHub就会返回给网站B一个授权码,网站B再通过这个授权码访问GitHub获取token的接口,获取令牌。优点啰嗦了,哈哈,就是希望大家能理解这个模式,因为微信的扫码登录也是这个模式。

接下来开始测试,发送请求获取授权码code。

访问:http://localhost:9000/auth/oauth/authorize?client_id=wj-pc&response_type=code

这里的client_id是在AuthorizationServerConfig中配置的。

输入账号密码进行登陆,账号和密码:admin/1234 ,是在CustomUserDetailsService中配置的

登陆成功后,选择Approve,点击Authorize,这里跳转到www.baidu.com ,并且后面携带了code,这里的code就是授权码,后面我们就可以通过授权码来获取令牌(access_token)

通过授权码获取令牌

我使用的是Apifox工具测试(用postman也一样):http://localhost:9000/auth/oauth/token,具体的参数配置如下,这里的grant_type是authorization_code,code是上一步获取的code

这里的username和password是在AuthorizationServerConfig中配置的

 发送请求后,成功获得了access_token

 注意,code只能获取一次access_token,获取后就会失效,第二次获取就会失败

2.密码授权模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己在服务提供商(认证服务器)上的用户名和密码,然后客户端通过用户提供的用户名和密码向服务提供商(认证服务器)获取令牌。

如果用户名和密码遗漏,服务提供商(认证服务器)无法判断客户端提交的用户和密码是否盗取来的,那意味着令牌就可随时获取,数据被丢失。

所以密码授权模式适用于产品都是企业内部的,用户名密码共享不要紧。如果是第三方这种不太适合。也适用手机APP提交用户名密码。

访问:localhost:9000/auth/oauth/token

填上请求参数:

 

 发送请求即可获取到access_token:

 篇幅有限,这里就介绍这两种常用的模式了,其他模式不常用就不演示了

最后一项测试就是拿着令牌去访问我们

资源服务器的资源了

访问:http://localhost:8000/product/list,请求头参数一定要写对Authorization,值为bearer后面加一个空格,再加上你的access_token令牌

 

小结

本文介绍了如何使用Spring Security和OAuth2协议搭建一个简单的单点登录系统。。希望本文可以帮助你进一步学习和掌握这两个技术。如果这篇文章有不足的地方希望大家多多指出,如果有幸帮助到你,希望读者大大们可以给作者点个赞呀😶‍🌫️😶‍🌫️

  • 28
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
单点登录(Single Sign-On, SSO)是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统Java 可以通过集成 SSO 框架实现单点登录功能。下面介绍一种常见的 SSO 框架 CAS(Central Authentication Service)的实现方式: 1. 安装和配置 CAS 服务器 CAS 服务器是一个单点登录认证服务器,需要先安装和配置。可以从官网下载最新的 CAS 服务器,然后按照官方文档进行安装和配置。 2. 集成 CAS 客户端 在需要实现单点登录的应用系统中,集成 CAS 客户端。CAS 客户端是一个 Java Web 应用,需要在应用系统中添加 CAS 客户端的 jar 包,并在 web.xml 中配置 CAS 客户端的过滤器。 3. 修改应用系统登录模块 在应用系统的登录模块中,添加 CAS 客户端的登录验证逻辑。当用户访问应用系统时,如果没有登录,就会被 CAS 客户端拦截,跳转到 CAS 服务器进行登录认证。如果用户已经在其他应用系统中登录过,CAS 服务器会直接返回认证成功的结果,用户就可以访问当前应用系统。 4. 集成单点注销功能 当用户在一个应用系统中注销登录时,需要通知 CAS 服务器,让 CAS 服务器也将用户注销。这样用户在其他应用系统中访问时,就会被 CAS 客户端拦截,跳转到 CAS 服务器进行重新登录认证。 以上是 Java 实现单点登录的一种常见方式。在实际应用中,还需要考虑安全性、性能等因素,进行更加严谨的设计和实现。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java学长小李

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

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

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

打赏作者

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

抵扣说明:

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

余额充值