SSO单点登录

SSO单点登录

1. 什么是单点登录?

​ 指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的系统。简而言之,多个系统,统一登陆。

2. 为什么需要做单点登录系统呢?

​ 在一些互联网公司中,公司旗下可能会有多个子系统,每个登陆实现统一管理,多个账户信息统一管理 SSO单点登陆认证授权系统。比如阿里系的淘宝和天猫,显而易见这是两个系统,但是在使用过程中,只要你登录了淘宝,同时也意味着登录了天猫,如果每个子系统都需要登录认证,用户早就疯了,所以我们要解决的问题就是,用户只需要登录一次就可以访问所有相互信任的应用系统。

3. 使用token实现

​ 1. 在项目某个模块进行登录,服务端登录之后,按照规则生成字符串,把登陆之后用户包含到生成字符串里面,把字符串返回

​ (1)可以把字符串通过cookie返回

​ (2)把字符串通过地址栏返回

2.再去访问项目其他模块,每次访问在地址栏带着生成的字符串,在访问模块里面获取地址字符串,根据字符串获取用户信息。如果可以获取到就能登录。

单点登录一般使用jwt技术实现,jwt是一个加密技术,用于生成token。
在这里插入图片描述

4.Token身份认证

​ 使用基于Token的身份认证验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

​ (1)客户端使用用户名、密码请求登录

​ (2)服务端收到请求,去验证用户名、密码

​ (3)验证成功后,服务端会签发一个Token,再把这个Token发送给客户端

​ (4)客户端收到Token以后可以把它存储起来,比如放在Cookie里或者Local Storage(浏览器缓存)。

​ (5)客户端每次向服务端请求资源的时候需要带着服务端签发的Token

​ (6)服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端发送请求得数据

使用token的优势

​ 无状态、可扩展

​ 在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务传到其他服务器上。

安全性

​ 请求中发送token而不是发送cookie能够防止CSRF(跨域请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。

5. JWT(JSON Web Token)机制

​ JWT是一种紧凑自包含的,用于在多方传递JSON对象的技术。传递的数据可以使用数字签名增加其安全性。可以使用RSA公钥/私钥加密方式。

​ 紧凑:数据小,可以通过URL,POST参数,请求头发送。且数据小代表传输速度快。

​ 自包含:使用payload数据块记录用户必要且不隐私的数据,可以有效的减少数据库访问次数,提高代码性能。

​ JWT一般用于处理用户身份验证数据信息交换

​ 用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为他的开销很小,并且能够轻松地跨不同的域使用。

​ 数据信息交换:JWT是一种非常方便的多方传递数据的载体,因为其可以使用数据前面来保证数据的有效性和安全性。

官网:JSON Web Tokens - jwt.io

5.1 JWT数据结构

JWT的数据结构是:A.B.C。 由字符点 '.'来分隔三部分数据。

A - header 头信息

B - payload (有效荷载)

C - Signature 签名

5.1.1 header

数据结构:{“alg”:“加密算法名称”,“type”:“JWT”}

alg是加密算法定义内容,如:HMAC SHA256 或 RSA

type是token类型,这里固定为 JWT。

5.1.2 playload

​ 在payload数据块中一般用于记录实体(通常为用户信息)或其他数据的。主要分为三个部分,分别是:已注册信息(registered claims),公开数据(public claims),私有数据(private claims)。

​ payload中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。前面列举的都是已注册信息。

​ 公开数据部分一般都会在JWT注册表中增加定义。避免和已注册信息冲突。

​ 公开数据和私有数据可以由程序员任意定义。

​ 注意:即使JWT有签名加密机制,但是payload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能。不推荐在payload中记录任何敏感数据。

5.1.3 Signature

​ 签名信息。这是一个由开发者提供的信息,是服务器验证的传递的数据是否有效安全的标准。在生成JWT最终数据之前。先使用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head。加密后的payload。在使用相同的加密算法,对加密后的数据和签名进行加密。最终得到结果。

5.2 JWT执行流程

img

简单来说,单点登录就是在多个系统中,用户只需要一次登录,各个系统即可感知该用户已经登录。

单点登录的流程:

用户登录—>登录验证—>根据用户信息生成token—>响应token给页面—>前端将token放入cookie

校验:将cookie信息放在请求头—>对token进行验证—>得到用户信息

介绍完了单点登录,下面用代码实现。完整代码如下。

项目结构图:

在这里插入图片描述

pom文件

<?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.7.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pan</groupId>
    <artifactId>springboot-jwt-redis-sso</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-jwt-redis-sso</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <!--jwt起步依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>

        <!--MyBatis-Plus代码生成器需要的依赖,开始-->
        <!-- 持久层 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!--模板引擎-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.1</version>
        </dependency>
        <!--MyBatis-Plus代码生成器需要的依赖,结束-->
        
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.41</version>
        </dependency>

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

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

</project>

yml文件

server:
  port: 8081
#数据库
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
    username: root
    password: 123456
  #redis
  redis:
    database: 4
    host: 192.168.45.128
    port: 6379
    password: 123456
    jedis:
      pool:
        max-active: 100
        max-idle: 10
        max-wait: 100000
    timeout: 5000
#mybatis-plus
mybatis-plus:
  mapper-locations: classpath:mapper/*Mapper.xml

AuthenticationInterceptor 拦截器

package com.pan.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.pan.pojo.TUser;
import com.pan.util.IpUtils;
import com.pan.util.RedisUtil;
import com.pan.vo.PassToken;
import com.pan.vo.RedisPreEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @PackageName: com.pan.config
 * @ClassName: AuthenticationInterceptor 拦截器
 * @author: zhangpan
 * @data: 2023/3/16 11:40
 */
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        String token = request.getHeader("token");// 从 http 请求头中取出 token
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        if (token == null) {
            throw new RuntimeException("无token,请重新登录");
        }
        String userId;
        try {
            userId = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            throw new Exception("token无效");
        }
        TUser user = null;
        String key = RedisPreEnum.JWT_TOKEN_PRE.getPre() + userId + IpUtils.getIpAddr(request);
        if (redisUtil.hasKey(key)) {
            Object o = redisUtil.get(key);
            user = JSONObject.toJavaObject((JSON) JSON.toJSON(o), TUser.class);
        }
        if (user == null) {
            throw new RuntimeException("请重新登录");
        }
        // 验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            jwtVerifier.verify(token);
        } catch (Exception e) {
            throw new Exception("token无效");
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }


}

全局拦截器:InterceptorConfig

package com.pan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @PackageName: com.pan.config
 * @ClassName: InterceptorConfig 全局拦截器
 * @author: zhangpan
 * @data: 2023/3/16 11:32
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }

}

RedisConfig

package com.pan.config;

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @PackageName: com.pan.config
 * @ClassName: RedisConfig
 * @author: zhangpan
 * @data: 2023/3/16 11:30
 */
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        //使用fastjson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean(StringRedisTemplate.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

TUserController

package com.pan.controller;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.pan.pojo.TUser;
import com.pan.service.TUserService;
import com.pan.service.TokenService;
import com.pan.util.IpUtils;
import com.pan.util.RedisUtil;
import com.pan.vo.PassToken;
import com.pan.vo.RedisPreEnum;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author zhangpan
 * @since 2023-03-16
 */
@RestController
@RequestMapping("/t-user")
public class TUserController {
    @Resource
    private TUserService userService;
    @Resource
    TokenService tokenService;
    @Resource
    private RedisUtil redisUtil;

    @PassToken
    @PostMapping("login")
    public Object login(@RequestBody TUser user,  HttpServletRequest request) {
        JSONObject jsonObject = new JSONObject();
        //根据用户查询用户信息
        TUser tUser = userService.getOne(new QueryWrapper<TUser>().eq("username", user.getUsername()));
        String ipAddr = IpUtils.getIpAddr(request);
        if (tUser == null) {
            jsonObject.put("message","登陆失败,用户不存在。");
            return jsonObject;
        } else {
            if (!tUser.getPassword().equals(user.getPassword())) {
                jsonObject.put("message","登陆失败,密码不正确。");
                return jsonObject;
            } else {
                System.out.println("登陆成功。。。。。。。。。。。。。。。。。");
                String token = tokenService.getToken(tUser);
                String key = RedisPreEnum.JWT_TOKEN_PRE.getPre()+ tUser.getId();
                Set<String> keys = redisUtil.keys(key + "*");
                if (CollectionUtils.isEmpty(keys)) {
                    redisUtil.set(key + ipAddr, tUser, RedisPreEnum.JWT_TOKEN_PRE.getExpired());
                } else {
                    //清空之前的key
                    for (String k : keys) {
                        redisUtil.del(k);
                    }
                    //重新设置key
                    redisUtil.set(key + ipAddr, tUser, RedisPreEnum.JWT_TOKEN_PRE.getExpired());
                }
                jsonObject.put("token",token);
                jsonObject.put("user",tUser);
                return jsonObject;
            }
        }
    }

    @GetMapping("getMessage")
    public String getMessage() {
        return "你已经通过验证。";
    }

}

mapper接口

package com.pan.mapper;

import com.pan.pojo.TUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author zhangpan
 * @since 2023-03-16
 */
public interface TUserMapper extends BaseMapper<TUser> {

}

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pan.mapper.TUserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.pan.pojo.TUser">
        <id column="id" property="id" />
        <result column="username" property="username" />
        <result column="password" property="password" />
        <result column="salt" property="salt" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, username, password, salt
    </sql>

</mapper>

实体类

package com.pan.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 
 * </p>
 *
 * @author zhangpan
 * @since 2023-03-16
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TUser implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String username;

    private String password;

    private String salt;

}

service接口

package com.pan.service;

import com.pan.pojo.TUser;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author zhangpan
 * @since 2023-03-16
 */
public interface TUserService extends IService<TUser> {

}
package com.pan.service.impl;

import com.pan.pojo.TUser;
import com.pan.mapper.TUserMapper;
import com.pan.service.TUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author zhangpan
 * @since 2023-03-16
 */
@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {

}

TokenService

package com.pan.service;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.pan.pojo.TUser;
import org.springframework.stereotype.Component;

import java.util.Calendar;

/**
 * @PackageName: com.pan.service
 * @ClassName: TokenService
 * @author: zhangpan
 * @data: 2023/3/16 11:57
 */
@Component
public class TokenService {
    private final static String SIGN = "";

    public String getToken(TUser user) {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,1);
        String token="";
        token= JWT.create().withAudience(String.valueOf(user.getId()))
                .withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }

    public String verifyToken(String token){
        return null;
    }

}

IpUtils

package com.pan.util;

import javax.servlet.http.HttpServletRequest;

/**
 * @PackageName: com.pan.util
 * @ClassName: IpUtils
 * @author: zhangpan
 * @data: 2023/3/16 11:49
 */
public class IpUtils {
    //客户端类型  手机、电脑、平板
    //UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("user-agent"));
    //String clientType = userAgent.getOperatingSystem().getDeviceType().toString();
    //操作系统类型
    //String os = userAgent.getOperatingSystem().getName();
    //请求ip
    //String ip = IpUtils.getIpAddr(request);
    //浏览器类型
    //String browser = userAgent.getBrowser().toString();
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
            //LOGGER.error("X-Real-IP:"+ip);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("http_client_ip");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        // 如果是多级代理,那么取第一个ip为客户ip
        if (ip != null && ip.indexOf(",") != -1) {
            ip = ip.substring(ip.lastIndexOf(",") + 1, ip.length()).trim();
        }
        return ip;
    }

}

RedisUtil

package com.pan.util;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @PackageName: com.pan.util
 * @ClassName: RedisUtil
 * @author: zhangpan
 * @data: 2023/3/16 11:44
 */
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public Set<String> keys(String keys){
        try {
            return redisTemplate.keys(keys);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入, 不存在放入,存在返回
     * @param key 键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean setnx(String key, Object value) {
        try {
            redisTemplate.opsForValue().setIfAbsent(key,value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间,不存在放入,存在返回
     * @param key 键
     * @param value 值
     * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean setnx(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     * @param key 键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     * @param key 键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    /**
     * HashGet
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     * @param key 键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     * @param key 键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param by 要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param by 要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     * @param key 键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     * @param key 键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================
    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     * @param key 键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

}

自定义PassToken注解

package com.pan.vo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * PassToken注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

枚举

package com.pan.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * RedisPreEnum枚举类
 */
@AllArgsConstructor
@Getter
public enum RedisPreEnum {
    JWT_TOKEN_PRE("JWT_TOKEN_","token前缀",60*60*24);
    private String pre;
    private String desc;
    private Integer expired;
}

启动控制类

package com.pan;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @PackageName: com.pan
 * @ClassName: ssoApplication
 * @author: zhangpan
 * @data: 2023/3/15 15:24
 */
@SpringBootApplication
@MapperScan("com.pan.mapper")
public class ssoApplication {
    public static void main(String[] args) {
        SpringApplication.run(ssoApplication.class,args);
    }
}

测试效果

登录

在这里插入图片描述

查看token

在这里插入图片描述

token已经存在缓存中了。

验证

在头部信息中添加token
在这里插入图片描述

到此已经验证了单点登录。

接下来验证同一个账号只能在一台设备上登录,由于本地测试,获取到的ip地址都是127.0.0.1。
这里使用postman在请求头添加x-forwarded-for字段,模拟ip地址

用一个账号,模拟用不同ip地址登录,看看在redis当中是什么样的
第一次登录:

使用新用户名和密码

{"username":"q1","password":"123"}

在这里插入图片描述

redis中的token

在这里插入图片描述

访问localhost:8081/t-user/getMessage

在头部信息中添加token 进行访问

第二次登录

修改ip为192.168.2.2

登录后查看redis中的token

在这里插入图片描述

然后重新刷新第二次登录后访问的访问localhost:8081/t-user/getMessage

会提示你重新登录。这样就实现了一个账号只能在一台设备上登录了。

登录时,判断该账号是否在其它设备登录,如果有,就把key清除,然后存储用户信息和ip地址拼接为key,存储在redis当中
在拦截器当中(用户每次接口请求都会经过该拦截器),去获取这个key,如果key没有,直接返回重新登录。本文采取的并非建立长连接的方式。
所以同一个账号设备A登录,然后又在设备B登录时,设备A并不会直接强制退出,需要刷新页面才能强制退出,当然,如果你想达到强制退出的效果,可以模仿长连接的心跳检查,也就是前端定时向服务器发送接口请求,该接口什么事也不用做,只是接受浏览器的请求,然后经过拦截器即可。也能达到强制退出的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
致远OA集成单点登录的过程如下: 1. 首先,需要在致远OA的CIP平台上进行单点登录的配置。这包括登录集团管理员(企业版登录单位管理员),进行产品登记和应用注册,以及设置应用接入和单点登录配置。 2. 在单点登录配置中,需要指定一个统一的SSO地址,可以先指向到OA的自定义控制层。这个地址可以是模拟第三方的单点登录入口或OA的自定义控制层。 3. 当用户访问单点登录入口时,OA会自动维护一个ticket,并将其携带给外部系统。 4. 外部系统拿到ticket后,会向OA握手一次,并解析出单点登录人的信息。这个过程可以通过解析ticket的方式进行,例如使用以下示例链接进行解析:http://localhost/seeyon/thirdpartyController.do?ticket=-4588595864324061939。 5. 外部系统在解析出单点登录人信息后,可以判断是否存在这个用户登录名。如果存在,则允许用户登录;否则,不允许登录。 总的来说,致远OA集成单点登录的逻辑是,通过在OA的CIP平台上进行配置,生成一个ticket并传递给外部系统,外部系统解析ticket并验证用户信息,最终决定是否允许用户登录。\[1\]\[2\] 希望以上信息对您有所帮助。如果还有其他问题,请随时提问。 #### 引用[.reference_title] - *1* [致远OA单点登陆到第三方系统(零代码实现)](https://blog.csdn.net/qq_33064191/article/details/120470762)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [Java实现从第三方系统单点登录到致远OA](https://blog.csdn.net/FZ_9426/article/details/107529512)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [开源OA:手把手教你搭建OA办公系统(13)将O2OA集成到钉钉](https://blog.csdn.net/liyi_hz2008/article/details/124611661)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值