springboot+Oauth2.0实现单点登录

1,875 阅读8分钟

springboot+Oauth2.0实现单点登录

本篇文章描述用springboot3.5.2+Oauth2.0实现单点登录的过程。

1、单点登录原理于流程

详细了解单点登录原理于流程请参照文章:单点登录

个人理解:

单点登录即用户在访问网站资源时,在多个系统间来回跳转,不需要重复登录,在门户网站上实现一次登录即可。

例如学校的综合平台(门户网站),在学校的综合平台上可以跳转到多个其他的子系统中如教务系统。教务系统这个子系统在访问时是需要对用户进行身份认证的,自由学校的学生和教职工才可访问。但是如果我们登录学校的综合平台,可以直接从综合平台上跳转教务系统,不需要重复的登录。这就是单点登录。

请看下图

image.png 下面将详细描述上图单点登录流程,涉及成员(用户、客户端1(门户网站)、客户端2、认证服务器、资源服务器、数据库、缓存数据库)

1、在用户第一次访问客户端1时(门户网站),客户端1要认证用户角色,用户必须先注册授权的用户角色,用户在客户端1申请注册,客户端1提交用户注册的请求到认证服务器,认证服务器提供注册的表单,接收用户注册信息并把注册信息存入到数据库。

2、用户注册成功后进行登录操作,客户端1提交用户登录请求,认证服务器先于数据库中查询用户登录记录判断用户是否已登录,没有登录,提供登录表单,接收用户登录信息,再查询数据库中是否有相匹配的用户角色,匹配成功则获得用户登录认证,生成授权码返回给客户端1。

3、客户端1利用授权码申请token,认证服务器生成token并保存于缓存数据库中,返回token给客户端1,客户端1利用token申请获取用户信息,资源服务器认证用户token,成功后返回用户信息给客户端1。(完成登录)

4、当用户在客户端1上申请访问客户端2时,客户端2提交用户登录请求到认证服务器,认证服务器查询用户是否已登录,发现用户已登录状态,获得或用登录认证,直接生成授权码返回给客户端2,客户端2执行上述的第3步操作,实现用户跳转页面。

上述是用户单点登录实现的业务流程,下面直接上代码。

2、实现代码

再次声明springboot版本为2.5.3,下面将以三个工程实现单点登录

工程three既作为认证服务器又作为资源服务器

工程four、five分别作为客户端1、客户端2

1、工程three(端口1111)

目录结构

CustomAuthentication为弃用类。

AuthServerConfig为配置客户端详细信息类

SecurityConfig为资源限制/放行类

IndexController为测试返回主页面类

UserController为返回用户登录信息类

login.html为登录静态资源

index.html为测试主页面

image.png

启动类

认证服务器又作资源服务器需要加上注解@EnableResourceServer

@EnableResourceServer
@SpringBootApplication
public class ThreeApplication {

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

}

pom文件

引入thymeleaf方便测试 springboot版本2.5.3

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>three</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>three</name>
    <description>three</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.3.5.RELEASE</version>
        </dependency>
        <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.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

AuthServerConfig类

配置客户端详细信息,针对自己项目设置

/**
 * 授权服务器
 * 配置客户端详细信息
 */
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory() //使用内存存储客户端信息。
                .withClient("javaboy") //客户端名。
                .secret(passwordEncoder.encode("123")) //设置客户端的密钥。密钥是使用密码编码器(passwordEncoder)加密的。
                .autoApprove(true) // 设置客户端自动授权,即跳过用户授权页面,自动同意授权请求。
                // 注册两个重定向URI,用于客户端应用程序进行授权过程中的重定向。
                .redirectUris("http://localhost:1112/login", "http://localhost:1113/login")
                .scopes("user") // 设置授权范围为"user",即客户端可以访问用户的信息。
                .accessTokenValiditySeconds(3600) // 设置访问令牌的有效期为(1小时)。
                .authorizedGrantTypes("authorization_code"); // 设置授权类型为"authorization_code",表示使用授权码模式进行授权。

    }
}

SecurityConfig类

用户信息内存存储写死的方法,放行静态资源和资源请求限制

@Configuration
@Order(1)  //因为资源服务器和授权服务器在一起,所以我们需要一个 @Order 注解来提升 Spring Security 配置的优先级。
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 静态资源放行
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/login.html", "/css/**", "/js/**", "/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requestMatchers() //用于配置请求匹配规则。
                //设置允许所有用户访问的策略
                .antMatchers("/login") //指定 "/login" 路径的请求不需要进行认证,允许所有用户访问此路径。
                /**
                 * 当访问"/index"路径时,Spring Security会检查用户是否已经进行了认证。
                 * 如果用户已经认证,则会允许用户继续访问"/index"页面。
                 * 如果用户尚未认证,则会将用户重定向到登录页面,要求用户提供有效的凭据进行身份验证。
                 *
                 * 对于其他未在.antMatchers中指定的路径,通过默认的配置.anyRequest().authenticated(),
                 * Spring Security将要求对这些资源进行    完全的   身份验证。
                 * 这意味着用户必须提供有效的认证凭据才能访问这些资源,否则会被拒绝访问。
                 */
                .antMatchers("/index")
                .antMatchers("/oauth/authorize") //指定 "/oauth/authorize" 路径的请求不需要进行认证
                .and()
                .authorizeRequests().anyRequest().authenticated() //对除了前面指定的路径之外的所有请求进行身份认证。
                .and()
                .formLogin() //配置表单登录。
                .loginPage("/login.html") //指定登录页面的路径为 "/login.html",用户访问受限页面时将被重定向到该登录页面。
                .loginProcessingUrl("/login") //指定登录表单提交的URL为 "/login",即登录时将发送POST请求到该URL。
                .and()
                .logout().permitAll()  //启用注销功能,并允许所有用户访问注销接口。
                .and()
                .csrf().disable(); //禁用跨站请求伪造(CSRF)保护。
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //inMemoryAuthentication()方法,表示使用基于内存的方式进行用户认证。
        auth.inMemoryAuthentication()
                .withUser("sang")
                //encode("123")使用密码编码器对密码进行加密,防止以明文存储密码。
                .password(passwordEncoder().encode("123"))
                .roles("admin");
    }
}

IndexController类

返回index.html页面

@Controller
public class IndexController {
    @GetMapping("/index")
    public String index(Model model) {
        return "index";
    }
}

UserController类

返回用户信息

/**
 * 暴露用户信息的接口
 */
@RestController
public class UserController {
    @GetMapping("/user")
    public Principal getCurrentUser(Principal principal) {
        return principal;
    }
}

login界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>统一认证中心</title>
</head>
<body>
<form action="/login" method="post">
    <div class="input">
        <label for="name">用户名</label>
        <input type="text" name="username" id="name">
        <span class="spin"></span>
    </div>
    <div class="input">
        <label for="pass">密码</label>
        <input type="password" name="password" id="pass">
        <span class="spin"></span>
    </div>
    <div class="button login">
        <button type="submit">
            <span>登录</span>
            <i class="fa fa-check"></i>
        </button>
    </div>
</form>
</body>
</html>

测试主页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>认证服务器主页面</title>
</head>
<body>
    <div style="margin: auto">
        <h1>模拟门户网站</h1>
        <div style="padding: 30px">
            <button id="clint-one">前往客户端一</button>
            <button id="clint-two">前往客户端二</button>
        </div>
    </div>
</body>
<script>
    let clintOne = document.getElementById("clint-one");
    clintOne.addEventListener("click", function() {
        window.location.href = "http://localhost:1112/hello";
    });
    let clintTwo = document.getElementById("clint-two");
    clintTwo.addEventListener("click", function() {
        window.location.href = "http://localhost:1113/welcome";
    });
</script>
</html>

2、工程four(端口1112、five端口1113)

案例工程four和工程five完全一样,可以自行编写测试页面

目录结构

SecurityConfig类 开启单点登录配置

HelloController类返回hello页面,测试使用

application.properties配置登录地址、cookie image.png

pom文件

于工程three相同

SecurityConfig类

/**
 * 单点登录拦截器
 * 对请求进行授权限制
 */
@Configuration
@EnableOAuth2Sso  //开启单点登录功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();
    }
}

HelloController类

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model) {
        // Authentication表示身份验证的结果 --> 获取当前用户的身份验证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        model.addAttribute("UserName", authentication.getName());
        return "hello";
    }
}

hello页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>客户端一主页面</title>
</head>
<body>
    <div style="margin: auto">
      <h1>模拟客户端一 -- Hello</h1>
        <div>尊贵的 “<span th:text="${UserName}"></span>” 用户,欢迎来到客户端一主页面</div>
    </div>
</body>
</html>

application.properties配置

# 端口
server.port=1112

# 配置
# 客户端密钥
security.oauth2.client.client-secret=123

# 客户端client-id
security.oauth2.client.client-id=javaboy

# 用户授权码接口
security.oauth2.client.user-authorization-uri=http://localhost:1111/oauth/authorize

# 获取令牌接口
security.oauth2.client.access-token-uri=http://localhost:1111/oauth/token

# 获取用户信息接口 ( 从资源服务器获取 )
security.oauth2.resource.user-info-uri=http://localhost:1111/user


# cookie命名
server.servlet.session.cookie.name=s1

# cookie最大存活时间 ( 1小时 )
server.servlet.session.cookie.max-age=3600

# cookie只能服务端访问
server.servlet.session.cookie.http-only=true

3、测试

开启三个工程。

1、首先访问工程four的hello资源

访问1112的hello资源,没有用户认证,重定向到1112/login做用户认证 image.png

1112没有得到用户认证授权码,重定向1111端口(认证服务器)请求授权码 image.png

1111端口获取授权码没有用户权限,重定向到登录界面获取用户权限 image.png 最后登录成功可以获取1112端口hello的资源。(访问1113一样)

2、访问工程three的index资源

访问1111的index资源,没有用户权限,重定向登录页面获取用户权限 image.png

用户登录后获取资源 image.png

点击按钮 前往客户端一,实现跳转页面,客户端1先要获取用户登录认证,重定向到登录获取用户认证 image.png

没有用户授权码,请求用户授权码 image.png

请求授权码,1111端口给出用户认证凭证,返回给客户端一,客户端一拿到用户认证凭证,用户获取资源 image.png

跳转客户端二同理。