springboot+Oauth2.0实现单点登录
本篇文章描述用springboot3.5.2+Oauth2.0实现单点登录的过程。
1、单点登录原理于流程
详细了解单点登录原理于流程请参照文章:单点登录。
个人理解:
单点登录即用户在访问网站资源时,在多个系统间来回跳转,不需要重复登录,在门户网站上实现一次登录即可。
例如学校的综合平台(门户网站),在学校的综合平台上可以跳转到多个其他的子系统中如教务系统。教务系统这个子系统在访问时是需要对用户进行身份认证的,自由学校的学生和教职工才可访问。但是如果我们登录学校的综合平台,可以直接从综合平台上跳转教务系统,不需要重复的登录。这就是单点登录。
请看下图
下面将详细描述上图单点登录流程,涉及成员(用户、客户端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为测试主页面
启动类
认证服务器又作资源服务器需要加上注解@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
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做用户认证
1112没有得到用户认证授权码,重定向1111端口(认证服务器)请求授权码
1111端口获取授权码没有用户权限,重定向到登录界面获取用户权限
最后登录成功可以获取1112端口hello的资源。(访问1113一样)
2、访问工程three的index资源
访问1111的index资源,没有用户权限,重定向登录页面获取用户权限
用户登录后获取资源
点击按钮 前往客户端一,实现跳转页面,客户端1先要获取用户登录认证,重定向到登录获取用户认证
没有用户授权码,请求用户授权码
请求授权码,1111端口给出用户认证凭证,返回给客户端一,客户端一拿到用户认证凭证,用户获取资源
跳转客户端二同理。