基于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)
blank

相关推荐

  • ftp客户端软件,8款最受欢迎的ftp客户端软件

    对于ftp客户端软件,你了解多少?其实一般人也接触不到这种软件。ftp客户端软件主要是针对从事网站管理的工作人员比较有利的一款工具。可以帮助他们快速的解决工作中的问题。方便、简单、快捷又明了的解决问题,下面有六款ftp客户端软件的介绍。第一款:IIS7服务器管理工具这款工具是真的好用,童叟无欺的那种好用。在我心里它是排在中文版javaftp工具类中的榜首的。它不仅拥有每个javaftp工具类都具备的批量管理功能,还具备很多你意想不到的地方,比如定时同步(上传和下载)、多任务同时进行、定时备份还能够自

  • internal server error是什么意思?

    internal server error是什么意思?internalservererror错误通常发生在用户访问网页的时候发生,该错误的意思是因特网服务错误。能够引起internalservererror报错的原因有多个,如果你是网站主的话,可以对下列情形进行一一排查。  1.服务器资源超载。如果网站文件没有做过修改,最有可能的是同服务器的资源超载:即同一时间内处理器有太多的进程需要处理的时候,会出现500错误。借助SSH,可以在命令行中输入以下命令查看:psfauxpsfaux|grepusername如果你查到某个进程消耗过多资源,

  • git clone与git pull区别

    git clone与git pull区别原地址最近一直焦虑换工作与面试,自然面试过程中也被问到了很多问题,在一家公司中,被问到了git相关的知识。面试官提出了gitclone与gitpull有什么区别。由于自己对git的掌握情况不是特别深入,感觉瞬间被问蒙圈一样。后来,查了相关的文档,看了一些文章,自己有了一丁点的理解,觉得应该…

  • 爬取爱套图网上的图片

    爬取爱套图网上的图片#coding=utf-8frombs4importBeautifulSoupimportrequestsforiinrange(20):i=str(i)url=’https://www.aitaotu.com/weimei/16359_’+i+’.html’html=requests.g…

  • pytest重试_qq插件加载失败如何处理

    pytest重试_qq插件加载失败如何处理安装:pip3installpytest-rerunfailures重新运行所有失败用例要重新运行所有测试失败的用例,请使用–reruns命令行选项,并指定要运行测试的最大次数:$py

  • java向上取整向下取整

    java向上取整向下取整向上取整用Math.ceil(doublea)向下取整用Math.floor(doublea)举例:publicstaticvoidmain(String[]args)throwsException{doublea=35;doubleb=20;doublec=a/b;System.ou

发表回复

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

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