• SpringBoot+JWT实现单点登录解决方案


    一、什么是单点登录?

    单点登录是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的系统,不需要重新登录验证

    单点登录一般用于互相授信的系统,实现单一位置登录,其他信任的应用直接免登录的方式,在多个应用系统中,只需要登录一次,就可以访问其他互相信任的应用系统。

    随着时代的演进,大型web系统早已从单体应用架构发展为如今的多系统分布式应用群。但无论系统内部多么复杂,对用户而言,都是一个统一的整体,访问web系统的整个应用群要和访问单个系统一样,登录/注销只要一次就够了,不可能让一个用户在每个业务系统上都进行一次登录验证操作,这时就需要独立出一个单独的认证系统,它就是单点登录系统。

    二、单点登录的优点

    1.方便用户使用。用户不需要多次登录系统,不需要记住多个密码,方便用户操作。

    2.提高开发效率。单点登录为开发人员提供类一个通用的验证框架。

    3.简化管理。如果在应用程序中加入了单点登录的协议,管理用户账户的负担就会减轻。

    三、JWT 机制

    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. 返回结果。

    四.创建Maven父项目

     2.指定打包类型为pom

     五.创建认证模块sso

     1.添加依赖,完整的pom文件如下:

    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0modelVersion>
    5. <parent>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-parentartifactId>
    8. <version>2.7.13version>
    9. <relativePath/>
    10. parent>
    11. <groupId>com.examplegroupId>
    12. <artifactId>ssoartifactId>
    13. <version>0.0.1-SNAPSHOTversion>
    14. <name>ssoname>
    15. <description>ssodescription>
    16. <properties>
    17. <java.version>1.8java.version>
    18. properties>
    19. <dependencies>
    20. <dependency>
    21. <groupId>org.springframework.bootgroupId>
    22. <artifactId>spring-boot-starter-webartifactId>
    23. dependency>
    24. <dependency>
    25. <groupId>org.projectlombokgroupId>
    26. <artifactId>lombokartifactId>
    27. dependency>
    28. <dependency>
    29. <groupId>com.auth0groupId>
    30. <artifactId>java-jwtartifactId>
    31. <version>3.8.2version>
    32. dependency>
    33. <dependency>
    34. <groupId>org.springframework.bootgroupId>
    35. <artifactId>spring-boot-starter-thymeleafartifactId>
    36. dependency>
    37. <dependency>
    38. <groupId>org.springframework.bootgroupId>
    39. <artifactId>spring-boot-starter-testartifactId>
    40. <scope>testscope>
    41. dependency>
    42. dependencies>
    43. <build>
    44. <plugins>
    45. <plugin>
    46. <groupId>org.springframework.bootgroupId>
    47. <artifactId>spring-boot-maven-pluginartifactId>
    48. plugin>
    49. plugins>
    50. build>
    51. project>

    2添加jwt相关配置

     3.创建JWT配置类和JWT工具类

    示例代码如下:

    1. package com.example.sso.bean;
    2. import lombok.Getter;
    3. import lombok.Setter;
    4. import org.springframework.boot.context.properties.ConfigurationProperties;
    5. import org.springframework.stereotype.Component;
    6. /**
    7. * @author qx
    8. * @date 2023/7/4
    9. * @des Jwt配置类
    10. */
    11. @Component
    12. @ConfigurationProperties(prefix = "jwt")
    13. @Getter
    14. @Setter
    15. public class JwtProperties {
    16. /**
    17. * 过期时间-分钟
    18. */
    19. private Integer expireTime;
    20. /**
    21. * refreshToken时间
    22. */
    23. private Integer refreshTime;
    24. /**
    25. * 密钥
    26. */
    27. private String secret;
    28. }
    1. package com.example.sso.util;
    2. import com.auth0.jwt.JWT;
    3. import com.auth0.jwt.JWTVerifier;
    4. import com.auth0.jwt.algorithms.Algorithm;
    5. import com.auth0.jwt.interfaces.Claim;
    6. import com.auth0.jwt.interfaces.DecodedJWT;
    7. import com.example.sso.bean.JwtProperties;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.stereotype.Component;
    10. import java.util.Date;
    11. import java.util.HashMap;
    12. import java.util.Map;
    13. /**
    14. * @author qx
    15. * @date 2023/7/4
    16. * @des JWT工具类
    17. */
    18. @Component
    19. public class JwtUtil {
    20. @Autowired
    21. private JwtProperties jwtProperties;
    22. /**
    23. * 生成一个jwt字符串
    24. *
    25. * @param username 用户名
    26. * @return jwt字符串
    27. */
    28. public String sign(String username) {
    29. Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
    30. return JWT.create()
    31. // 设置过期时间1个小时
    32. .withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getExpireTime() * 60 * 1000))
    33. // 设置负载
    34. .withClaim("username", username).sign(algorithm);
    35. }
    36. /**
    37. * 生成refreshToken
    38. *
    39. * @param username 用户名
    40. * @return
    41. */
    42. public String refreshToken(String username) {
    43. Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
    44. return JWT.create()
    45. // 设置更新时间2个小时
    46. .withExpiresAt(new Date(System.currentTimeMillis() + jwtProperties.getRefreshTime() * 60 * 1000))
    47. // 设置负载
    48. .withClaim("username", username).sign(algorithm);
    49. }
    50. public static void main(String[] args) {
    51. Algorithm algorithm = Algorithm.HMAC256("KU5TjMO6zmh03bU3");
    52. String username = "admin";
    53. String token = JWT.create()
    54. // 设置过期时间1个小时
    55. .withExpiresAt(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
    56. // 设置负载
    57. .withClaim("username", username).sign(algorithm);
    58. System.out.println(token);
    59. }
    60. /**
    61. * 校验token是否正确
    62. *
    63. * @param token token值
    64. */
    65. public boolean verify(String token) {
    66. if (token == null || token.length() == 0) {
    67. throw new RuntimeException("token为空");
    68. }
    69. try {
    70. Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
    71. JWTVerifier jwtVerifier = JWT.require(algorithm).build();
    72. DecodedJWT decodedJWT = jwtVerifier.verify(token);
    73. Map map = decodedJWT.getClaims();
    74. System.out.println("claims:" + map.get("username").asString());
    75. return true;
    76. } catch (Exception e) {
    77. e.printStackTrace();
    78. return false;
    79. }
    80. }
    81. /**
    82. * 重新生成token和refreshToken
    83. *
    84. * @param refreshToken refreshToken
    85. * @return 返回token和refreshToken
    86. */
    87. public Map refreshJwt(String refreshToken) {
    88. if (refreshToken == null || refreshToken.length() == 0) {
    89. throw new RuntimeException("refreshToken为空");
    90. }
    91. Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getSecret());
    92. JWTVerifier jwtVerifier = JWT.require(algorithm).build();
    93. DecodedJWT decodedJWT = jwtVerifier.verify(refreshToken);
    94. Map map = decodedJWT.getClaims();
    95. // 获取用户名
    96. String username = map.get("username").asString();
    97. Map resultMap = new HashMap<>();
    98. // 重新生成token和refreshToken
    99. resultMap.put("token", sign(username));
    100. resultMap.put("refreshToken", refreshToken(username));
    101. return resultMap;
    102. }
    103. }

    4.创建服务层

    1. package com.example.sso.service;
    2. import com.example.sso.util.JwtUtil;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.stereotype.Service;
    5. import java.util.HashMap;
    6. import java.util.Map;
    7. /**
    8. * @author qx
    9. * @date 2023/7/4
    10. * @des 登录服务层
    11. */
    12. @Service
    13. public class LoginService {
    14. @Autowired
    15. private JwtUtil jwtUtil;
    16. /**
    17. * 登录
    18. *
    19. * @param username 用户名
    20. * @param password 密码
    21. * @return token值
    22. */
    23. public Map login(String username, String password) {
    24. Map map = new HashMap<>();
    25. if ("".equals(username) || "".equals(password)) {
    26. throw new RuntimeException("用户名或密码不能为空");
    27. }
    28. // 为了测试方便 不去数据库比较密码
    29. if ("123".equals(password)) {
    30. // 返回生成的token
    31. map.put("token", jwtUtil.sign(username));
    32. map.put("refreshToken", jwtUtil.refreshToken(username));
    33. }
    34. return map;
    35. }
    36. /**
    37. * 校验jwt是否成功
    38. *
    39. * @param token token值
    40. * @return 校验是否超过
    41. */
    42. public boolean checkJwt(String token) {
    43. return jwtUtil.verify(token);
    44. }
    45. /**
    46. * 重新生成token和refreshToken
    47. *
    48. * @param refreshToken refreshToken
    49. * @return token和refreshToken
    50. */
    51. public Map refreshJwt(String refreshToken) {
    52. return jwtUtil.refreshJwt(refreshToken);
    53. }
    54. }

    5.创建控制层

    1. package com.example.sso.controller;
    2. import com.example.sso.service.LoginService;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.stereotype.Controller;
    5. import org.springframework.ui.ModelMap;
    6. import org.springframework.web.bind.annotation.GetMapping;
    7. import org.springframework.web.bind.annotation.PostMapping;
    8. import org.springframework.web.bind.annotation.RequestMapping;
    9. import org.springframework.web.bind.annotation.ResponseBody;
    10. import java.util.Map;
    11. /**
    12. * @author qx
    13. * @date 2023/7/4
    14. * @des 验证控制层
    15. */
    16. @Controller
    17. @RequestMapping("/sso")
    18. public class AuthController {
    19. @Autowired
    20. private LoginService loginService;
    21. /**
    22. * 登录页面
    23. */
    24. @GetMapping("/login")
    25. public String toLogin() {
    26. return "login";
    27. }
    28. /**
    29. * 登录
    30. *
    31. * @param username 用户名
    32. * @param password 密码
    33. * @return token值
    34. */
    35. @PostMapping("/login")
    36. @ResponseBody
    37. public Map login(String username, String password) {
    38. return loginService.login(username, password);
    39. }
    40. /**
    41. * 验证jwt
    42. *
    43. * @param token token
    44. * @return 验证jwt是否合法
    45. */
    46. @RequestMapping("/checkJwt")
    47. @ResponseBody
    48. public boolean checkJwt(String token) {
    49. return loginService.checkJwt(token);
    50. }
    51. /**
    52. * 重新生成token和refreshToken
    53. *
    54. * @param refreshToken refreshToken
    55. * @return token和refreshToken
    56. */
    57. @RequestMapping("/refreshJwt")
    58. @ResponseBody
    59. public Map refreshJwt(String refreshToken) {
    60. return loginService.refreshJwt(refreshToken);
    61. }
    62. }

    6.创建一个登录页面login.html

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>登录title>
    6. head>
    7. <body>
    8. <form method="post" action="/sso/login">
    9. 用户名:<input type="text" name="username"/><br/>
    10. 密码:<input type="password" name="password"/><br/>
    11. <button type="submit">登录button>
    12. form>
    13. body>
    14. html>

    六、创建应用系统projectA 

     1.项目pom文件如下所示

    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    5. <modelVersion>4.0.0modelVersion>
    6. <parent>
    7. <groupId>org.examplegroupId>
    8. <artifactId>my-ssoartifactId>
    9. <version>1.0-SNAPSHOTversion>
    10. parent>
    11. <artifactId>projectAartifactId>
    12. <properties>
    13. <maven.compiler.source>8maven.compiler.source>
    14. <maven.compiler.target>8maven.compiler.target>
    15. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    16. properties>
    17. <dependencies>
    18. <dependency>
    19. <groupId>org.springframework.bootgroupId>
    20. <artifactId>spring-boot-starter-webartifactId>
    21. <version>2.7.13version>
    22. dependency>
    23. <dependency>
    24. <groupId>com.squareup.okhttp3groupId>
    25. <artifactId>okhttpartifactId>
    26. <version>4.10.0version>
    27. dependency>
    28. dependencies>
    29. project>

     2.修改配置文件

     3.创建过滤器

    1. package com.example.projectA.filter;
    2. import okhttp3.OkHttpClient;
    3. import okhttp3.Request;
    4. import okhttp3.Response;
    5. import org.springframework.beans.factory.annotation.Value;
    6. import org.springframework.stereotype.Component;
    7. import javax.servlet.*;
    8. import javax.servlet.annotation.WebFilter;
    9. import javax.servlet.http.HttpServletRequest;
    10. import javax.servlet.http.HttpServletResponse;
    11. import java.io.IOException;
    12. /**
    13. * @author qx
    14. * @date 2023/7/4
    15. * @des 登录过滤器
    16. */
    17. @Component
    18. @WebFilter(urlPatterns = "/**")
    19. public class LoginFilter implements Filter {
    20. @Value("${sso_server}")
    21. private String serverHost;
    22. @Override
    23. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    24. HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    25. String token = httpServletRequest.getParameter("token");
    26. if (this.check(token)) {
    27. filterChain.doFilter(servletRequest, servletResponse);
    28. } else {
    29. // token过期后再使用refreshToken处理
    30. String refreshToken = httpServletRequest.getHeader("refreshToken");
    31. if (check(refreshToken)) {
    32. // 重新生成token和refreshtoken给客户端保存 下次传递token参数的时候使用这个重新生成的
    33. System.out.println("更新后的token和refreshToken:" + refreshToken(refreshToken));
    34. filterChain.doFilter(servletRequest, servletResponse);
    35. }
    36. // 如果refreshToken也过期 那么需要重新登录
    37. HttpServletResponse response = (HttpServletResponse) servletResponse;
    38. String redirect = serverHost + "/login";
    39. response.sendRedirect(redirect);
    40. }
    41. }
    42. /**
    43. * 验证token
    44. *
    45. * @param token
    46. * @return
    47. * @throws IOException
    48. */
    49. private boolean check(String token) throws IOException {
    50. if (token == null || token.trim().length() == 0) {
    51. return false;
    52. }
    53. OkHttpClient client = new OkHttpClient();
    54. // 请求验证token的合法性
    55. String url = serverHost + "/checkJwt?token=" + token;
    56. Request request = new Request.Builder().url(url).build();
    57. Response response = client.newCall(request).execute();
    58. return Boolean.parseBoolean(response.body().string());
    59. }
    60. /**
    61. * 重新获取token和refreshToken
    62. *
    63. * @param refreshToken
    64. * @return
    65. * @throws IOException
    66. */
    67. private String refreshToken(String refreshToken) throws IOException {
    68. if (refreshToken == null || refreshToken.trim().length() == 0) {
    69. return null;
    70. }
    71. OkHttpClient client = new OkHttpClient();
    72. // 请求重新获取token和refreshToken
    73. String url = serverHost + "/refreshJwt?refreshToken=" + refreshToken;
    74. Request request = new Request.Builder().url(url).build();
    75. Response response = client.newCall(request).execute();
    76. return response.body().string();
    77. }
    78. }

    4.创建测试控制层

    1. package com.example.projectA.controller;
    2. import org.springframework.web.bind.annotation.GetMapping;
    3. import org.springframework.web.bind.annotation.RestController;
    4. /**
    5. * @author qx
    6. * @date 2023/7/4
    7. * @des 测试A
    8. */
    9. @RestController
    10. public class IndexController {
    11. @GetMapping("/testA")
    12. public String testA() {
    13. return "输出testA";
    14. }
    15. }

    5.启动类

    1. package com.example.projectA;
    2. import org.springframework.boot.SpringApplication;
    3. import org.springframework.boot.autoconfigure.SpringBootApplication;
    4. import org.springframework.boot.web.servlet.ServletComponentScan;
    5. /**
    6. * @author qx
    7. * @date 2023/7/4
    8. * @des Projecta启动类
    9. */
    10. @SpringBootApplication
    11. @ServletComponentScan
    12. public class ProjectaApplication {
    13. public static void main(String[] args) {
    14. SpringApplication.run(ProjectaApplication.class,args);
    15. }
    16. }

    6.启动项目测试

    我们访问http://localhost:8081/testA 系统跳转到了验证模块的登录页面

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

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

     我们看到正确获取到了数据。

    当我们的token过期之后,我们使用refreshToken重新获取到新的token和refreshToken。

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

    一般的我们的refreshToken时间设置要比token的过期时间要长。

    客户端登录后,将 token和refreshToken 保存在客户端本地,每次访问将 token 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果 refreshToken 有效,服务端就生成新的 accessToken 给客户端。否则,客户端就重新登录即可。

    七、创建应用系统projectB

    我们再次模仿projectA创建projectB子模块。

     

     启动模块B

    我们直接测试带上token参数

    通过之前的token,无需登录即可成功进入了应用系统B。说明我们的单点登录系统搭建成功。

  • 相关阅读:
    【Flutter系列】第一期——初识Dart语言
    OpenHarmony自定义组件的生命周期
    VCS 和 SCM
    Excel 插入和提取超链接
    Linux驱动开发笔记(四):设备驱动介绍、熟悉杂项设备驱动和ubuntu开发杂项设备Demo
    01-10-Hadoop-HA-概述
    【C++】类和对象 从入门到超神 (上)
    JavaScript中获取屏幕,窗口和网页大小
    408真题-2021
    js+vue,前端关于页面滚动让头部菜单淡入淡出实现原理
  • 原文地址:https://blog.csdn.net/qinxun2008081/article/details/131533908