最近发生了些糟糕的事情,自己受到了比较大的影响,也影响了更新的频率,后面会慢慢补上。
一般 Springboot 项目默认都会使用 session 的方式管理会话,但是在集群项目中,使用 session 的管理方式就会变的比较麻烦了(单点登录问题),可能需要为每个节点同步 session,还伴随有内存的损耗。这个时候 token 的方式就是一个很好的解决方案,具体原因可以参考之前的《cookie,session,token 的理解》一文。
接下来的内容将会介绍如何在 Springboot 的项目中接入 token 来管理会话。
1、添加依赖库
标记处的依赖如下
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
2、配置 token
标记处的配置信息如下
# header:凭证(校验的变量名)
config.jwt.header=token
# expire:有效期1天(单位:s)
config.jwt.expire=3600
# secret:秘钥(普通字符串)
config.jwt.secret=aHR0cHM6Ly9teS5vc2NoaW5hLm5ldC91LzM2ODE4Njg=
3、代码实现
在配置 token 的信息时,会发现没有自动提示,这里需要将配置信息手动的引入代码中。
JwtConfig 类的具体实现如下
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = "config.jwt")
@Component
public class JwtConfig {
/*
* 生成 Token
*/
public String getToken (String identityId){
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(identityId)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/*
* 获取 Token 中注册信息
*/
public Claims getTokenClaim (String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/*
* Token 是否过期验证
*/
public boolean isTokenExpired (Date expirationTime) {
return expirationTime.before(new Date());
}
// 密钥
private String secret;
// 超时时间
private long expire;
private String header;
}
设置拦截器,拦截 http 请求,校验 token
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
@Resource
private JwtConfig jwtConfig ;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 过滤登录的 url
String uri = request.getRequestURI();
System.out.println("uri=" + uri);
if (uri.contains("/login")){
return true ;
}
// token 校验
String token = request.getHeader(jwtConfig.getHeader());
if(StringUtils.isEmpty(token)){
token = request.getParameter(jwtConfig.getHeader());
}
if(StringUtils.isEmpty(token)){
throw new Exception(jwtConfig.getHeader()+ "不能为空");
}
Claims claims = jwtConfig.getTokenClaim(token);
if(claims == null || jwtConfig.isTokenExpired(claims.getExpiration())){
throw new Exception(jwtConfig.getHeader() + "失效,请重新登录");
}
request.setAttribute("identityId", claims.getSubject());
return true;
}
}
接下来需要让拦截器生效
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private TokenInterceptor tokenInterceptor ;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
}
}
登录的接口
TokenController 类的实现
import com.alibaba.fastjson.JSON;
import com.hosh.tech.security.JwtConfig;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TokenController {
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User {
String username;
String password;
}
@Resource
private JwtConfig jwtConfig ;
// 拦截器直接放行,返回Token
@PostMapping("/login")
public Map<String,String> login (@RequestBody User user){
System.out.println("xx------------- 1");
System.out.println("xx------------- 1 " + JSON.toJSONString(user));
// 这里需要验证用户名和密码,验证成功以后,走后续的 token 生成流程
Map<String,String> result = new HashMap<>() ;
// 省略数据源校验
String token = jwtConfig.getToken(user.getUsername()+user.getPassword()) ;
if (!StringUtils.isEmpty(token)) {
result.put("token",token) ;
}
result.put("userName",user.getUsername()) ;
return result ;
}
}
4、测试 token 的效果
为了更好的体现 token 对单点登录问题的解决效果,需要做一个集群,集群使用 nginx 做负载均衡。
通过截图可以发现,使用 nginx 做负载均衡,同时为这两个实例使用相同的负载均衡策略。
测试接口的实现
先使用登录接口
将登录接口返回的 token 作为 header 添加至后续的请求接口
因为使用了 nginx 做负载均衡,会自动的分发到两个服务实例上,看看连续发送查询请求,两个服务实例的反应,为了区分,两个实例的打印稍微有点区别
好了,完美解决单点登录问题。