SkyBlog中的Token認證機制

Session認證與Token認證的取捨

在項目剛剛開始的時候,我還是規劃使用Session認證的,期間遇到了不少問題。

  1. Session認證是通過把Cookie交給服務端管理的,而fetch在設置credentials時又會要求CORS的Access-Control-Allow-Origin不能設置爲*,必須指定域名。
  2. 前後端分離中,前端還是自行管理狀態纔是真正的前後端分離
  3. 後臺提供的RESTfulAPI,指定域名不符合RESTful的設計要求

所以最終我決定改用Token認證,不過不使用OAuth2,而是自己編寫相關邏輯

Token的生成使用的是JWT,減少請求時對數據庫的查詢

Token認證源碼解析

項目是使用Spring Security作爲安全框架,對用戶進行認證、權限管理,所以要集成JWT的話,有幾點要關注:

  1. 將存儲在Header中的JWT轉換爲Spring SecurityUserDetails
  2. 將必要的信息存儲在Token當中,返回給用戶
  3. 提供獲取、刷新Token的接口

所以就有了以下幾個類

TokenFilter

負責將請求中的Token轉換爲UsernamePasswordAuthenticationToken

        // 有時請求會錯誤發送null與undefined
        final String nullStr = "null";
        final String undefinedStr = "undefined";

        String token = getRequestToken(request);

        // 如果token不存在,則直接放行,由之後的Filter攔截
        if(StringUtils.isBlank(token) ||
                nullStr.equals(token) ||
                undefinedStr.equals(token)) {
            chain.doFilter(request,response);
            return;
        }

        // 從Token中讀取User,設置Authentication
        SecurityUser user = securityService.getUserByToken(token);
        if(user != null && SecurityUtils.getAuthentication() == null){
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        chain.doFilter(request,response);

SecurityService

負責提供Token相關的服務

  1. 登錄時,調用Spring SecurityAuthenticationManager,對用戶進行驗證,並在驗證通過時生成Token
  2. Token中包裝了三段重要的信息:id、username、role,通過這些信息便足以組成一個用戶的基本對象
  3. 服務提供了從Token中獲取各類信息的方法,也提供了驗證Token是否合法、過期的方法,確保Token的可靠性
   @Override
    public AccessToken login(String username, String password) {
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        HttpServletRequest request = ServletUtils.getRequest();
        authRequest.setDetails(new WebAuthenticationDetails(request));

        try{
            Authentication authentication = authenticationManager.authenticate(authRequest);
            SecurityUser user = (SecurityUser) authentication.getPrincipal();

            return generateToken(user);
        }catch (BadCredentialsException e){
            throw new CommonException("用戶名/密碼不正確");
        }catch (LockedException e){
            throw new CommonException("賬號被鎖定,請聯繫管理員");
        }
    }

    @Override
    public AccessToken generateToken(SecurityUser user) {
        if(user.getLocked()){
            throw new CommonException("賬號被鎖定,請聯繫管理員");
        }

        AccessToken accessToken = new AccessToken();
        long expire = jwtSettings.getExpire();

        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        // 構建JWT
        String token = Jwts.builder()

                // 將User的標識信息放入JWT中
                .setSubject(String.valueOf(user.getId()))
                .claim(CLAIM_USERNAME,user.getUsername())
                .claim(CLAIM_ROLE,user.getRole())

                // 設置過期時間
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)

                // 設置加密方式
                .signWith(SignatureAlgorithm.HS512,
                        jwtSettings.getSecret())
                .compact();

        accessToken.setToken(token);
        accessToken.setExpire(expire);
        accessToken.setUser(user);

        return accessToken;
    }

AuthController

負責提供登錄以及刷新Token的接口

   /**
     * @param username 用戶名
     * @param password 密碼
     * @return AccessToken
     */
    @ApiOperation(value = "登錄,獲取Token")
    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public Result<AccessToken> login(@ApiParam("用戶名") @RequestParam String username,
                                     @ApiParam("密碼") @RequestParam String password) {
        AccessToken accessToken = securityService.login(username,password);
        return Result.ok(accessToken);
    }

    /**
     * 通過老Token換取新Token
     * @param token 老Token
     * @return 新Token
     */
    @ApiOperation("通過老Token換取新Token")
    @RequestMapping(value = "/refresh",method = RequestMethod.POST)
    public Result<AccessToken> refresh(@ApiParam("老Token") @RequestParam String token){
        Integer userId = securityService.getIDByToken(token);
        UserDO user = userService.getByID(userId);

        AccessToken accessToken = securityService.generateToken(new SecurityUser(user));
        return Result.ok(accessToken);
    }

至此,便完成了Spring SecurityJWT的整合便完成了。

發佈了24 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章