Spring Boot使用JWT和自定義注解完成用戶登錄認(rèn)證和權(quán)限管理

0x00 JWT(JSON Web Token)

JWT的全稱是JSON Web Token,它是一種緊湊的、URL安全的,在兩方傳輸之中提供數(shù)據(jù)的Token。

協(xié)議

參考與在線轉(zhuǎn)換

中文教程參考

在用戶成功的進(jìn)行認(rèn)證之后,可以使用用戶信息來(lái)生成JWT,并將JWT設(shè)置于響應(yīng)的cookie或者響應(yīng)頭中,在接下來(lái)的請(qǐng)求中使用cookie(一般用于瀏覽器)或者請(qǐng)求頭(一般用于App)中的JWT來(lái)檢查用戶的登錄狀態(tài)以及權(quán)限信息。由于JWT的特點(diǎn),我們認(rèn)為一個(gè)有效的JWT中的信息是可信賴的。

0x01 JWT整合入Spring Boot

a.添加依賴

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

jjwt是Java生成和解析JWT的庫(kù),當(dāng)然也可以自己實(shí)現(xiàn)。

b.編寫(xiě)過(guò)濾器組件

/**
 * 如果請(qǐng)求中(請(qǐng)求頭或者Cookie)中存在JWT,則:
 * 1、解析JWT并查找對(duì)應(yīng)的用戶信息,然后加入request attribute中
 * 2、更新Cookie時(shí)間、更新JWT失效時(shí)間放入Header
 */
@Component
@Slf4j
public class WebSecurityFilter extends OncePerRequestFilter {

  public static final String SECURITY_USER = "SECURITY_USER";

  @Resource
  private JwtUtil jwtUtil;

  @Resource
  private UserRepository userRepository;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    try {

      String jwt = getJwt(request);

      if (StringUtils.isNotBlank(jwt) && jwtUtil.validateJwtToken(jwt) != null) {
        Jws<Claims> claimsJws = jwtUtil.validateJwtToken(jwt);
        String userUid = claimsJws.getBody().getSubject();

        //獲取相應(yīng)的用戶信息,可以在過(guò)濾器中先行獲取,也可以先保存用戶ID,在需要時(shí)進(jìn)行獲取
        Optional<User> optionalUser = userRepository.findUserByUserUid(userUid);

        if (optionalUser.isPresent()) {

          request.setAttribute(SECURITY_USER, optionalUser.get());

          jwt = "Bearer " + jwtUtil.refreshJwt(jwt);

          Cookie jwtCookie = new Cookie("Authorization", URLEncoder.encode(jwt, "UTF-8"));
          jwtCookie.setHttpOnly(true);
          jwtCookie.setMaxAge(jwtUtil.getJwtExpiration());
          jwtCookie.setPath("/");
          response.addCookie(jwtCookie);

          response.addHeader("Authorization", jwt);
        }

      }

    } catch (Exception e) {
      log.error("Can NOT set user authentication -> Message: {}", e);
    }

    filterChain.doFilter(request, response);
  }

  private String getJwt(HttpServletRequest request) {

    //先從header中獲取
    String authHeader = request.getHeader("Authorization");

    //再?gòu)腸ookie中獲取
    if (StringUtils.isBlank(authHeader)) {
      try {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
          for (Cookie cookie : cookies) {
            if ("Authorization".equals(cookie.getName())) {
              authHeader = URLDecoder.decode(cookie.getValue(), "UTF-8");
            }
          }
        }
      } catch (Exception e) {
        log.error("Can NOT get jwt from cookie -> Message: {}", e);
      }
    }

    if (StringUtils.startsWith(authHeader, "Bearer ")) {
      authHeader = authHeader.replace("Bearer ", "");
    }

    return authHeader;
  }
}

c.JWT工具類

本類是生成和驗(yàn)證JWT的方法

@Component
@Slf4j
public class JwtUtil {

  @Value("${jwt.secretKey:paycms}")
  private String jwtSecret;

  @Value("${jwt.expiration:86400}")
  private int jwtExpiration;

  public int getJwtExpiration() {
    return this.jwtExpiration;
  }

  public static final String CLAIM_KEY_ROLES = "roles";


  public String generateJwt(String subject, LocalDateTime localDateTime, Map<String, Object> claims) {

    Date issued = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
    Date expiration = Date.from(localDateTime.plusSeconds(jwtExpiration).atZone(ZoneId.systemDefault()).toInstant());

    JwtBuilder jwtBuilder = Jwts.builder()
      .setId(UUID.randomUUID().toString())
      .setSubject(subject)
      .setIssuedAt(issued)
      .setExpiration(expiration)
      .signWith(SignatureAlgorithm.HS512, jwtSecret);

    if (claims.get(CLAIM_KEY_ROLES) != null) {
      jwtBuilder.claim(CLAIM_KEY_ROLES, claims.get(CLAIM_KEY_ROLES));
    }

    return jwtBuilder.compact();
  }

  public String refreshJwt(String jwt) {
    Jws<Claims> claimsJws = validateJwtToken(jwt);
    String subject = claimsJws.getBody().getSubject();
    Object roles = claimsJws.getBody().get(CLAIM_KEY_ROLES);
    Map<String, Object> claims = new HashMap<>();
    if (roles != null) {
      claims.put(CLAIM_KEY_ROLES, roles);
    }

    return generateJwt(subject, LocalDateTime.now(), claims);
  }

  public Jws<Claims> validateJwtToken(String authToken) {
    Jws<Claims> claimsJws = null;

    if (StringUtils.isNotBlank(authToken)) {
      try {
        claimsJws = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
      } catch (SignatureException e) {
        log.error("Invalid JWT signature -> Message: {} ", e);
      } catch (MalformedJwtException e) {
        log.error("Invalid JWT token -> Message: {}", e);
      } catch (ExpiredJwtException e) {
        log.error("Expired JWT token -> Message: {}", e);
      } catch (UnsupportedJwtException e) {
        log.error("Unsupported JWT token -> Message: {}", e);
      } catch (IllegalArgumentException e) {
        log.error("JWT claims string is empty -> Message: {}", e);
      }
    }

    return claimsJws;
  }

}

d.注冊(cè)過(guò)濾器

將特定的URL或者任意的URL注冊(cè)過(guò)濾器

@Configuration
public class WebSecurityFilterConfig {

  @Resource
  private WebSecurityFilter webSecurityFilter;

  @Bean
  public FilterRegistrationBean webSecurityFilterRegistration() {

    FilterRegistrationBean<WebSecurityFilter> registration = new FilterRegistrationBean<>();
    registration.setFilter(webSecurityFilter);
    registration.addUrlPatterns("/api/*");
    registration.setOrder(1);
    return registration;
  }

}

e.登錄方法

登錄之后,在回應(yīng)頭中和cookie中設(shè)置JWT

String jwt = "Bearer " + jwtUtil.generateJwt("get user id from some where", LocalDateTime.now(), 
  ImmutableMap.of(CLAIM_KEY_ROLES, "some roles"));

Cookie jwtCookie = new Cookie("Authorization", URLEncoder.encode(jwt, "UTF-8"));
jwtCookie.setHttpOnly(true);
jwtCookie.setMaxAge(jwtUtil.getJwtExpiration());
response.addCookie(jwtCookie);

response.addHeader("Authorization", jwt);

將用戶ID寫(xiě)入JWT中以便在后續(xù)過(guò)濾中進(jìn)行解析和用戶的查詢。用戶的查詢?nèi)绻窃诩焊鳈C(jī)器都能訪問(wèn)到的地方,那么使用JWT驗(yàn)證的方式就天然地支持了分布式的部署,同樣的JWT請(qǐng)求到不同的機(jī)器上的效果完全是一樣的。

0x02 添加注解控制用戶訪問(wèn)權(quán)限

a.自定義權(quán)限控制注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAccess {

  /**
   * 訪問(wèn)允許的角色,默認(rèn)不限制角色
   */
  String[] value() default "";

  /**
   * 訪問(wèn)是否需要登錄,默認(rèn)需要登錄
   */
  boolean needLogin() default true;

}

注解定義在方法上(Controller方法上),通過(guò)指定訪問(wèn)需要的角色名稱來(lái)控制權(quán)限

b.加入切面方法(spring aop)

@Aspect
@Component
@Slf4j
public class WebSecurityAspect {

  @Before(value = "@annotation(UserAccess)")
  public void authoritiesCheck(JoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    log.info("user access security check for method: {}", methodName);

    try {
      UserAccess userAccess = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(UserAccess.class);
      //如果是不需要登錄就可以訪問(wèn)的API,則直接放行
      if (!userAccess.needLogin()) {
        return;
      }

      HttpServletRequest request =
        ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

      User user = (User) request.getAttribute(WebSecurityFilter.SECURITY_USER);

      //需要登錄才能訪問(wèn)的API,沒(méi)有發(fā)現(xiàn)用戶,返回禁止
      if (user == null) {
        throw new ForbiddenAccessException("User Not Found");
      }

      //value為空直接放行,代表登錄即可
      String[] permitRoles = userAccess.value();
      if (ArrayUtils.isEmpty(permitRoles)) {
        return;
      }

      if (CollectionUtils.isEmpty(user.getUserRoles())) {
        throw new UnauthorizedAccessException("User Has No Authorities");
      }

      Set<String> roles = user.getUserRoles().stream().map(UserRole::getRoleCode).collect(Collectors.toSet());

      Set<String> intersectionRoles =
        Stream.of(permitRoles).filter(roles::contains).collect(Collectors.toSet());

      if (CollectionUtils.isEmpty(intersectionRoles)) {
        throw new UnauthorizedAccessException("User Has No Authorities");
      }

    } catch (UnauthorizedAccessException | ForbiddenAccessException e) {
      throw e;
    } catch (Exception e) {
      log.error("user access security check Exception: {}", methodName, e);
      throw e;
    }

  }
}

c.注解的使用

直接定義在controller方法上即可,例如

@RequestMapping(value = "/api/protected", method = RequestMethod.GET)
@ResponseBody
@UserAccess(Role.Constants.SYS_ADMIN)

另外,可以使用自定義的@UserAccess注解,也可以使用JSR-250 javax.annotation.security.RolesAllowed 注解,好處是使用了Java定義的標(biāo)準(zhǔn)注解,可以和其他的庫(kù)或者別人的代碼進(jìn)行無(wú)縫對(duì)接,比如Spring Security也實(shí)現(xiàn)在@RolesAllowed注解,對(duì)注解的方法進(jìn)行了處理。

切面方法會(huì)在加入了自定義注解的方法執(zhí)行前進(jìn)行執(zhí)行,檢查用戶的權(quán)限是否匹配。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容