Springboot 依賴 token 管理會話(解決單點登錄)

最近發生了些糟糕的事情,自己受到了比較大的影響,也影響了更新的頻率,後面會慢慢補上。

一般 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 做負載均衡,會自動的分發到兩個服務實例上,看看連續發送查詢請求,兩個服務實例的反應,爲了區分,兩個實例的打印稍微有點區別
在這裏插入圖片描述
在這裏插入圖片描述
好了,完美解決單點登錄問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章