0


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

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

什么是单点登录?

单点登录的英文名叫做: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)publicclassRedisConfig{@Bean@ConditionalOnMissingBean(name ="redisTemplate")public RedisTemplate<Object, Object>redisTemplate(
            RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> template =newRedisTemplate<>();//使用fastjson序列化
        FastJsonRedisSerializer fastJsonRedisSerializer =newFastJsonRedisSerializer(Object.class);// value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);// key的序列化采用StringRedisSerializer
        template.setKeySerializer(newStringRedisSerializer());
        template.setHashKeySerializer(newStringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBean(StringRedisTemplate.class)public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate template =newStringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);return template;}}

全局拦截器:InterceptorConfig

@ConfigurationpublicclassInterceptorConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**");}@Beanpublic AuthenticationInterceptor authenticationInterceptor(){returnnewAuthenticationInterceptor();}}

UserController:

@RestController@RequestMapping("/users")publicclassUsersController{@Resourceprivate UsersService usersService;@Resource
    TokenService tokenService;@Resourceprivate RedisUtil redisUtil;//登录@PassToken@PostMapping("/login")public Object login(@RequestBody Users user, HttpServletRequest request){
        JSONObject jsonObject=newJSONObject();//根据用户名查询用户信息
        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{//请空之前的keyfor(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:

@ComponentpublicclassTokenService{privatefinalstatic 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:

https://blog.csdn.net/weixin_43412919/article/details/122050884?spm=1001.2014.3001.5501

IpUtils:

package com.utils;import eu.bitwalker.useragentutils.UserAgent;import javax.servlet.http.HttpServletRequest;publicclassIpUtils{//客户端类型  手机、电脑、平板//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();publicstatic 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为客户ipif(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 @interfacePassToken{booleanrequired()defaulttrue;}

RedisPreEnum枚举类:

/**
 * redis key前缀
 */@AllArgsConstructor@Getterpublicenum 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)publicclassUsersimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@TableId(value ="id", type = IdType.AUTO)private Integer id;private String name;private String password;}

这里使用的是mybatis-plus操作数据库,实体类使用代码生成器生成,没有使用mybatis-plus的小伙伴 只需要把findByUsername替换成自己的的即可,就是单纯 根据用户名查询用户信息

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

publicclassAuthenticationInterceptorimplementsHandlerInterceptor{@Resourceprivate RedisUtil redisUtil;@OverridepublicbooleanpreHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object)throws Exception {
        String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token// 如果不是映射到方法直接通过if(!(object instanceofHandlerMethod)){returntrue;}
        HandlerMethod handlerMethod =(HandlerMethod) object;
        Method method = handlerMethod.getMethod();//检查是否有passtoken注释,有则跳过认证if(method.isAnnotationPresent(PassToken.class)){
            PassToken passToken = method.getAnnotation(PassToken.class);if(passToken.required()){returntrue;}}if(token == null){thrownewRuntimeException("无token,请重新登录");}
        String userId;try{
            userId = JWT.decode(token).getAudience().get(0);}catch(JWTDecodeException j){thrownewException("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){thrownewRuntimeException("请重新登录");}// 验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();try{
            jwtVerifier.verify(token);}catch(Exception e){thrownewException("token无效");}returntrue;}@OverridepublicvoidpostHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView)throws Exception {}@OverridepublicvoidafterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e)throws Exception {}}

数据库mysql对应的表

在这里插入图片描述
效果截图:
使用postman,进行登录:
在这里插入图片描述
redis当中,可以看到有这个key
在这里插入图片描述
请求头当中携带登录接口返回的token
在这里插入图片描述
到这里,只是验证了单点登录。

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

用一个账号,模拟用不同ip地址登录,看看在redis当中是什么样的
第一次登录:
在这里插入图片描述
redis当中的信息
在这里插入图片描述
第二次登录:
在这里插入图片描述

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

好的,本文分享就到这里了,代码本人实测有效,有不好的地方或者错误的地方,欢迎各位多多评论指出。谢谢

标签: redis 数据库 jwt

本文转载自: https://blog.csdn.net/weixin_43412919/article/details/122049579
版权归原作者 木偶亽~ 所有, 如有侵权,请联系我们删除。

“Springboot集成JWT+Redis实现单点登录和同一账号只允许在一处登录”的评论:

还没有评论