美文网首页SSO技术方案分布式
Spring Boot使用JWT和自定义注解完成用户登录认证和权

Spring Boot使用JWT和自定义注解完成用户登录认证和权

作者: IT志男 | 来源:发表于2019-02-22 19:47 被阅读117次

0x00 JWT(JSON Web Token)

JWT的全称是JSON Web Token,它是一种紧凑的、URL安全的,在两方传输之中提供数据的Token。

协议

参考与在线转换

中文教程参考

在用户成功的进行认证之后,可以使用用户信息来生成JWT,并将JWT设置于响应的cookie或者响应头中,在接下来的请求中使用cookie(一般用于浏览器)或者请求头(一般用于App)中的JWT来检查用户的登录状态以及权限信息。由于JWT的特点,我们认为一个有效的JWT中的信息是可信赖的。

0x01 JWT整合入Spring Boot

a.添加依赖

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

jjwt是Java生成和解析JWT的库,当然也可以自己实现。

b.编写过滤器组件

/**
 * 如果请求中(请求头或者Cookie)中存在JWT,则:
 * 1、解析JWT并查找对应的用户信息,然后加入request attribute中
 * 2、更新Cookie时间、更新JWT失效时间放入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();

        //获取相应的用户信息,可以在过滤器中先行获取,也可以先保存用户ID,在需要时进行获取
        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");

    //再从cookie中获取
    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工具类

本类是生成和验证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.注册过滤器

将特定的URL或者任意的URL注册过滤器

@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.登录方法

登录之后,在回应头中和cookie中设置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写入JWT中以便在后续过滤中进行解析和用户的查询。用户的查询如果是在集群各机器都能访问到的地方,那么使用JWT验证的方式就天然地支持了分布式的部署,同样的JWT请求到不同的机器上的效果完全是一样的。

0x02 添加注解控制用户访问权限

a.自定义权限控制注解

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

  /**
   * 访问允许的角色,默认不限制角色
   */
  String[] value() default "";

  /**
   * 访问是否需要登录,默认需要登录
   */
  boolean needLogin() default true;

}

注解定义在方法上(Controller方法上),通过指定访问需要的角色名称来控制权限

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);
      //如果是不需要登录就可以访问的API,则直接放行
      if (!userAccess.needLogin()) {
        return;
      }

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

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

      //需要登录才能访问的API,没有发现用户,返回禁止
      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定义的标准注解,可以和其他的库或者别人的代码进行无缝对接,比如Spring Security也实现在@RolesAllowed注解,对注解的方法进行了处理。

切面方法会在加入了自定义注解的方法执行前进行执行,检查用户的权限是否匹配。

相关文章

网友评论

    本文标题:Spring Boot使用JWT和自定义注解完成用户登录认证和权

    本文链接:https://www.haomeiwen.com/subject/ugosyqtx.html