SSO單點登入

Richard.M.路發表於2020-10-09

1、單一伺服器模式

  1. 使用者向伺服器傳送使用者名稱和密碼。
  2. 驗證伺服器後,相關資料(如使用者角色,登入時間等)將儲存在當前會話中。
  3. 伺服器向使用者返回session_id,session資訊都會寫入到使用者的Cookie。
  4. 使用者的每個後續請求都將通過在Cookie中取出session_id傳給伺服器。
  5. 伺服器收到session_id並對比之前儲存的資料,確認使用者的身份。

在這裡插入圖片描述

2、SSO(single sign on)模式

隨著業務擴大,業務分開部署,儲存在session中時無法在另一臺伺服器中判斷登入。

2.1、session廣播

  • 將同伺服器的session複製到其他伺服器中

在這裡插入圖片描述

缺點:資料冗餘,增加伺服器的負擔

2.2、cookie+redis

  1. 登入之後資料儲存在兩個地方;Redis中Key生成使用者的唯一id,value值儲存使用者資訊,瀏覽器將Redis生成的唯一id儲存在cookie中。
  2. 訪問其他模組,傳送請求時攜帶cookie,取出cookie中的id對redis操作判斷是否登入。

在這裡插入圖片描述

優點: 使用者身份資訊獨立管理,更好的分散式管理; 可以自己擴充套件安全策略

缺點:認證伺服器訪問壓力較大

  1. Spring-session實現全域性session儲存
  • pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • application.properties
server.port=9000
spring.application.name=spring-session

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
  • 開啟spring-session
@EnableRedisHttpSession
@SpringBootApplication
public class SsoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SsoApplication.class, args);
    }
}
  • Controller
@GetMapping("put")
public String putSession(HttpSession session){
    session.setAttribute("user","登入資訊");
    return "ok";
}

@GetMapping("get")
public String getSession(HttpSession session){
    Object user = session.getAttribute("user");
    return "ok"+user;
}
  • redis
#keys *
 1)  "spring:session:expirations:1602219480000"
 2)  "spring:session:sessions:expires:d131e843-75f6-4c9c-97ab-a0a884a95a28"
 3)  "spring:session:sessions:d131e843-75f6-4c9c-97ab-a0a884a95a28"
  • 自定義序列化
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<Object,Object> redisTemplate =new RedisTemplate<>(); 
        redisTemplate.setConnectionFactory(factory);
        //Jackson序列化value
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jsonRedisSerializer.setObjectMapper(mapper);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        redisTemplate.setValueSerializer(jsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
        StringRedisTemplate redisTemplate =  new StringRedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

  • 原理
protected void doFilterInternal(HttpServletRequest request, 
                                HttpServletResponse response, 
                                FilterChain filterChain)throws ServletException, IOException {
    
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);//請求包裝
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);//響應包裝
    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);//包裝過的請求響應通過過濾器
    }
    finally {
        wrappedRequest.commitSession();
    }
}

2.3、token

  1. 登入之後,根據使用者資訊按照一定的規則生成字串傳送給前端。
  2. 訪問其他模組時,使用cookie攜帶token或位址列引數攜帶token傳送請求,後端收到token值進行解析判斷登入

優點:無狀態: token無狀態,session有狀態的;基於標準化: 可以採用標準化的 JSON Web Token (JWT)

缺點:佔用頻寬;無法在伺服器端銷燬

在這裡插入圖片描述

  1. JWT儲存登入資訊
  • JWT頭:描述JWT後設資料的JSON物件
  • alg屬性表示簽名使用的演算法,預設為HMAC SHA256(寫為HS256);
  • typ屬性表示令牌的型別,JWT令牌統一寫為JWT。
  • 最後,使用Base64 URL演算法將JSON物件轉換為字串儲存
{
  "alg": "HS256",
  "typ": "JWT"
}
  • JWT主體:有效載荷部分,是JWT的主體內容部分,也是一個JSON物件,包含需要傳遞的資料。

請注意,預設情況下JWT是未加密的,任何人都可以解讀其內容,因此不要構建隱私資訊欄位,存放保密信

息,以防止資訊洩露。JSON物件也使用Base64 URL演算法轉換為字串儲存。

預設提供欄位:
	#iss:發行人
	#exp:到期時間
	#sub:主題
	#aud:使用者
	#nbf:在此之前不可用
	#iat:釋出時間
	#jti:JWT ID用於標識該JWT
自定義欄位:
{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}
  • JWT簽名:簽名雜湊部分是對上面兩部分資料簽名,通過指定的演算法生成雜湊,以確保資料不會被篡改。
  • 首先,需要指定一個密碼(secret)。該密碼僅僅為儲存在伺服器中,並且不能向使用者公開。
  • 然後,使用標頭中指定的簽名演算法(預設情況下為HMAC SHA256)根據以下公式生成簽名。
  • JWT問題和趨勢
  1. JWT不僅可用於認證,還可用於資訊交換。善用JWT有助於減少伺服器請求資料庫的次數。

  2. 生產的token可以包含基本資訊,比如id、使用者暱稱、頭像等資訊,避免再次查庫

  3. 儲存在客戶端,不佔用服務端的記憶體資源

  4. JWT預設不加密,但可以加密。生成原始令牌後,可以再次對其進行加密。

  5. 當JWT未加密時,一些私密資料無法通過JWT傳輸。

  6. JWT的最大缺點是伺服器不儲存會話狀態,所以在使用期間不可能取消令牌或更改令牌的許可權。也就是說,

    一旦JWT簽發,在有效期內將會一直有效。

  7. JWT本身包含認證資訊,token是經過base64編碼,所以可以解碼,因此token加密前的物件不應該包含敏感資訊,一旦資訊洩露,任何人都可以獲得令牌的所有許可權。為了減少盜用,JWT的有效期不宜設定太長。對於某些重要操作,使用者在使用時應該每次都進行進行身份驗證。

  8. 為了減少盜用和竊取,JWT不建議使用HTTP協議來傳輸程式碼,而是使用加密的HTTPS協議進行傳輸。

  • JWT工具類
public class JwtUtils {

    //過期時間
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    //伺服器金鑰
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    //生成token
    public static String getJwtToken(String id, String nickname){
        String JwtToken = Jwts.builder()
            //頭資訊
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "HS256")
            //預設主體
            .setSubject("user")
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
            //自定義主體
            .claim("id", id)
            .claim("nickname", nickname)
            //簽名hash
            .signWith(SignatureAlgorithm.HS256, APP_SECRET)
            .compact();
        return JwtToken;
    }
    // 判斷token是否存在與有效   
    public static boolean checkToken(String jwtToken) {
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    // 判斷token是否存在與有效 
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
    //根據token獲取會員id   
    public static String getIdByReq(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).
            parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
    //根據token獲取會員id
    public static String getIdByJwt(String token) {
        if(StringUtils.isEmpty(token)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}
  • controller
@GetMapping("token/put")
public String putSession(){
    return JwtUtils.getJwtToken("1", "wdd");
}
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjAyMjIyNzQ0LCJleHAiOjE2MDIzMDkxNDQsImlkIjoiMSIsIm5pY2tuYW1lIjoid2RkIn0.
//LJyXdXttVyd1xI91QetkcWKd3WhuNiaJs3aLWy8RqcY


@GetMapping("token/get")
public String getSession(@RequestParam("token")String token){
    return JwtUtils.getIdByJwt(token);
}

相關文章