學習Spring Boot:(十六)使用Shiro與JWT 實現認證服務


前言

需要把Web應用做成無狀態的,即服務器端無狀態,就是說服務器端不會存儲像會話這種東西,而是每次請求時access_token進行資源訪問。這里我們將使用 JWT 1,基於散列的消息認證碼,使用一個密鑰和一個消息作為輸入,生成它們的消息摘要。該密鑰只有服務端知道。訪問時使用該消息摘要進行傳播,服務端然后對該消息摘要進行驗證。

認證步驟

  1. 客戶端第一次使用用戶名密碼訪問認證服務器,服務器驗證用戶名和密碼,認證成功,使用用戶密鑰生成JWT並返回
  2. 之后每次請求客戶端帶上JWT
  3. 服務器對JWT進行驗證

自定義 jwt 攔截器

/** * oauth2攔截器,現在改為 JWT 認證 */
public class OAuth2Filter extends FormAuthenticationFilter {
    /** * 設置 request 的鍵,用來保存 認證的 userID, */
    private final static String USER_ID = "USER_ID";
    @Resource
    private JwtUtils jwtUtils;

    /** * logger */
    private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class);


    /** * shiro權限攔截核心方法 返回true允許訪問resource, * * @param request * @param response * @param mappedValue * @return */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        String token = getRequestToken((HttpServletRequest) request);
        try {
            // 檢查 token 有效性
            //ExpiredJwtException JWT已過期
            //SignatureException JWT可能被篡改
            Jwts.parser().setSigningKey(jwtUtils.getSecret()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            // 身份驗證失敗,返回 false 將進入onAccessDenied 判斷是否登陸。
            onLoginFail(response);
            return false;
        }
        Long userId = getUserIdFromToken(token);
        // 存入到 request 中,在后面的業務處理中可以使用
        request.setAttribute(USER_ID, userId);
        return true;
    }

    /** * 當訪問拒絕時是否已經處理了; * 如果返回true表示需要繼續處理; * 如果返回false表示該攔截器實例已經處理完成了,將直接返回即可。 * * @param request * @param response * @return * @throws Exception */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            onLoginFail(response);
            return false;
        }
    }

    /** * 鑒定失敗,返回錯誤信息 * @param token * @param e * @param request * @param response * @return */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        try {
            ((HttpServletResponse) response).setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().print("賬號活密碼錯誤");
        } catch (IOException e1) {
            LOGGER.error(e1.getMessage(), e1);
        }
        return false;
    }

    /** * token 認證失敗 * * @param response */
    private void onLoginFail(ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
        try {
            response.getWriter().print("沒有權限,請聯系管理員授權");
        } catch (IOException e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /** * 獲取請求的token */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //從header中獲取token
        String token = httpRequest.getHeader(jwtUtils.getHeader());
        //如果header中不存在token,則從參數中獲取token
        if (StringUtils.isBlank(token)) {
            return httpRequest.getParameter(jwtUtils.getHeader());
        }
        if (StringUtils.isBlank(token)) {
            // 從 cookie 獲取 token
            Cookie[] cookies = httpRequest.getCookies();
            if (null == cookies || cookies.length == 0) {
                return null;
            }
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(jwtUtils.getHeader())) {
                    token = cookie.getValue();
                    break;
                }
            }
        }
        return token;
    }

    /** * 根據 token 獲取 userID * * @param token token * @return userId */
    private Long getUserIdFromToken(String token) {
        if (StringUtils.isBlank(token)) {
            throw new KCException("無效 token", HttpStatus.UNAUTHORIZED.value());
        }
        Claims claims = jwtUtils.getClaimByToken(token);
        if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
            throw new KCException(jwtUtils.getHeader() + "失效,請重新登錄", HttpStatus.UNAUTHORIZED.value());
        }
        return Long.parseLong(claims.getSubject());
    }

}

將自定義shiro攔截器,設置到 ShiroFilterFactoryBean 中,然后將需要進行權限驗證的 path 進行設置攔截過濾。

登陸

    @PostMapping("/login")
    @ApiOperation("系統登陸")
    public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {
        String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
        if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {
            throw new KCException("驗證碼不正確!");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.login(token);

        //賬號鎖定
        if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {
            throw new KCException("賬號已被鎖定,請聯系管理員");
        }
        // 登陸成功后直接返回 token ,然后后續放到 header 中認證
        return ResponseEntity.status(HttpStatus.OK).body(jwtUtils.generateToken(getUserId()));
    }

JwtUtils

我前面給 jwt 設置了三個參數

# jwt 配置
jwt:
  # 加密密鑰
  secret: 61D73234C4F93E03074D74D74D1E39D9 #blog.wuwii.com
  # token有效時長
  expire: 7 # 7天,單位天
  # token 存在 header 中的參數
  header: token

jwt 工具類的編寫

@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtils {
    /** * logger */
    private Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    /** * 密鑰 */
    private String secret;
    /** * 有效期限 */
    private int expire;
    /** * 存儲 token */
    private String header;

    /** * 生成jwt token * * @param userId 用戶ID * @return token */
    public String generateToken(long userId) {
        Date nowDate = new Date();

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                // 后續獲取 subject 是 userid
                .setSubject(userId + "")
                .setIssuedAt(nowDate)
                .setExpiration(DateUtils.addDays(nowDate, expire))
                // 這里我采用的是 HS512 算法
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /** * 解析 token, * 利用 jjwt 提供的parser傳入秘鑰, * * @param token token * @return 數據聲明 Map<String, Object> */
    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /** * token是否過期 * * @return true:過期 */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }
}

總結

由於 JWT 這種方式,服務端不需要保存任何狀態,所以服務端不需要使用 session 保存用戶信息,單元測試也比較方便,雖然中間轉碼解碼會消耗一些性能,但是影響不大,還比較方便的應用在 SSO 2


  1. JSON WEB Token
  2. Single Sign On


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM