基于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;
    },
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

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