從零開始的SpringBoot前後端分離入門級專案(三)

2020-09-22 11:01:04

實體類編寫

在前面的文章中我們已經完成了專案目錄和基本框架的搭建,現在我們開始編寫實體類,首先我們在model包下建立一個pojo子包。
注意:關於PO、VO、POJO、DTO等概念網路上已經有很多資料了,在本專案中不再闡述其的詳細含義,且為了方便起見只劃分POJO與DTO,為此產生的一些有歧義或錯誤的劃分方法或用法請讀者們見諒。
建立好子包之後我們建立相應的pojo,一張表對應一個類,如果編寫過Web專案的同學對這步應該是比較熟悉的,建立相應的類的過程筆者不一一講解,具體可以參見筆者的GitHub主頁,後續將會上傳完整的直接匯入的工程程式碼,建立完畢後請務必將@Data、@AllArgsConstructor、@NoArgsConstructor三個註解加上。
pojo創建完畢

利用Token進行使用者鑑權

實體類編寫完之後我們開始進行使用者許可權認證功能的編寫,在本專案中利用了jwt和攔截器(interceptor)進行實現。
首先在本專案僅分為三種角色:

  1. 實驗室管理員(ADMIN)
  2. 實驗室成員(VIP)
  3. 普通使用者(MEMBER)

這種劃分方式是不夠嚴謹和安全的,目前的許可權劃分一般是分為使用者角色和使用者許可權兩個部分,每個使用者對應某個角色,某個角色擁有某些許可權,歡迎讀者自行思考更完善的許可權劃分方案。

建立Jwt工具類

首先我們先前往util包中建立一個名為JwtUtil的類,類的內容如下:
JwtUtil

/**
 * @Author Alfalfa99
 * @Date 2020/9/13 15:54
 * @Version 1.0
 * JWT生成以及校驗工具類
 */

@ConfigurationProperties("jwt.config")
@Component
public class JwtUtil {
    private String key;
    private long ttl;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public long getTtl() {
        return ttl;
    }

    public void setTtl(long ttl) {
        this.ttl = ttl;
    }

    /**
     * 生成JWT
     *
     * @param id
     * @return
     */
    public String createJWT(String id, String roles) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder().setId(id)	//在這裡我們將使用者的id存入Jwt中,方便後續使用
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, key).claim("roles", roles); //在這裡我們將使用者的角色存入Jwt中,方便後續鑑權,如果想存別的內容也可以往裡寫
        if (ttl > 0) {
            builder.setExpiration(new Date(nowMillis + ttl));
        }
        return builder.compact();
    }

    /**
     * 解析JWT
     *
     * @param jwtStr
     * @return
     */
    public Claims parseJWT(String jwtStr) {
        return Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(jwtStr)
                .getBody();
    }
}

key是伺服器端用於加密的一個密文,通常隨機生成,ttl是token的過期時間,token的詳細原理請自行查閱學習,篇幅有限不再贅述。在這裡我們利用了@ConfigurationProperties("jwt.config")註解從application.yml組態檔中讀取了如下設定並自動注入欄位

jwt:
  config:
    key: SecretKey #伺服器端加密所使用的密文(自擬)
    ttl: 21600000 #毫秒
#請將該段複製至application.yml以便於能夠啟動專案

回到token,token是使用者在成功登入後讀取使用者的角色和id生成得來的,生成的token應該在登陸成功後返回給前端,前端之後每次存取介面都應該帶上這個token。
注意:千萬不要使用token儲存使用者的敏感資訊,token只能用於避免被篡改,而不用於加密!token本身攜帶的內容是可以在沒有伺服器端祕鑰的情況下直接解析出來的!!!
這時候我們就可以範例化JwtUtil並使用createJWT生成token或者使用parseJWT解析token了

利用攔截器(interceptor)實現使用者存取的攔截與鑑權

在上文中我們已經能夠解析與生成一個token了,但是如果我們在每一個介面都重新解析token進行許可權認證將過於繁瑣且損耗伺服器效能,如果之前有過JavaEE專案編寫經驗的讀者應該學習過過濾器(Filter),攔截器是Spring框架為我們提供的一種類似於Filter的元件但攔截器的功能更多更強勁,話不多說直接懟程式碼,首先我們在主啟動類的同級目錄建立一個interceptor包用於存放我們的攔截器,並在這個包下建立一個名為Tokeninterceptor的類,類的內容:

/**
 * @Author Alfalfa99
 * @Date 2020/9/13 18:18
 * @Version 1.0
 * 全域性校驗Token
 */
@Component
public class TokenInterceptor implements HandlerInterceptor {

    private final JwtUtil jwtUtil;

    public TokenInterceptor(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    /**
     * 通過攔截器對請求頭進行校驗
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String header = request.getHeader("Authorization");
        if (header != null && !"".equals(header)) {
            if (header.startsWith("Bearer ")) {
                //獲得token
                String token = header.substring(7);
                //驗證token
                try {
                    Claims claims = jwtUtil.parseJWT(token);
                    String roles = (String) claims.get("roles");
                    if (roles != null) {
                        request.setAttribute("uid",claims.getId());
                        request.setAttribute("roles",roles);
                        return true;
                    } else {
                        throw new BadCredentialsException("令牌已失效");
                    }
                } catch (Exception e) {
                    throw new BadCredentialsException("令牌已失效");
                }
            }
        }
        throw new AuthenticationCredentialsNotFoundException("請先登入");
    }
}

前端每次請求時token應該是放置於請求頭中鍵名為Authorization, 值為Bearer Token(Token為伺服器端生成的token)。

首先我們使用@Component註解將該類註冊為一個Spring容器,然後使用該類實現 HandlerInterceptor介面,preHandle說明我們是在目標方法執行前進行處理。我們先通過構造器的方式將JwtUtil注入便於後續使用,然後從請求頭中讀取token,如果token為空則說明使用者沒有登陸,直接丟擲異常即可,如果在解析token出現錯誤直接丟擲錯誤即可。這一個部分的主要思路就是如果不能正確的解析出token的內容則返回給前端錯誤資訊,讓前端跳轉到登入頁面進行重新登入,如果能夠正確解析出我們儲存的使用者id和使用者角色這兩個內容則新增到請求request域裡面去,後續我們在介面處的直接可以從request中讀到使用者iduid和使用者角色roles
這樣我們就可以判斷每次存取時使用者是否已經登陸了,並且我們將使用者id以及使用者角色都存於request中便於我們之後controller的開發。

編寫InterceptorConfig

在完成TokenInterceptor類的編寫之後我們關於Token鑑權的內容就基本結束了,當然我們還需要編寫一個InterceptorConfig類去設定我們的攔截器,那麼我們在config包中建立一個名為InterceptorConfig的類,類的內容如下:

/**
 * @author 苜蓿
 * @date 2020/9/13
 * 攔截器設定類
 */
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    private final TokenInterceptor tokenInterceptor;

    public InterceptorConfig(TokenInterceptor tokenInterceptor) {
        this.tokenInterceptor = tokenInterceptor;
    }


    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //攔截所有目錄,除了通向login和register的介面
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login/**", "/**/register/**")
                .excludePathPatterns("/**/*.html", "/**/*.js", "/**/*.css");
}

那麼我們的TokenInterceptor就已經設定完成了,攔截除了html檔案、css檔案、js檔案以及登入註冊介面之外的所有存取請求並進行鑑權。

那麼本篇部落格的內容也已經完成了,在下一篇部落格我們就開始進入controller的編寫。