单点登录是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的系统,不需要重新登录验证。
单点登录一般用于互相授信的系统,实现单一位置登录,其他信任的应用直接免登录的方式,在多个应用系统中,只需要登录一次,就可以访问其他互相信任的应用系统。
随着时代的演进,大型web系统早已从单体应用架构发展为如今的多系统分布式应用群。但无论系统内部多么复杂,对用户而言,都是一个统一的整体,访问web系统的整个应用群要和访问单个系统一样,登录/注销只要一次就够了,不可能让一个用户在每个业务系统上都进行一次登录验证操作,这时就需要独立出一个单独的认证系统,它就是单点登录系统。
1.方便用户使用。用户不需要多次登录系统,不需要记住多个密码,方便用户操作。
2.提高开发效率。单点登录为开发人员提供类一个通用的验证框架。
3.简化管理。如果在应用程序中加入了单点登录的协议,管理用户账户的负担就会减轻。
JWT(JSON Web Token)它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证JWTToken的正确性,只要正确就通过验证。
数据结构:
JWT包含三个部分:Header头部,Payload负载和Signature签名。三个部门用“.”分割。校验也是JWT内部自己实现的 ,并且可以将你存储时候的信息从token中取出来无须查库。
JWT执行流程:
JWT的请求流程也特别简单,首先使用账号登录获取Token,然后后面的各种请求,都带上这个Token即可。具体流程如下:
1. 客户端发起登录请求,传入账号密码;
2. 服务端使用私钥创建一个Token;
3. 服务器返回Token给客户端;
4. 客户端向服务端发送请求,在请求头中携带Token;
5. 服务器验证该Token;
6. 返回结果。


2.指定打包类型为pom


1.添加依赖,完整的pom文件如下:
- "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.0modelVersion>
- <parent>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-parentartifactId>
- <version>2.7.13version>
- <relativePath/>
- parent>
- <groupId>com.examplegroupId>
- <artifactId>ssoartifactId>
- <version>0.0.1-SNAPSHOTversion>
- <name>ssoname>
- <description>ssodescription>
- <properties>
- <java.version>1.8java.version>
- properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
-
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- dependency>
-
- <dependency>
- <groupId>com.auth0groupId>
- <artifactId>java-jwtartifactId>
- <version>3.8.2version>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-thymeleafartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-testartifactId>
- <scope>testscope>
- dependency>
- dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-maven-pluginartifactId>
- plugin>
- plugins>
- build>
-
- project>
2添加jwt相关配置

3.创建JWT配置类和JWT工具类
示例代码如下:
- package com.example.sso.bean;
-
- import lombok.Getter;
- import lombok.Setter;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import org.springframework.stereotype.Component;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des Jwt配置类
- */
- @Component
- @ConfigurationProperties(prefix = "jwt")
- @Getter
- @Setter
- public class JwtProperties {
-
- /**
- * 过期时间-分钟
- */
- private Integer expireTime;
-
- /**
- * refreshToken时间
- */
- private Integer refreshTime;
-
- /**
- * 密钥
- */
- private String secret;
- }
- package com.example.sso.util;
-
- import com.auth0.jwt.JWT;
- import com.auth0.jwt.JWTVerifier;
- import com.auth0.jwt.algorithms.Algorithm;
- import com.auth0.jwt.interfaces.Claim;
- import com.auth0.jwt.interfaces.DecodedJWT;
- import com.example.sso.bean.JwtProperties;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
-
- import java.util.Date;
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des JWT工具类
- */
- @Component
- public class JwtUtil {
-
- @Autowired
- private JwtProperties jwtProperties;
-
- /**
- * 生成一个jwt字符串
- *
- * @param username 用户名
- * @return jwt字符串
- */
- public String sign(String username) {
- Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
- return JWT.create()
- // 设置过期时间1个小时
- .withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpireTime() * 60 * 1000))
- // 设置负载
- .withClaim("username", username).sign(algorithm);
- }
-
- /**
- * 生成refreshToken
- *
- * @param username 用户名
- * @return
- */
- public String refreshToken(String username) {
- Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
- return JWT.create()
- // 设置更新时间2个小时
- .withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getRefreshTime() * 60 * 1000))
- // 设置负载
- .withClaim("username", username).sign(algorithm);
- }
-
-
- public static void main(String[] args) {
- Algorithm algorithm = Algorithm.HMAC256("KU5TjMO6zmh03bU3");
- String username = "admin";
- String token = JWT.create()
- // 设置过期时间1个小时
- .withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
- // 设置负载
- .withClaim("username", username).sign(algorithm);
- System.out.println(token);
- }
-
- /**
- * 校验token是否正确
- *
- * @param token token值
- */
- public boolean verify(String token) {
- if (token == null || token.length() == 0) {
- throw new RuntimeException("token为空");
- }
- try {
- Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
- JWTVerifier jwtVerifier = JWT.require(algorithm).build();
- DecodedJWT decodedJWT = jwtVerifier.verify(token);
- Map
map = decodedJWT.getClaims(); - System.out.println("claims:" + map.get("username").asString());
- return true;
- } catch (Exception e) {
- e.printStackTrace();
- return false;
- }
-
- }
-
- /**
- * 重新生成token和refreshToken
- *
- * @param refreshToken refreshToken
- * @return 返回token和refreshToken
- */
- public Map
refreshJwt(String refreshToken) { - if (refreshToken == null || refreshToken.length() == 0) {
- throw new RuntimeException("refreshToken为空");
- }
- Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
- JWTVerifier jwtVerifier = JWT.require(algorithm).build();
- DecodedJWT decodedJWT = jwtVerifier.verify(refreshToken);
- Map
map = decodedJWT.getClaims(); - // 获取用户名
- String username = map.get("username").asString();
- Map
resultMap = new HashMap<>(); - // 重新生成token和refreshToken
- resultMap.put("token", sign(username));
- resultMap.put("refreshToken", refreshToken(username));
- return resultMap;
- }
- }
4.创建服务层
- package com.example.sso.service;
-
- import com.example.sso.util.JwtUtil;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- import java.util.HashMap;
- import java.util.Map;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des 登录服务层
- */
- @Service
- public class LoginService {
-
-
- @Autowired
- private JwtUtil jwtUtil;
-
- /**
- * 登录
- *
- * @param username 用户名
- * @param password 密码
- * @return token值
- */
- public Map
login(String username, String password) { - Map
map = new HashMap<>(); - if ("".equals(username) || "".equals(password)) {
- throw new RuntimeException("用户名或密码不能为空");
- }
- // 为了测试方便 不去数据库比较密码
- if ("123".equals(password)) {
- // 返回生成的token
- map.put("token", jwtUtil.sign(username));
- map.put("refreshToken", jwtUtil.refreshToken(username));
- }
- return map;
- }
-
- /**
- * 校验jwt是否成功
- *
- * @param token token值
- * @return 校验是否超过
- */
- public boolean checkJwt(String token) {
- return jwtUtil.verify(token);
- }
-
- /**
- * 重新生成token和refreshToken
- *
- * @param refreshToken refreshToken
- * @return token和refreshToken
- */
- public Map
refreshJwt(String refreshToken) { - return jwtUtil.refreshJwt(refreshToken);
- }
- }
5.创建控制层
- package com.example.sso.controller;
-
- import com.example.sso.service.LoginService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
-
- import java.util.Map;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des 验证控制层
- */
- @Controller
- @RequestMapping("/sso")
- public class AuthController {
-
- @Autowired
- private LoginService loginService;
-
- /**
- * 登录页面
- */
- @GetMapping("/login")
- public String toLogin() {
- return "login";
- }
-
- /**
- * 登录
- *
- * @param username 用户名
- * @param password 密码
- * @return token值
- */
- @PostMapping("/login")
- @ResponseBody
- public Map
login(String username, String password) { - return loginService.login(username, password);
- }
-
- /**
- * 验证jwt
- *
- * @param token token
- * @return 验证jwt是否合法
- */
- @RequestMapping("/checkJwt")
- @ResponseBody
- public boolean checkJwt(String token) {
- return loginService.checkJwt(token);
- }
-
-
- /**
- * 重新生成token和refreshToken
- *
- * @param refreshToken refreshToken
- * @return token和refreshToken
- */
- @RequestMapping("/refreshJwt")
- @ResponseBody
- public Map
refreshJwt(String refreshToken) { - return loginService.refreshJwt(refreshToken);
- }
-
- }
6.创建一个登录页面login.html
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>登录title>
- head>
- <body>
- <form method="post" action="/sso/login">
- 用户名:<input type="text" name="username"/><br/>
- 密码:<input type="password" name="password"/><br/>
- <button type="submit">登录button>
- form>
- body>
- html>

- "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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0modelVersion>
- <parent>
- <groupId>org.examplegroupId>
- <artifactId>my-ssoartifactId>
- <version>1.0-SNAPSHOTversion>
- parent>
-
- <artifactId>projectAartifactId>
-
- <properties>
- <maven.compiler.source>8maven.compiler.source>
- <maven.compiler.target>8maven.compiler.target>
- <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
- properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- <version>2.7.13version>
- dependency>
-
- <dependency>
- <groupId>com.squareup.okhttp3groupId>
- <artifactId>okhttpartifactId>
- <version>4.10.0version>
- dependency>
- dependencies>
- project>
2.修改配置文件

3.创建过滤器
- package com.example.projectA.filter;
-
- import okhttp3.OkHttpClient;
- import okhttp3.Request;
- import okhttp3.Response;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Component;
-
- import javax.servlet.*;
- import javax.servlet.annotation.WebFilter;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des 登录过滤器
- */
- @Component
- @WebFilter(urlPatterns = "/**")
- public class LoginFilter implements Filter {
-
- @Value("${sso_server}")
- private String serverHost;
-
- @Override
- public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
- HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
- String token = httpServletRequest.getParameter("token");
- if (this.check(token)) {
- filterChain.doFilter(servletRequest, servletResponse);
- } else {
- // token过期后再使用refreshToken处理
- String refreshToken = httpServletRequest.getHeader("refreshToken");
- if (check(refreshToken)) {
- // 重新生成token和refreshtoken给客户端保存 下次传递token参数的时候使用这个重新生成的
- System.out.println("更新后的token和refreshToken:" + refreshToken(refreshToken));
- filterChain.doFilter(servletRequest, servletResponse);
- }
- // 如果refreshToken也过期 那么需要重新登录
- HttpServletResponse response = (HttpServletResponse) servletResponse;
- String redirect = serverHost + "/login";
- response.sendRedirect(redirect);
- }
- }
-
- /**
- * 验证token
- *
- * @param token
- * @return
- * @throws IOException
- */
- private boolean check(String token) throws IOException {
- if (token == null || token.trim().length() == 0) {
- return false;
- }
- OkHttpClient client = new OkHttpClient();
- // 请求验证token的合法性
- String url = serverHost + "/checkJwt?token=" + token;
- Request request = new Request.Builder().url(url).build();
- Response response = client.newCall(request).execute();
- return Boolean.parseBoolean(response.body().string());
- }
-
- /**
- * 重新获取token和refreshToken
- *
- * @param refreshToken
- * @return
- * @throws IOException
- */
- private String refreshToken(String refreshToken) throws IOException {
- if (refreshToken == null || refreshToken.trim().length() == 0) {
- return null;
- }
- OkHttpClient client = new OkHttpClient();
- // 请求重新获取token和refreshToken
- String url = serverHost + "/refreshJwt?refreshToken=" + refreshToken;
- Request request = new Request.Builder().url(url).build();
- Response response = client.newCall(request).execute();
- return response.body().string();
-
- }
- }
- package com.example.projectA.controller;
-
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des 测试A
- */
- @RestController
- public class IndexController {
-
- @GetMapping("/testA")
- public String testA() {
- return "输出testA";
- }
- }
- package com.example.projectA;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.boot.web.servlet.ServletComponentScan;
-
- /**
- * @author qx
- * @date 2023/7/4
- * @des Projecta启动类
- */
- @SpringBootApplication
- @ServletComponentScan
- public class ProjectaApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(ProjectaApplication.class,args);
- }
- }
我们访问http://localhost:8081/testA 系统跳转到了验证模块的登录页面

我们输入账号密码登录成功后返回token

如何我们复制这段数据,把数据传递到token参数。

我们看到正确获取到了数据。
当我们的token过期之后,我们使用refreshToken重新获取到新的token和refreshToken。


由于我们的项目中refreshToken的更新时间为2分钟,我们等这个时间过期之后我们再次请求接口,那么这个时候就要去重新登录了。

一般的我们的refreshToken时间设置要比token的过期时间要长。
客户端登录后,将 token和refreshToken 保存在客户端本地,每次访问将 token 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果 refreshToken 有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。
我们再次模仿projectA创建projectB子模块。



启动模块B
我们直接测试带上token参数
通过之前的token,无需登录即可成功进入了应用系统B。说明我们的单点登录系统搭建成功。