前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于SpringBoot的JWT单点登录

基于SpringBoot的JWT单点登录

作者头像
全栈程序员站长
发布2022-08-26 11:45:12
5610
发布2022-08-26 11:45:12
举报
文章被收录于专栏:全栈程序员必看

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

单点登录

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

快速入门

首先引入Jwt依赖

代码语言:javascript
复制
<!-- 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工具类

代码语言:javascript
复制
/** * 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工具类

代码语言:javascript
复制
/** * 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. 在网关配置全局过滤器,下次登录的时候来解析前台携带的请求头中的密文,校验密文的合法性,如果密文验证成功则放行,如果验证失败则对该请求进行拦截。

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

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

代码语言:javascript
复制
/** * 登录成功的处理 */
@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);
        }
    }
}

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

代码语言:javascript
复制
/** * 对所有请求进行拦截,放行登录成功的请求 */
@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加密对象也封装进去。

代码语言:javascript
复制
 @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中

在这里插入图片描述
在这里插入图片描述

前端配置

代码语言:javascript
复制
//配置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.cn/143610.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 单点登录
  • 快速入门
    • JWT工具类
    • 使用RSA生成公钥和私钥
      • 以上的准备工作就做好了,接下来的操作步骤上可以分为
      • 登录成功的后对用户信息加密后返回前端
      • 网关对前端的请求头进行解析
      • token自动过期时间自动刷新问题
      • 解决问题
      相关产品与服务
      访问管理
      访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档