springboot集成JWT+Redis实现单点登录和同一账号只允许在一处登录

功能简述:JWT+Redis实现单点登录功能的同时,也实现同一个账号只能在一台设备上登录,实现方式并非是建立长连接,因为长连接是比较消耗系统性能的。这里只是简单的redis方式实现。

1、什么是单点登录?

单点登录的英文名叫做:Single Sign On(简称SSO)。
在最开始的单体架构(或者说单系统)当中,所有的代码都放在一个项目当中,传统的登录流程是

用户登录—>登录校验(校验用户名密码)—>将用户名等信息放入session当中—>成功登录。
这样就可以从session当中获取用户信息来判断是否登录或者登录人是谁

后来,我们为了合理利用资源和降低耦合性,于是把单系统拆分成多个子系统。如果继续使用传统的登录方式。会产生什么问题呢?简单举个例子

我们都知道session是存在于服务器当中的,假如有两个服务 订单服务和支付服务,分别部署在服务器A和服务器B,在订单服务当中,用户进行了登录,服务器A保存了用户的登录信息,用户进行下单访问服务器A,获取用户信息生成订单,然后支付再访问服务器B,这时候,服务器B是没有方法获取到用户信息的。

这样肯定是不行的,当然session共享可以解决这个问题,但是session共享也有许多弊端,许多公司基本不会使用,而是使用主流的JWT做单点登录

简单来说,单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
单点登录流程:
用户登录—>登录校验—>根据用户信息生成token—>响应token给页面—>前端将token放入cookie
校验:将cookie信息放在请求头—>对token进行验证—>得到用户信息

介绍完了单点登录,废话不多说,上代码

pom文件:

    <dependencies>
        <!--jwt起步依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/eu.bitwalker/UserAgentUtils -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>1.21</version>
        </dependency>
       
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--MyBatis-Plus代码生成器需要的依赖,开始-->
        <!-- 持久层 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </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>

        <!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

yml文件:

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

先在config包下配置两个类

RedisConfig:

@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;
    }

}

全局拦截器:InterceptorConfig

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

UserController:

@RestController
@RequestMapping("/users")
public class UsersController {

    @Resource
    private UsersService usersService;
    @Resource
    TokenService tokenService;
    @Resource
    private RedisUtil redisUtil;
    //登录
    @PassToken
    @PostMapping("/login")
    public Object login(@RequestBody Users user, HttpServletRequest request){
        JSONObject jsonObject=new JSONObject();
        //根据用户名查询用户信息
        Users userForBase=usersService.findByUsername(user);
        String ipAddr = IpUtils.getIpAddr(request);
        if(userForBase==null){
            jsonObject.put("message","登录失败,用户不存在");
            return jsonObject;
        }else {
            if (!userForBase.getPassword().equals(user.getPassword())){
                jsonObject.put("message","登录失败,密码错误");
                return jsonObject;
            }else {
                String token = tokenService.getToken(userForBase);
                String key = RedisPreEnum.JWT_TOKEN_PRE.getPre()+userForBase.getId();
                Set<String> keys = redisUtil.keys(key+"*");
                if (CollectionUtils.isEmpty(keys)){
                    redisUtil.set(key+ipAddr,userForBase,RedisPreEnum.JWT_TOKEN_PRE.getExpired());
                }else {
                    //请空之前的key
                    for (String k:keys) {
                        redisUtil.del(k);
                    }
                    //重新设置key
                    redisUtil.set(key+ipAddr,userForBase,RedisPreEnum.JWT_TOKEN_PRE.getExpired());
                }
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }
    @GetMapping("/getMessage")
    public String getMessage(){
        return "你已通过验证";
    }
}

TokenService:

@Component
public class TokenService {

    private final static String SIGN = "";

    public String getToken(Users 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;
    }
}

RedisUtil:

参上。

IpUtils:

package com.utils;

import eu.bitwalker.useragentutils.UserAgent;

import javax.servlet.http.HttpServletRequest;


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;
    }
}

PassToken注解:

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

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

RedisPreEnum枚举类:

/**
 * redis key前缀
 */
@AllArgsConstructor
@Getter
public enum RedisPreEnum {
    JWT_TOKEN_PRE("JWT_TOKEN_","token前缀",60*60*24);

    private String pre;
    private String desc;
    private Integer expired;
}

User实体类:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Users implements Serializable {

    private static final long serialVersionUID = 1L;

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

    private String name;

    private String password;
}

最重要的一个拦截器类:AuthenticationInterceptor

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Resource
    private RedisUtil redisUtil;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.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无效");
        }
        Users user = null;
        String key = RedisPreEnum.JWT_TOKEN_PRE.getPre() + userId + IpUtils.getIpAddr(httpServletRequest);
        if (redisUtil.hasKey(key)) {
            Object o = redisUtil.get(key);
            user = JSONObject.toJavaObject((JSON) JSON.toJSON(o), Users.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 {
    }
}

数据库mysql对应的表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9l69bP6d-1689061920034)(java-note\img\cfc909d93a2147ae89b71b0773661a32.png)]

总结:

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

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单点登录(Single Sign-On,简称SSO)是一种身份验证技术,可以让用户只需一次登录,就可以访问多个应用程序。在实际开发中,我们可以使用Spring Boot、JWTRedis实现单点登录功能。 下面是实现单点登录的步骤: 1. 创建Spring Boot项目并引入所需依赖:spring-boot-starter-web、spring-boot-starter-data-redis和jjwt。 2. 创建一个User实体类,包含用户名和密码等信息。 3. 创建一个UserService,实现对用户信息的操作,包括注册、登录等。 4. 引入JWT依赖后,我们需要创建一个JWTUtil类,实现token的生成和解析。 5. 创建一个LoginController,用于理用户的登录请求。在登录成功后,生成token并将其存储到Redis中。 6. 创建一个AuthController,用于验证用户的token是否有效。在验证成功后,可以获取用户信息并返回。 7. 在需要进行单点登录验证的应用程序中,只需要在请求中携带token,并调用AuthController进行验证即可。 具体实现细节可以参考以下代码示例: User实体类: ```java public class User { private String username; private String password; // 省略setter和getter方法 } ``` UserService接口: ```java public interface UserService { void register(User user); String login(String username, String password); } ``` UserService实现类: ```java @Service public class UserServiceImpl implements UserService { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public void register(User user) { // 省略用户注册逻辑 } @Override public String login(String username, String password) { // 省略用户登录逻辑 // 登录成功后生成token并存储到Redis中 String token = JWTUtil.generateToken(username); redisTemplate.opsForValue().set(username, token, 30, TimeUnit.MINUTES); return token; } } ``` JWTUtil类: ```java public class JWTUtil { private static final String SECRET_KEY = "my_secret_key"; private static final long EXPIRATION_TIME = 3600000; public static String generateToken(String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } } ``` LoginController: ```java @RestController public class LoginController { @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<String> login(@RequestBody User user) { String token = userService.login(user.getUsername(), user.getPassword()); return ResponseEntity.ok(token); } } ``` AuthController: ```java @RestController public class AuthController { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping("/auth") public ResponseEntity<User> auth(@RequestHeader("Authorization") String token) { String username = JWTUtil.getUsernameFromToken(token); if (StringUtils.isEmpty(username)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } String redisToken = redisTemplate.opsForValue().get(username); if (!token.equals(redisToken)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } User user = new User(); user.setUsername(username); return ResponseEntity.ok(user); } } ``` 在请求中携带token的示例: ```java @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> { String token = // 从Redis中获取token request.getHeaders().add("Authorization", token); return execution.execute(request, body); })); return restTemplate; } } ``` 以上就是使用Spring Boot、JWTRedis实现单点登录的步骤和示例代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值