天天看點

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

超詳細的Java知識點彙總

單點登入是什麼

SSO(Single Sign On)SSO的定義是在多個應用系統中,使用者隻需要登入一次就可以通路所有互相信任的應用系統。

為什麼需要單點登入

以前分布式系統的多個相關的應用系統,都需要分别進行登入,非常繁瑣。

原來登入的過程:

1)使用者輸入賬号密碼

2)送出到背景驗證,成功後将使用者存在Session中

3)需要進行登入狀态判斷時,判斷Session中是否存在該對象

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

存在問題:分布式系統有N個伺服器,每個伺服器有自己的Session,無法登入一次,所有伺服器能判斷使用者登入狀态。

單點登入的解決方案

SSO有哪些常見的解決方案

1)使用Redis實作Session共享

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

有狀态的登入,需要在伺服器中儲存使用者的資料;REST架構推薦使用無狀态通信,不在伺服器端儲存使用者狀态,伺服器壓力更小,成本更低,擴充更加容器。

2)使用Token機制實作

将使用者的狀态儲存到用戶端的cookie中,每次請求伺服器時,都會攜帶使用者資訊,伺服器對使用者資訊進行解析和判斷,來進行登入鑒權。

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

1)使用者輸入賬号密碼,通過網關,進入驗證服務

2)驗證服務進行登入驗證

3)驗證成功後,将使用者資訊儲存到token字元串,将token寫入cookie

4)cookie被儲存到使用者浏覽器中

5)使用者再通路微服務時,經過網關,網關對token進行解析

6)解析成功,允許通路其他微服務

7)解析失敗,不允許通路

這種方式是無狀态登入,伺服器不儲存使用者狀态,狀态儲存到用戶端,資訊存在安全性問題,需要加密。

加密算法

加密算法可以分為:

  • 對稱式加密技術

    對稱式加密就是加密和解密使用同一個密鑰,通常稱之為“Session Key ”這種加密技術在當今被廣泛采用,如美國政府所采用的DES加密标準就是一種典型的“對稱式”加密法,它的Session Key長度為56bits。

    常見對稱式加密技術:DES、3DES、TDEA、Blowfish、RC5、IDEA算法。

  • 非對稱式加密技術

    非對稱式加密就是加密和解密所使用的不是同一個密鑰,通常有兩個密鑰,稱為“公鑰”和“私鑰”,它們兩個必需配對使用,否則不能打開加密檔案。這裡的“公鑰”是指可以對外公布的,“私鑰”則不能,隻能由持有人一個人知道。它的優越性就在這裡,因為對稱式的加密方法如果是在網絡上傳輸加密檔案就很難不把密鑰告訴對方,不管用什麼方法都有可能被别竊聽到。而非對稱式的加密方法有兩個密鑰,且其中的“公鑰”是可以公開的,也就不怕别人知道,收件人解密時隻要用自己的私鑰即可以,這樣就很好地避免了密鑰的傳輸安全性問題。

    常見的非對稱式加密技術:RSA、Elgamal、背包、Rabin、D-H、ECC算法。

    其中最常用的是RSA算法,單點登入采用的是JWT+RSA實作。

  • 不可逆的加密技術

    加密後的資料是無法被解密的,無法根據密文推算出明文

    常見的不可逆的加密技術:MD5、SHA

JWT

Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基于JSON的開放标準((RFC 7519).該token被設計為緊湊且安全的,特别适用于分布式站點的單點登入(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便于從資源伺服器擷取資源,也可以增加一些額外的其它業務邏輯所必須的聲明資訊,該token也可直接被用于認證,也可被加密。

官網:https://jwt.io

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

可以在官網測試儲存資訊到JWT中,可以看到JWT分為三個部分:

  1. header 頭部,包含聲明類型和加密算法
  2. payload 負載,就是有效資料,一般是使用者資訊
  3. signature 簽名,資料的認證資訊

JWT的互動流程

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入
  1. 使用者登入,發送賬号密碼
  2. 服務的認證,通過後根據secret生成token
  3. 将生成的token傳回給浏覽器
  4. 使用者每次請求攜帶token
  5. 服務端利用公鑰解讀jwt簽名,判斷簽名有效後,從Payload中擷取使用者資訊
  6. 處理請求,傳回響應結果

實作JWT單點登入

1)建立登入鑒權服務,引入依賴

<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>
           

2)配置檔案的工具類

/**
 * 讀取配置檔案的配置類
 */
@Data
@Configuration
//讀取配置檔案的注解
@ConfigurationProperties(prefix = "blb.jwt")
public class JwtProperties {

    private String secret;//秘鑰
    private String pubKeyPath;//公鑰路徑
    private String priKeyPath;//私鑰路徑
    private String cookieName;//cookie名稱
    private Integer expire;//cookie過期時間
    private Integer cookieMaxAge;//cookie生命周期
    private PublicKey publicKey;//公鑰
    private PrivateKey privateKey;//私鑰

    //在構造方法之後自動執行
    @PostConstruct
    public void init(){
        File pubKey = new File(pubKeyPath);
        File priKey = new File(priKeyPath);
        try {
            //判斷公鑰和私鑰如果不存在就建立
            if (!priKey.exists() || !pubKey.exists()) {
                //建立公鑰和私鑰檔案
                RsaUtils.generateKey(this.pubKeyPath, this.priKeyPath, this.secret);
            }
            //讀取公鑰和私鑰内容
            this.publicKey = RsaUtils.getPublicKey(this.pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(this.priKeyPath);
        }catch (Exception ex){
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }
}
           

application.properties

#秘鑰
blb.jwt.secret=[email protected]#$%
#公鑰路徑
blb.jwt.pubKeyPath=D:\\java_code\\pub.rsa
#私鑰路徑
blb.jwt.priKeyPath=D:\\java_code\\pri.rsa
#cookie名稱
blb.jwt.cookieName=token
#cookie過期時間
blb.jwt.expire=30
#cookie生命周期
blb.jwt.cookieMaxAge=1800
           

UserService

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserInfo> implements IUserService {

    @Override
    public UserInfo login(String username, String password) {
    	//查詢使用者
        UserInfo user = this.getOne(new QueryWrapper<UserInfo>().lambda().eq(UserInfo::getUsername, username));
        if(user == null){
            return null;
        }
        //将密碼加密加鹽後進行比對
        String encrypt = Md5Utils.encrypt(password, user.getSalt());
        if(encrypt.equals(user.getPassword())){
            return user;
        }
        return null;
    }
}
           

UserController

/**
 * 登入驗證控制器
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;
    @Autowired
    private JwtProperties properties;

    @PostMapping("/login")
    public JsonResult<UserInfo> login(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        UserInfo info = userService.login(username, password);
        if(info != null){
            //驗證通過,将使用者加密為token
            String token = JwtUtils.generateToken(info, properties.getPrivateKey(), properties.getExpire());
            //儲存token到cookie中
            CookieUtils.setCookie(request,response,properties.getCookieName(),token,
                    properties.getCookieMaxAge(),null,true);
            return new JsonResult<>(1,info);
        }
        return new JsonResult<>(0,null);
    }
}
           

登入頁面

<template>
    <el-card style="width: 480px">
        <span style="color:red">{{msg}}</span>
        <el-form v-model="form">
            <el-input v-model="form.username" placeholder="請輸入賬号"></el-input>
            <el-input type="password" v-model="form.password" placeholder="請輸入密碼"></el-input>
            <el-button @click="login">登入</el-button>
        </el-form>
    </el-card>
</template>

<script>
    export default {
        name: "Login",
        data(){
            return{
                msg:"",
                form:{username:"",password:""}
            }
        },
        methods:{
            login(){
                this.$http.post("http://api.blb.com/api/auth-api/user/login",this.$qs.stringify(this.form))
                    .then(res => {
                        if(res.data.code == 1){
                            this.$message.info("登入成功");
                            this.$router.push("/index");
                        }else{
                            this.msg = "賬号或密碼錯誤";
                        }
                    });
            }
        }
    }
</script>
           

解決cookie寫入失敗的問題

原因1:出現跨域,導緻Cookie不能寫入

1)CORS的配置

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

2)axios的配置

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

原因2:Nginx轉發域名不一緻的問題

Nginx轉發配置 : proxy_set_header Host $host;

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

原因3:zuul的敏感頭過濾

關閉敏感頭過濾

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

解決Cookie寫入問題後,将公鑰複制到網關伺服器上,在網關中進行token解析實作統一的通路鑒權

JWT單點登入單點登入是什麼為什麼需要單點登入單點登入的解決方案加密算法JWT實作JWT單點登入

網關判斷使用者登入狀态

1)配置白名單,直接通過不進行登入驗證

2)建立過濾器ZuulFilter

3)過濾到白名單就直接放行

4)非白名單的請求,獲得cookie中的token,解析token

5)如果解析成功,放行,解析失敗,就進行攔截

網關的配置檔案工具類

@Data
@Configuration
//讀取配置檔案的注解
@ConfigurationProperties(prefix = "blb.jwt")
public class JwtProperties {

    private List<String> whiteList;//白名單
    private String pubKeyPath;//公鑰路徑
    private String cookieName;//cookie名稱
    private PublicKey publicKey;//公鑰

    //在構造方法之後自動執行
    @PostConstruct
    public void init(){
        try {
            //讀取公鑰内容
            this.publicKey = RsaUtils.getPublicKey(this.pubKeyPath);
        }catch (Exception ex){
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
    }
}
           

application.properties

# 白名單
blb.jwt.whiteList=/api/auth-api
# 公鑰路徑
blb.jwt.pubKeyPath=D:\\java_code\\pub.rsa
# cookie名稱
blb.jwt.cookieName=token
           

鑒權過濾器

/**
 * 登入鑒權的過濾器
 */
@Component
public class AuthFilter extends ZuulFilter {

    @Autowired
    private JwtProperties properties;

    @Override
    public String filterType() {
        //前置過濾器
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 99;
    }

    //是否進行過濾,true過濾(執行run方法),false(跳過run)
    @Override
    public boolean shouldFilter() {
        //讀取目前請求的位址
        String uri = RequestContext.getCurrentContext().getRequest().getRequestURI();
        List<String> whiteList = properties.getWhiteList();
        //如果位址以白名單中的位址為開頭,就不過濾
        for(String str : whiteList){
            if(uri.startsWith(str)){
                return false;
            }
        }
        //不是白名單就過濾
        return true;
    }

    //過濾邏輯
    @Override
    public Object run() throws ZuulException {
        //先從cookie中讀取token
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String token = CookieUtils.getCookieValue(request, properties.getCookieName());
        //使用公鑰對token進行解析
        try {
            UserInfo user = JwtUtils.getInfoFromToken(token, properties.getPublicKey());
            return user;
        }catch (Exception ex){
            ex.printStackTrace();
            //登入攔截
            currentContext.setSendZuulResponse(false);
            currentContext.setResponseStatusCode(401);
        }
        return null;
    }
}