基于SpringBoot的JWT单点登录

基于SpringBoot的JWT单点登录单点登录单点登录SSO,分布式架构中通过一次登录,就能访问多个相关的服务。快速入门首先引入Jwt依赖<!–JWT–><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.4&

大家好,又见面了,我是你们的朋友全栈君。

单点登录

单点登录SSO,分布式架构中通过一次登录,就能访问多个相关的服务。

快速入门

首先引入Jwt依赖

<!-- JWT -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.9.9</version>
        </dependency>

JWT工具类

/** * JWT工具类 */
public class JwtUtil { 
   

    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USERNAME = "username";
    public static final String JWT_KEY_ICON = "icon";
    public static final String JWT_KEY_REALNAME = "realname";
    public static final int EXPIRE_MINUTES = 30;

    /** * 私钥加密token */
    public static String generateToken(String id, String username, String realname, String icon, PrivateKey privateKey, int expireMinutes) throws Exception { 
   

        return Jwts.builder()
                .claim(JWT_KEY_ID, id)
                .claim(JWT_KEY_ICON, icon)
                .claim(JWT_KEY_USERNAME, username)
                .claim(JWT_KEY_REALNAME, realname)
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /** * 从token解析用户 * * @param token * @param publicKey * @return * @throws Exception */
    public static User getUserInfoFromToken(String token, PublicKey publicKey) throws Exception { 
   
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        String id = (String) body.get(JWT_KEY_ID);
        String username = (String) body.get(JWT_KEY_USERNAME);
        String icon = (String) body.get(JWT_KEY_ICON);
        String realname = (String) body.get(JWT_KEY_REALNAME);
        User user = new User(Integer.valueOf(id),username,null,realname,null,icon,0);
        return user;
    }
}

使用RSA生成公钥和私钥

JSON Web Token 用于Web应用进行权限验证的令牌字符串

需要对用户信息进行加密

加密分为:

  • 对称式加密

    加密和解密使用一个秘钥

    常用的算法:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK

  • 非对称式加密

    加密和解密使用不同的秘钥:私钥、公钥

    私钥是保存在服务内部,公钥可以公开到其它服务中

    常用的算法:RSA、DSA等

  • 不可逆加密

    加密后不能解密

    如:MD5

我们采用JWT+RSA算法进行加密

RSA工具类

/** * RSA工具类 */
public class RsaUtil { 
   

    public static final String RSA_SECRET = "edu.learn.sys@#$%"; //秘钥
    public static final String RSA_PATH = "C:\\rsa\\";//秘钥保存位置
    public static final String RSA_PUB_KEY_PATH = RSA_PATH + "pub.rsa";//公钥路径
    public static final String RSA_PRI_KEY_PATH = RSA_PATH + "pri.rsa";//私钥路径

    public static PublicKey publicKey; //公钥
    public static PrivateKey privateKey; //私钥

    /** * 类加载后,生成公钥和私钥文件 */
    static { 
   
        try { 
   
            File rsa = new File(RSA_PATH);
            if (!rsa.exists()) { 
   
                rsa.mkdirs();
            }
            File pubKey = new File(RSA_PUB_KEY_PATH);
            File priKey = new File(RSA_PRI_KEY_PATH);
            //判断公钥和私钥如果不存在就创建
            if (!priKey.exists() || !pubKey.exists()) { 
   
                //创建公钥和私钥文件
                RsaUtil.generateKey(RSA_PUB_KEY_PATH, RSA_PRI_KEY_PATH, RSA_SECRET);
            }
            //读取公钥和私钥内容
            publicKey = RsaUtil.getPublicKey(RSA_PUB_KEY_PATH);
            privateKey = RsaUtil.getPrivateKey(RSA_PRI_KEY_PATH);
        } catch (Exception ex) { 
   
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }

    /** * 从文件中读取公钥 * * @param filename 公钥保存路径,相对于classpath * @return 公钥对象 * @throws Exception */
    public static PublicKey getPublicKey(String filename) throws Exception { 
   
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /** * 从文件中读取密钥 * * @param filename 私钥保存路径,相对于classpath * @return 私钥对象 * @throws Exception */
    public static PrivateKey getPrivateKey(String filename) throws Exception { 
   
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /** * 获取公钥 * * @param bytes 公钥的字节形式 * @return * @throws Exception */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception { 
   
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /** * 获取密钥 * * @param bytes 私钥的字节形式 * @return * @throws Exception */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception { 
   
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /** * 根据密文,生存rsa公钥和私钥,并写入指定文件 * * @param publicKeyFilename 公钥文件路径 * @param privateKeyFilename 私钥文件路径 * @param secret 生成密钥的密文 * @throws IOException * @throws NoSuchAlgorithmException */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception { 
   
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception { 
   
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException { 
   
        File dest = new File(destPath);
        if (!dest.exists()) { 
   
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

以上的准备工作就做好了,接下来的操作步骤上可以分为

  1. 在用户登录的时候将用户的登录信息通过jwt工具类加密为密文返回前台
  2. 前台接受到密文信息后存储到请求头中
  3. 在网关配置全局过滤器,下次登录的时候来解析前台携带的请求头中的密文,校验密文的合法性,如果密文验证成功则放行,如果验证失败则对该请求进行拦截。

登录成功的后对用户信息加密后返回前端

只要用户登录成功就会进去改代码块,执行加密逻辑

/** * 登录成功的处理 */
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler { 
   

    @Autowired
    private UserDao userService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { 
   
        Object principal = authentication.getPrincipal();
        try { 
   
            //读取用户的其它信息
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("username",authentication.getName());
            User userObj = userService.selectOne(queryWrapper);
            //将用户名转换为JWT
            String token = JwtUtil.generateToken(userObj.getId().toString(),userObj.getUsername(),userObj.getRealname(),
                    userObj.getIcon(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
// //保存到Cookie中
            CookieUtil.saveCookie(resp,"userToken",token,7 * 24 * 3600);
            resp.setContentType("application/json;charset=utf-8");
            //发送用户信息到前端
            PrintWriter out = resp.getWriter();
            UserVO userVO = new UserVO(userObj,token);
            out.write(new ObjectMapper().writeValueAsString(userVO));
            out.flush();
            out.close();
            log.info("生成token保存-->{}" , userVO);
        } catch (Exception e) { 
   
            log.error("保存token失败",e);
        }
    }
}

网关对前端的请求头进行解析

/** * 对所有请求进行拦截,放行登录成功的请求 */
@Slf4j
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered { 
   

    @Autowired
    private GatewayConfig gatewayConfig;

    private static final Integer EXPIRE_DATE = 60 * 30;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 
   
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //放行不拦截的请求
        List<String> whiteList = gatewayConfig.getWhiteList();
        for (String str : whiteList) { 
   
            if (request.getURI().getPath().contains(str)) { 
   
                log.info("放行 {}",request.getURI().getPath());
                return chain.filter(exchange);
            }
        }
        try { 
   
            String token = request.getHeaders().getFirst("Authorization");
// //读取cookie中的token
// String token = request.getCookies().getFirst("token").getValue();
            //解析该token为用户对象
            User user = JwtUtil.getUserInfoFromToken(token, RsaUtil.publicKey);
            log.info("登录成功!{}" , user);
        } catch (Exception e) { 
   
            log.error("{}请求被拦截",request.getURI().getPath(),e);
            //拦截请求
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            String msg = "Request Denied!!";
            DataBuffer wrap = response.bufferFactory().wrap(msg.getBytes());
            return response.writeWith(Mono.just(wrap));
        }
        //放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() { 
   
        return 0;
    }
}

token自动过期时间自动刷新问题

这样我们的jwt单点登录的业务就完成了,但是还存在一个问题,加入用户在访问的过程中登录密文已经过期,那么是十分影响用户体验。我们如何解决这个问题

解决问题

我的思路是在用户的热点访问接口上,对用户的请求头进行截取,重新包装,设置新的过期时间,只要用户在不停的访问我们的热点接口,我们就会不断的给用户刷新token的过期时间,这样只要用户在使用的过程中就不会频繁的重复去登录。

我们认为搜索课程服务为一个热点服务接口,因此在搜索课程的service层来设置新的过期时间返回前台,在返回分页对象的时候把我们的新的token加密对象也封装进去。

 @SneakyThrows
    @Override
    public PageEntity<Course> searchCoursePage(Map<String, String> map, HttpServletRequest request) { 
   
        try { 
   
            // 读取header
            String token = request.getHeader("Authorization");
            //解析该token为用户对象
            User user = JwtUtil.getUserInfoFromToken(token, RsaUtil.publicKey);
            // 给token做延时处理,生成一个新的token 原有基础上增加30分钟
            String userToken = JwtUtil.generateToken(user.getId().toString(), user.getUsername(), user.getRealname(), user.getIcon(), RsaUtil.privateKey, EXPIRE_DATE);
            //获得当前页数和长度
            int current = Integer.valueOf(map.get("current"));
            int size = Integer.valueOf(map.get("size"));
            //获得过滤条件和排序方式
            String search = map.get("search");
            String sort = map.get("sort");
            Map<String, String> searchMap = JSONUtil.parseMap(search);
            Map<String, String> sortMap = JSONUtil.parseMap(sort);
            //执行分页查询
            PageEntity<Course> coursePageEntity = dao.searchPage(INDEX_NAME, searchMap, sortMap, (current - 1) * size, size, Course.class);
            // 将加密信息包装到分页对象中一起返回为前端
            UserVO userVO = new UserVO();
            userVO.setUser(user);
            userVO.setToken(userToken);
            coursePageEntity.setUserVO(userVO);
            return coursePageEntity;
        } catch (IOException e) { 
   
            e.printStackTrace();
            throw new RuntimeException(e);
        }

    }

前端会将刷新的新的token存入header中
在这里插入图片描述
前端配置

//配置axios拦截请求,添加token头信息
axios.interceptors.request.use(
    config => { 
   
      let token = localStorage.getItem("token");
      console.log("token:" + token);
      if (token) { 
   
        //把localStorage的token放在Authorization里
        config.headers.Authorization = token;
      }
      return config;
    },
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/143610.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • PHPstrom激活码[最新免费获取]

    (PHPstrom激活码)这是一篇idea技术相关文章,由全栈君为大家提供,主要知识点是关于2021JetBrains全家桶永久激活码的内容IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html1STL5S9V8F-eyJsa…

    2022年3月27日
    83
  • 写给大忙人看的 – Java中图片压缩上传至MinIO服务器(4)[通俗易懂]

    写给大忙人看的 – Java中图片压缩上传至MinIO服务器(4)[通俗易懂]之前文章已经介绍了MinIO的环境搭建,已经对文件的上传下载方法,本篇文章一起与大家来学习图片压缩上传的方法1、背景最近客户总抱怨APP中图片显示较慢,升级服务器带宽又没有多的预算,查看原因,是因为现在大家都是用的智能手机拍照,拍出来的照片小则2-3M,大则十几M,所以导致图片显示较慢。思考再三,决定将图片进行压缩再上传图片服务器来解决图片显示慢的问题

    2022年6月18日
    69
  • c#实现图片gif去水印「建议收藏」

    做项目时候会遇到在网络上爬的源文件,png图片或者动画gif背景都带有水印,“百度出品”“不得转载”等等,这样出来的文件放在项目里面当做自己的资源来用肯定是不可以的,现在就来用lockbits替换背景的颜色,实现水印消除的目的。话不多述,上图:处理前:这是处理之前的图,其实底部的“baidu汉语“看着并不是很明显(仔细看),仍然需要把字体的背部水印去掉,这里开始用lockbits来去水印了。处理

    2022年4月9日
    136
  • vue父传子 子传父 prop定义方法

    vue父传子 子传父 prop定义方法简单的例子没有多余代码 父传子 template 父组件引用子组件 aaa 两种写法传递 num1 num2 div aaav bind father1 num1 father2 num2 aaav bind father1 num1 father2 num2 div template script import script

    2025年11月1日
    2
  • 1.零基础如何学习Web安全渗透测试?[通俗易懂]

    1.零基础如何学习Web安全渗透测试?[通俗易懂]零基础如何学习Web安全渗透测试?这可能是史上最详细的自学路线图!转载于拼客学院陈鑫杰拼客院长陈鑫杰(若有侵权,请联系邮件751493745@qq.com,我会及时删除)(转载链接:https://mp.weixin.qq.com/s/SlG_tWSEXapMeOezfBrnww)…

    2025年6月13日
    2
  • windows安装wget命令_wget怎么用

    windows安装wget命令_wget怎么用在linux操作系统中,我们会经常要用到wget下载文件。wget非常稳定,它在带宽很窄的情况下和不稳定网络中有很强的适应性。在linux中使用wget时,若报-bash:wget:commandnotfound,则表明没有安装wget,需要安装,安装命令如下:yum-yinstallwget安装完成即可以使用。…

    2022年8月31日
    6

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号