最近發生了些糟糕的事情,自己受到了比較大的影響,也影響了更新的頻率,後面會慢慢補上。
一般 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 做負載均衡,會自動的分發到兩個服務實例上,看看連續發送查詢請求,兩個服務實例的反應,爲了區分,兩個實例的打印稍微有點區別
好了,完美解決單點登錄問題。