大家好,又见面了,我是你们的朋友全栈君。
单点登录
单点登录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);
}
}
以上的准备工作就做好了,接下来的操作步骤上可以分为
- 在用户登录的时候将用户的登录信息通过jwt工具类加密为密文返回前台
- 前台接受到密文信息后存储到请求头中
- 在网关配置全局过滤器,下次登录的时候来解析前台携带的请求头中的密文,校验密文的合法性,如果密文验证成功则放行,如果验证失败则对该请求进行拦截。
登录成功的后对用户信息加密后返回前端
只要用户登录成功就会进去改代码块,执行加密逻辑
/** * 登录成功的处理 */
@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账号...